Skip to content

Commit 4a02609

Browse files
committed
Refactor: modernize plugin with operation-based API
- Upgrade to .NET 10.0 and FlowSynx.PluginCore 1.4.0 - Replace handler-based API with strongly-typed operations (Extract, Map, Transform) - Introduce per-operation parameter classes and metadata - Add ParseDataHelper for input normalization - Remove legacy handler interfaces and InputParameter - Use strongly-typed JsonPluginSpecifications - Overhaul test suite for new API and parameter validation - Improve error handling, validation, and extensibility - Set minimum FlowSynx version to 1.3.0 #9
1 parent 47b45e6 commit 4a02609

23 files changed

Lines changed: 533 additions & 671 deletions

src/FlowSynx.Plugins.Json.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net9.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="FlowSynx.PluginCore" Version="1.3.2" />
10+
<PackageReference Include="FlowSynx.PluginCore" Version="1.4.0" />
1111
</ItemGroup>
1212

1313
<ItemGroup>

src/Helpers/ParseDataHelper.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using FlowSynx.PluginCore;
2+
using FlowSynx.Plugins.Json.Services;
3+
4+
namespace FlowSynx.Plugins.Json.Helpers;
5+
6+
internal class ParseDataHelper
7+
{
8+
private readonly IGuidProvider _guidProvider;
9+
10+
public ParseDataHelper(IGuidProvider guidProvider)
11+
{
12+
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
13+
}
14+
15+
public PluginContext ParseDataToContext(object? data)
16+
{
17+
if (data is null)
18+
throw new ArgumentNullException(nameof(data), "Input data cannot be null.");
19+
20+
return data switch
21+
{
22+
PluginContext singleContext => singleContext,
23+
IEnumerable<PluginContext> => throw new NotSupportedException("List of PluginContext is not supported."),
24+
string strData => new PluginContext(_guidProvider.NewGuid().ToString(), "Data") { Content = strData },
25+
_ => throw new NotSupportedException("Unsupported input data format.")
26+
};
27+
}
28+
}

src/JsonPlugin.cs

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
using FlowSynx.PluginCore;
22
using FlowSynx.PluginCore.Extensions;
3-
using FlowSynx.Plugins.Json.Models;
3+
using FlowSynx.Plugins.Json.Operations.Extract;
4+
using FlowSynx.Plugins.Json.Operations.Map;
5+
using FlowSynx.Plugins.Json.Operations.Transform;
46
using FlowSynx.Plugins.Json.Services;
5-
using Newtonsoft.Json.Linq;
67

78
namespace FlowSynx.Plugins.Json;
89

