Skip to content

Commit c7da128

Browse files
committed
Huge refactoring to make it testable and write unit tests for code
#2
1 parent fe3991c commit c7da128

20 files changed

Lines changed: 878 additions & 118 deletions

Plugins.Json.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlowSynx.Plugins.Json", "sr
77
EndProject
88
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DEC33F76-6AA7-41D1-9ADE-C5CFC3B1185C}"
99
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{48C12015-81A9-44DD-94A9-9E7A5D12305D}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlowSynx.Plugin.Json.UnitTests", "tests\FlowSynx.Plugin.Json.UnitTests\FlowSynx.Plugin.Json.UnitTests.csproj", "{F986079F-4C54-4BDC-AD63-9CFF25834DDC}"
13+
EndProject
1014
Global
1115
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1216
Debug|Any CPU = Debug|Any CPU
@@ -17,11 +21,16 @@ Global
1721
{46F69BA2-2760-4A30-A813-FDDEE427C068}.Debug|Any CPU.Build.0 = Debug|Any CPU
1822
{46F69BA2-2760-4A30-A813-FDDEE427C068}.Release|Any CPU.ActiveCfg = Release|Any CPU
1923
{46F69BA2-2760-4A30-A813-FDDEE427C068}.Release|Any CPU.Build.0 = Release|Any CPU
24+
{F986079F-4C54-4BDC-AD63-9CFF25834DDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25+
{F986079F-4C54-4BDC-AD63-9CFF25834DDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
26+
{F986079F-4C54-4BDC-AD63-9CFF25834DDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{F986079F-4C54-4BDC-AD63-9CFF25834DDC}.Release|Any CPU.Build.0 = Release|Any CPU
2028
EndGlobalSection
2129
GlobalSection(SolutionProperties) = preSolution
2230
HideSolutionNode = FALSE
2331
EndGlobalSection
2432
GlobalSection(NestedProjects) = preSolution
2533
{46F69BA2-2760-4A30-A813-FDDEE427C068} = {DEC33F76-6AA7-41D1-9ADE-C5CFC3B1185C}
34+
{F986079F-4C54-4BDC-AD63-9CFF25834DDC} = {48C12015-81A9-44DD-94A9-9E7A5D12305D}
2635
EndGlobalSection
2736
EndGlobal

src/FlowSynx.Plugins.Json.csproj

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

3-
<PropertyGroup>
4-
<TargetFramework>net9.0</TargetFramework>
5-
<ImplicitUsings>enable</ImplicitUsings>
6-
<Nullable>enable</Nullable>
7-
</PropertyGroup>
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
88

9-
<ItemGroup>
10-
<PackageReference Include="FlowSynx.PluginCore" Version="1.3.0" />
11-
</ItemGroup>
9+
<ItemGroup>
10+
<PackageReference Include="FlowSynx.PluginCore" Version="1.3.0" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<InternalsVisibleTo Include="FlowSynx.Plugin.Json.UnitTests" />
15+
</ItemGroup>
1216

13-
<ItemGroup>
14-
<Compile Update="Resources.Designer.cs">
15-
<DesignTime>True</DesignTime>
16-
<AutoGen>True</AutoGen>
17-
<DependentUpon>Resources.resx</DependentUpon>
18-
</Compile>
19-
</ItemGroup>
17+
<ItemGroup>
18+
<Compile Update="Resources.Designer.cs">
19+
<DesignTime>True</DesignTime>
20+
<AutoGen>True</AutoGen>
21+
<DependentUpon>Resources.resx</DependentUpon>
22+
</Compile>
23+
</ItemGroup>
2024

21-
<ItemGroup>
22-
<EmbeddedResource Update="Resources.resx">
23-
<Generator>ResXFileCodeGenerator</Generator>
24-
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
25-
</EmbeddedResource>
26-
</ItemGroup>
25+
<ItemGroup>
26+
<EmbeddedResource Update="Resources.resx">
27+
<Generator>ResXFileCodeGenerator</Generator>
28+
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
29+
</EmbeddedResource>
30+
</ItemGroup>
2731

28-
<ItemGroup>
29-
<None Update="flowsynx.png">
30-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
31-
</None>
32-
<None Update="README.md">
33-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
34-
</None>
35-
</ItemGroup>
32+
<ItemGroup>
33+
<None Update="flowsynx.png">
34+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
35+
</None>
36+
<None Update="README.md">
37+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
38+
</None>
39+
</ItemGroup>
3640

3741
</Project>

src/JsonPlugin.cs

Lines changed: 50 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,59 @@
11
using FlowSynx.PluginCore;
22
using FlowSynx.PluginCore.Extensions;
3-
using FlowSynx.PluginCore.Helpers;
43
using FlowSynx.Plugins.Json.Models;
4+
using FlowSynx.Plugins.Json.Services;
55
using Newtonsoft.Json.Linq;
66

77
namespace FlowSynx.Plugins.Json;
88

99
public class JsonPlugin : IPlugin
1010
{
11+
private readonly IGuidProvider _guidProvider;
12+
private readonly IReflectionGuard _reflectionGuard;
1113
private IPluginLogger? _logger;
1214
private bool _isInitialized;
1315

14-
public PluginMetadata Metadata
16+
public JsonPlugin() : this(new GuidProvider(), new DefaultReflectionGuard()) { }
17+
18+
internal JsonPlugin(IGuidProvider guidProvider, IReflectionGuard reflectionGuard)
1519
{
16-
get
17-
{
18-
return new PluginMetadata
19-
{
20-
Id = Guid.Parse("61519421-6eb9-466b-aaed-366098da1922"),
21-
Name = "Json",
22-
CompanyName = "FlowSynx",
23-
Description = Resources.PluginDescription,
24-
Version = new PluginVersion(1, 0, 0),
25-
Category = PluginCategory.Data,
26-
Authors = new List<string> { "FlowSynx" },
27-
Copyright = "© FlowSynx. All rights reserved.",
28-
Icon = "flowsynx.png",
29-
ReadMe = "README.md",
30-
RepositoryUrl = "https://github.com/flowsynx/plugin-json",
31-
ProjectUrl = "https://flowsynx.io",
32-
Tags = new List<string>() { "flowSynx", "json", "data", "data-platform" }
33-
};
34-
}
20+
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
21+
_reflectionGuard = reflectionGuard ?? throw new ArgumentNullException(nameof(reflectionGuard));
3522
}
3623

24+
public PluginMetadata Metadata => new()
25+
{
26+
Id = Guid.Parse("61519421-6eb9-466b-aaed-366098da1922"),
27+
Name = "Json",
28+
CompanyName = "FlowSynx",
29+
Description = Resources.PluginDescription,
30+
Version = new PluginVersion(1, 0, 0),
31+
Category = PluginCategory.Data,
32+
Authors = new List<string> { "FlowSynx" },
33+
Copyright = "© FlowSynx. All rights reserved.",
34+
Icon = "flowsynx.png",
35+
ReadMe = "README.md",
36+
RepositoryUrl = "https://github.com/flowsynx/plugin-json",
37+
ProjectUrl = "https://flowsynx.io",
38+
Tags = new List<string>() { "flowSynx", "json", "data", "data-platform" }
39+
};
40+
3741
public PluginSpecifications? Specifications { get; set; }
3842

3943
public Type SpecificationsType => typeof(JsonPluginSpecifications);
4044

41-
private Dictionary<string, Func<JObject, InputParameter, object>> OperationMap => new(StringComparer.OrdinalIgnoreCase)
45+
private Dictionary<string, IJsonOperationHandler> OperationMap => new(StringComparer.OrdinalIgnoreCase)
4246
{
43-
["extract"] = HandleExtract,
44-
["map"] = HandleMap,
45-
["transform"] = HandleTransform
47+
["extract"] = new ExtractOperationHandler(_guidProvider),
48+
["map"] = new MapOperationHandler(_guidProvider),
49+
["transform"] = new TransformOperationHandler(_guidProvider)
4650
};
4751

48-
public IReadOnlyCollection<string> SupportedOperations => new[] { "extract", "map", "transform" };
52+
public IReadOnlyCollection<string> SupportedOperations => OperationMap.Keys;
4953

5054
public Task Initialize(IPluginLogger logger)
5155
{
52-
if (ReflectionHelper.IsCalledViaReflection())
56+
if (_reflectionGuard.IsCalledViaReflection())
5357
throw new InvalidOperationException(Resources.ReflectionBasedAccessIsNotAllowed);
5458

5559
ArgumentNullException.ThrowIfNull(logger);
@@ -58,79 +62,40 @@ public Task Initialize(IPluginLogger logger)
5862
return Task.CompletedTask;
5963
}
6064

61-
public async Task<object?> ExecuteAsync(PluginParameters parameters, CancellationToken cancellationToken)
65+
public Task<object?> ExecuteAsync(PluginParameters parameters, CancellationToken cancellationToken)
6266
{
63-
if (ReflectionHelper.IsCalledViaReflection())
67+
cancellationToken.ThrowIfCancellationRequested();
68+
69+
if (_reflectionGuard.IsCalledViaReflection())
6470
throw new InvalidOperationException(Resources.ReflectionBasedAccessIsNotAllowed);
6571

6672
if (!_isInitialized)
6773
throw new InvalidOperationException($"Plugin '{Metadata.Name}' v{Metadata.Version} is not initialized.");
6874

6975
var inputParameter = parameters.ToObject<InputParameter>();
70-
var operation = inputParameter.Operation;
71-
72-
if (OperationMap.TryGetValue(operation, out var handler))
73-
{
74-
var json = inputParameter.Json ?? throw new ArgumentException("Input JSON is required.");
75-
var jsonObj = JObject.Parse(json);
76-
77-
return handler(jsonObj, inputParameter);
78-
}
79-
80-
throw new NotSupportedException($"Json plugin: Operation '{operation}' is not supported.");
81-
}
82-
83-
private object HandleExtract(JObject json, InputParameter inputParameter)
84-
{
85-
string? path = inputParameter.jsonPath;
86-
if (string.IsNullOrWhiteSpace(path))
87-
throw new ArgumentException("jsonPath parameter is required for extract.");
88-
89-
var token = json.SelectToken(path);
90-
return token?.ToString() ?? "null";
91-
}
92-
93-
private object HandleMap(JObject json, InputParameter inputParameter)
94-
{
95-
if (inputParameter.Mappings == null)
96-
throw new InvalidOperationException("Mappings not defined in specifications.");
97-
98-
var result = new Dictionary<string, object?>();
99-
foreach (var kvp in inputParameter.Mappings)
76+
if (!OperationMap.TryGetValue(inputParameter.Operation, out var handler))
10077
{
101-
result[kvp.Key] = json.SelectToken(kvp.Value)?.ToString();
78+
throw new NotSupportedException($"Operation '{inputParameter.Operation}' is not supported.");
10279
}
10380

104-
return result;
105-
}
106-
107-
private object HandleTransform(JObject json, InputParameter inputParameter)
108-
{
109-
var result = json;
81+
var context = ParseDataToContext(inputParameter.Data);
82+
var json = context.Content ?? throw new ArgumentException("Input JSON is required.");
11083

111-
if (inputParameter.Flatten)
112-
result = FlattenJson(json);
113-
114-
return result;
84+
var jsonToken = JToken.Parse(json);
85+
return Task.FromResult(handler.Handle(jsonToken, inputParameter));
11586
}
11687

117-
private JObject FlattenJson(JObject input)
88+
private PluginContext ParseDataToContext(object? data)
11889
{
119-
var result = new JObject();
90+
if (data is null)
91+
throw new ArgumentNullException(nameof(data), "Input data cannot be null.");
12092

121-
void Flatten(JObject obj, string prefix)
93+
return data switch
12294
{
123-
foreach (var prop in obj.Properties())
124-
{
125-
var path = string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}.{prop.Name}";
126-
if (prop.Value is JObject nested)
127-
Flatten(nested, path);
128-
else
129-
result[path] = prop.Value;
130-
}
131-
}
132-
133-
Flatten(input, "");
134-
return result;
95+
PluginContext singleContext => singleContext,
96+
IEnumerable<PluginContext> => throw new NotSupportedException("List of PluginContext is not supported."),
97+
string strData => new PluginContext(_guidProvider.NewGuid().ToString(), "Data") { Content = strData },
98+
_ => throw new NotSupportedException("Unsupported input data format.")
99+
};
135100
}
136101
}

src/Models/InputParameter.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
internal class InputParameter
44
{
55
public string Operation { get; set; } = "extract";
6-
public string? Json { get; set; }
7-
public string? jsonPath { get; set; }
6+
public object? Data { get; set; }
7+
public string? Path { get; set; }
88
public Dictionary<string, string>? Mappings { get; set; }
99
public bool Flatten { get; set; } = false;
10+
public bool Indented { get; set; } = false;
1011
}

src/Resources.Designer.cs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Resources.resx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,6 @@
121121
<value>Loads and parses local JSON files. Supports transformation, extraction, and mapping of hierarchical data structures in workflows.</value>
122122
</data>
123123
<data name="ReflectionBasedAccessIsNotAllowed" xml:space="preserve">
124-
<value>The specified path must be not empty!</value>
124+
<value>Reflection-based access is not allowed.</value>
125125
</data>
126126
</root>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using FlowSynx.PluginCore.Helpers;
2+
3+
namespace FlowSynx.Plugins.Json.Services;
4+
5+
internal class DefaultReflectionGuard : IReflectionGuard
6+
{
7+
public bool IsCalledViaReflection() => ReflectionHelper.IsCalledViaReflection();
8+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using FlowSynx.PluginCore;
2+
using FlowSynx.Plugins.Json.Models;
3+
using Newtonsoft.Json;
4+
using Newtonsoft.Json.Linq;
5+
6+
namespace FlowSynx.Plugins.Json.Services;
7+
8+
internal class ExtractOperationHandler : IJsonOperationHandler
9+
{
10+
private readonly IGuidProvider _guidProvider;
11+
12+
public ExtractOperationHandler(IGuidProvider guidProvider)
13+
{
14+
_guidProvider = guidProvider;
15+
}
16+
17+
public object Handle(JToken json, InputParameter inputParameter)
18+
{
19+
string? path = inputParameter.Path;
20+
if (string.IsNullOrWhiteSpace(path))
21+
throw new ArgumentException("Path parameter is required for extract.");
22+
23+
var tokens = json.SelectTokens(path).ToList();
24+
string filename = $"{_guidProvider.NewGuid()}.json";
25+
26+
if (tokens.Count == 0)
27+
{
28+
return new PluginContext(filename, "Data")
29+
{
30+
Format = "Json",
31+
Content = "null"
32+
};
33+
}
34+
35+
JToken result = tokens.Count == 1
36+
? tokens[0]
37+
: new JArray(tokens);
38+
39+
return new PluginContext(filename, "Data")
40+
{
41+
Format = "Json",
42+
Content = result.ToString(inputParameter.Indented ? Formatting.Indented : Formatting.None)
43+
};
44+
}
45+
}

src/Services/GuidProvider.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace FlowSynx.Plugins.Json.Services;
2+
3+
internal class GuidProvider : IGuidProvider
4+
{
5+
public Guid NewGuid() => Guid.NewGuid();
6+
}

src/Services/IGuidProvider.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace FlowSynx.Plugins.Json.Services;
2+
3+
public interface IGuidProvider
4+
{
5+
Guid NewGuid();
6+
}

0 commit comments

Comments
 (0)