910
public class JsonPlugin : IPlugin
1011
{
1112
private readonly IGuidProvider _guidProvider;
1213
private readonly IReflectionGuard _reflectionGuard;
14+
private JsonPluginSpecifications? _specifications = null;
1315
private IPluginLogger? _logger;
1416
private bool _isInitialized;
1517

@@ -27,7 +29,7 @@ internal JsonPlugin(IGuidProvider guidProvider, IReflectionGuard reflectionGuard
2729
Name = "Json",
2830
CompanyName = "FlowSynx",
2931
Description = Resources.PluginDescription,
30-
Version = new Version(1, 1, 1),
32+
Version = new Version(1, 2, 0),
3133
Category = PluginCategory.Data,
3234
Authors = new List<string> { "FlowSynx" },
3335
Copyright = "© FlowSynx. All rights reserved.",
@@ -36,34 +38,40 @@ internal JsonPlugin(IGuidProvider guidProvider, IReflectionGuard reflectionGuard
3638
RepositoryUrl = "https://github.com/flowsynx/plugin-json",
3739
ProjectUrl = "https://flowsynx.io",
3840
Tags = new List<string>() { "flowSynx", "json", "data", "data-platform" },
39-
MinimumFlowSynxVersion = new Version(1, 1, 1),
41+
MinimumFlowSynxVersion = new Version(1, 3, 0),
4042
};
4143

42-
public PluginSpecifications? Specifications { get; set; }
44+
public IPluginSpecifications? Specifications => _specifications;
4345

44-
public Type SpecificationsType => typeof(JsonPluginSpecifications);
45-
46-
private Dictionary<string, IJsonOperationHandler> OperationMap => new(StringComparer.OrdinalIgnoreCase)
46+
public IReadOnlyCollection<IPluginOperation> SupportedOperations { get; } = new IPluginOperation[]
4747
{
48-
["extract"] = new ExtractOperationHandler(_guidProvider),
49-
["map"] = new MapOperationHandler(_guidProvider),
50-
["transform"] = new TransformOperationHandler(_guidProvider)
48+
new ExtractOperation(),
49+
new MapOperation(),
50+
new TransformOperation()
5151
};
5252

53-
public IReadOnlyCollection<string> SupportedOperations => OperationMap.Keys;
54-
55-
public Task Initialize(IPluginLogger logger)
53+
public Task InitializeAsync(IPluginLogger logger, IDictionary<string, object?>? specifications)
5654
{
5755
if (_reflectionGuard.IsCalledViaReflection())
5856
throw new InvalidOperationException(Resources.ReflectionBasedAccessIsNotAllowed);
5957

60-
ArgumentNullException.ThrowIfNull(logger);
61-
_logger = logger;
58+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
59+
60+
var jsonSpecifications = new JsonPluginSpecifications();
61+
if (specifications != null)
62+
jsonSpecifications.FromDictionary(specifications);
63+
64+
jsonSpecifications.Validate();
65+
_specifications = jsonSpecifications;
66+
6267
_isInitialized = true;
6368
return Task.CompletedTask;
6469
}
6570

66-
public Task<object?> ExecuteAsync(PluginParameters parameters, CancellationToken cancellationToken)
71+
public async Task<object?> ExecuteAsync(
72+
string? operationName,
73+
PluginParameters parameters,
74+
CancellationToken cancellationToken)
6775
{
6876
cancellationToken.ThrowIfCancellationRequested();
6977

@@ -73,30 +81,22 @@ public Task Initialize(IPluginLogger logger)
7381
if (!_isInitialized)
7482
throw new InvalidOperationException($"Plugin '{Metadata.Name}' v{Metadata.Version} is not initialized.");
7583

76-
var inputParameter = parameters.ToObject<InputParameter>();
77-
if (!OperationMap.TryGetValue(inputParameter.Operation, out var handler))
84+
var operation = SupportedOperations
85+
.FirstOrDefault(op => string.Equals(op.Name, operationName, StringComparison.OrdinalIgnoreCase))
86+
?? throw new NotSupportedException($"Operation '{operationName}' is not supported.");
87+
88+
return operation.Name.ToLowerInvariant() switch
7889
{
79-
throw new NotSupportedException($"Operation '{inputParameter.Operation}' is not supported.");
80-
}
90+
"extract" => await ((ExtractOperation)operation)
91+
.ExecuteAsync(parameters.ToObject<ExtractParameters>(), cancellationToken),
8192

82-
var context = ParseDataToContext(inputParameter.Data);
83-
var json = context.Content ?? throw new ArgumentException("Input JSON is required.");
93+
"map" => await ((MapOperation)operation)
94+
.ExecuteAsync(parameters.ToObject<MapParameters>(), cancellationToken),
8495

85-
var jsonToken = JToken.Parse(json);
86-
return Task.FromResult(handler.Handle(jsonToken, inputParameter));
87-
}
96+
"transform" => await ((TransformOperation)operation)
97+
.ExecuteAsync(parameters.ToObject<TransformParameters>(), cancellationToken),
8898

89-
private PluginContext ParseDataToContext(object? data)
90-
{
91-
if (data is null)
92-
throw new ArgumentNullException(nameof(data), "Input data cannot be null.");
93-
94-
return data switch
95-
{
96-
PluginContext singleContext => singleContext,
97-
IEnumerable<PluginContext> => throw new NotSupportedException("List of PluginContext is not supported."),
98-
string strData => new PluginContext(_guidProvider.NewGuid().ToString(), "Data") { Content = strData },
99-
_ => throw new NotSupportedException("Unsupported input data format.")
99+
_ => throw new InvalidOperationException($"Unsupported operation: {operation.Name}")
100100
};
101101
}
102102
}

src/JsonPluginSpecifications.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using FlowSynx.PluginCore;
2+
3+
namespace FlowSynx.Plugins.Json;
4+
5+
public class JsonPluginSpecifications : PluginSpecifications
6+
{
7+
public override void Validate()
8+
{
9+
10+
}
11+
}

src/Models/InputParameter.cs

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/Models/JsonPluginSpecifications.cs

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/Services/ExtractOperationHandler.cs renamed to src/Operations/Extract/ExtractOperation.cs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
11
using FlowSynx.PluginCore;
2-
using FlowSynx.Plugins.Json.Models;
2+
using FlowSynx.Plugins.Json.Helpers;
3+
using FlowSynx.Plugins.Json.Services;
34
using Newtonsoft.Json;
45
using Newtonsoft.Json.Linq;
56

6-
namespace FlowSynx.Plugins.Json.Services;
7+
namespace FlowSynx.Plugins.Json.Operations.Extract;
78

8-
internal class ExtractOperationHandler : IJsonOperationHandler
9+
internal class ExtractOperation : IPluginOperation<ExtractParameters, PluginContext>
910
{
10-
private readonly IGuidProvider _guidProvider;
11+
private readonly IGuidProvider _guidProvider = new GuidProvider();
1112

12-
public ExtractOperationHandler(IGuidProvider guidProvider)
13-
{
14-
_guidProvider = guidProvider;
15-
}
13+
public string Name => "Extract";
14+
public string Description => "Extracts data from a JSON content.";
1615

17-
public object Handle(JToken json, InputParameter inputParameter)
16+
public async Task<PluginContext?> ExecuteAsync(ExtractParameters parameters, CancellationToken cancellationToken)
1817
{
19-
string? path = inputParameter.Path;
18+
string? path = parameters.Path;
2019
if (string.IsNullOrWhiteSpace(path))
2120
throw new ArgumentException("Path parameter is required for extract.");
2221

23-
var tokens = json.SelectTokens(path).ToList();
22+
var helper = new ParseDataHelper(_guidProvider);
23+
24+
var jsonCtx = helper.ParseDataToContext(parameters.Data);
25+
if (string.IsNullOrWhiteSpace(jsonCtx.Content))
26+
throw new ArgumentException("Input JSON content cannot be empty.");
27+
var jsonToken = JToken.Parse(jsonCtx.Content);
28+
var tokens = jsonToken.SelectTokens(path).ToList();
2429
string filename = $"{_guidProvider.NewGuid()}.json";
2530

2631
List<Dictionary<string, object>>? structuredData = null;
@@ -43,7 +48,7 @@ public object Handle(JToken json, InputParameter inputParameter)
4348
return new PluginContext(filename, "Data")
4449
{
4550
Format = "Json",
46-
Content = result.ToString(inputParameter.Indented ? Formatting.Indented : Formatting.None),
51+
Content = result.ToString(parameters.Indented ? Formatting.Indented : Formatting.None),
4752
StructuredData = structuredData
4853
};
4954
}
@@ -57,6 +62,8 @@ public object Handle(JToken json, InputParameter inputParameter)
5762
{
5863
if (item is JObject obj)
5964
list.Add(obj.Properties().ToDictionary(p => p.Name, p => (object)p.Value.Type.ToString()));
65+
else if (item is JValue jv)
66+
list.Add(new Dictionary<string, object> { { "$value", jv.Type.ToString() } });
6067
}
6168
return list.Count > 0 ? list : null;
6269
}
@@ -67,6 +74,13 @@ public object Handle(JToken json, InputParameter inputParameter)
6774
obj2.Properties().ToDictionary(p => p.Name, p => (object)p.Value.Type.ToString())
6875
};
6976
}
77+
if (token is JValue jv2)
78+
{
79+
return new List<Dictionary<string, object>>
80+
{
81+
new Dictionary<string, object> { { "$value", jv2.Type.ToString() } }
82+
};
83+
}
7084
return null;
7185
}
7286
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using FlowSynx.PluginCore;
2+
3+
namespace FlowSynx.Plugins.Json.Operations.Extract;
4+
5+
internal class ExtractParameters
6+
{
7+
[OperationParameterMetadata(Description = "The JSON data to extract information from.", IsRequired = true)]
8+
public object? Data { get; set; }
9+
10+
[OperationParameterMetadata(Description = "The JSON path to extract specific data.", IsRequired = true)]
11+
public string? Path { get; set; }
12+
13+
[OperationParameterMetadata(Description = "Whether to indent the output JSON for readability.", IsRequired = false)]
14+
public bool Indented { get; set; } = false;
15+
}

src/Operations/Map/MapOperation.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using FlowSynx.PluginCore;
2+
using FlowSynx.Plugins.Json.Helpers;
3+
using FlowSynx.Plugins.Json.Services;
4+
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Linq;
6+
7+
namespace FlowSynx.Plugins.Json.Operations.Map;
8+
9+
internal class MapOperation : IPluginOperation<MapParameters, PluginContext>
10+
{
11+
private readonly IGuidProvider _guidProvider = new GuidProvider();
12+
13+
public string Name => "Map";
14+
public string Description => "Maps data from one structure to another.";
15+
16+
public async Task<PluginContext?> ExecuteAsync(MapParameters parameters, CancellationToken cancellationToken)
17+
{
18+
if (parameters.Mappings == null)
19+
throw new InvalidOperationException("Mappings not defined in specifications.");
20+
21+
var helper = new ParseDataHelper(_guidProvider);
22+
23+
var context = helper.ParseDataToContext(parameters.Data);
24+
var jsonToken = JToken.Parse(context.Content);
25+
26+
object mappedResult;
27+
List<Dictionary<string, object>>? structuredData = null;
28+
29+
if (jsonToken is JArray array)
30+
{
31+
var mappedList = array
32+
.OfType<JObject>()
33+
.Select(obj => MapSingleObject(obj, parameters.Mappings))
34+
.ToList();
35+
mappedResult = mappedList;
36+
structuredData = mappedList
37+
.Select(obj => obj.ToDictionary(kvp => kvp.Key, kvp => (object)(kvp.Value?.GetType()?.Name ?? "null")))
38+
.ToList();
39+
}
40+
else if (jsonToken is JObject obj)
41+
{
42+
var mappedObj = MapSingleObject(obj, parameters.Mappings);
43+
mappedResult = mappedObj;
44+
structuredData = new List<Dictionary<string, object>>
45+
{
46+
mappedObj.ToDictionary(kvp => kvp.Key, kvp => (object)(kvp.Value?.GetType()?.Name ?? "null"))
47+
};
48+
}
49+
else
50+
{
51+
throw new NotSupportedException("HandleMap only supports JSON objects or arrays of objects.");
52+
}
53+
54+
string convertedJson = JsonConvert.SerializeObject(mappedResult, parameters.Indented ? Formatting.Indented : Formatting.None);
55+
string filename = $"{_guidProvider.NewGuid()}.json";
56+
return new PluginContext(filename, "Data")
57+
{
58+
Format = "Json",
59+
Content = convertedJson,
60+
StructuredData = structuredData
61+
};
62+
}
63+
64+
private Dictionary<string, object?> MapSingleObject(JObject obj, Dictionary<string, string> mappings)
65+
{
66+
var result = new Dictionary<string, object?>();
67+
68+
foreach (var kvp in mappings)
69+
{
70+
var value = obj.SelectToken(kvp.Value);
71+
if (value == null)
72+
{
73+
result[kvp.Key] = null;
74+
continue;
75+
}
76+
77+
// Convert primitives to CLR strings to avoid leaking JValue into the result
78+
switch (value.Type)
79+
{
80+
case JTokenType.Object:
81+
case JTokenType.Array:
82+
// Keep complex types as-is
83+
result[kvp.Key] = value;
84+
break;
85+
default:
86+
// Ensure primitives are represented as strings (as expected by tests)
87+
result[kvp.Key] = value.ToString();
88+
break;
89+
}
90+
}
91+
92+
return result;
93+
}
94+
}

0 commit comments

Comments
 (0)