Skip to content

Commit 05d6a87

Browse files
committed
Refactor core logic and add unit tests
- Updated `FlowPack.sln` to target Visual Studio 18, added `FlowPack.UnitTests` project, and reorganized solution structure. - Enhanced `PackOptions` argument parsing for `--output` flag. - Refactored `PluginReflector` to use `CreateInstanceBestEffort` for improved instance creation logic. - Simplified `TypeExtensions.GetCanonicalAiTypeName` and improved handling of complex types. - Added `FlowPack.UnitTests` project with xUnit and test dependencies. - Implemented unit tests for `PackOptions`, `PluginMetadata`, and `TypeExtensions` to ensure robust functionality. #19
1 parent 1af08a7 commit 05d6a87

9 files changed

Lines changed: 570 additions & 86 deletions

FlowPack.sln

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 17
4-
VisualStudioVersion = 17.12.35728.132
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.0.11222.15 d18.0
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlowPack", "src\FlowPack.csproj", "{13B38F4C-6E7B-4853-A3E6-FBF4CEF8216A}"
77
EndProject
8+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6240DC87-BF93-4271-AF55-4EF8E9640912}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{5F57EDB0-627C-432E-A5A4-86D51F39EE40}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlowPack.UnitTests", "tests\FlowPack.UnitTests.csproj", "{530CB1E2-282C-D124-5B19-4660B404BCB2}"
13+
EndProject
814
Global
915
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1016
Debug|Any CPU = Debug|Any CPU
@@ -15,8 +21,16 @@ Global
1521
{13B38F4C-6E7B-4853-A3E6-FBF4CEF8216A}.Debug|Any CPU.Build.0 = Debug|Any CPU
1622
{13B38F4C-6E7B-4853-A3E6-FBF4CEF8216A}.Release|Any CPU.ActiveCfg = Release|Any CPU
1723
{13B38F4C-6E7B-4853-A3E6-FBF4CEF8216A}.Release|Any CPU.Build.0 = Release|Any CPU
24+
{530CB1E2-282C-D124-5B19-4660B404BCB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25+
{530CB1E2-282C-D124-5B19-4660B404BCB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
26+
{530CB1E2-282C-D124-5B19-4660B404BCB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{530CB1E2-282C-D124-5B19-4660B404BCB2}.Release|Any CPU.Build.0 = Release|Any CPU
1828
EndGlobalSection
1929
GlobalSection(SolutionProperties) = preSolution
2030
HideSolutionNode = FALSE
2131
EndGlobalSection
32+
GlobalSection(NestedProjects) = preSolution
33+
{13B38F4C-6E7B-4853-A3E6-FBF4CEF8216A} = {6240DC87-BF93-4271-AF55-4EF8E9640912}
34+
{530CB1E2-282C-D124-5B19-4660B404BCB2} = {5F57EDB0-627C-432E-A5A4-86D51F39EE40}
35+
EndGlobalSection
2236
EndGlobal

src/FlowPack.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
<DebugType>None</DebugType>
1414
</PropertyGroup>
1515

16+
<ItemGroup>
17+
<InternalsVisibleTo Include="FlowPack.UnitTests" />
18+
</ItemGroup>
19+
1620
<ItemGroup>
1721
<PackageReference Include="FlowSynx.PluginCore" Version="1.4.0" />
1822
</ItemGroup>

src/PackOptions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@ public static PackOptions Parse(string[] args)
1616
switch (args[i])
1717
{
1818
case "--output":
19-
options.OutputPath = args.Length > i + 1 ? args[++i] : null;
19+
// Only consume the next arg as output if it exists and is not another flag
20+
if (args.Length > i + 1 && !(args[i + 1].StartsWith("--", StringComparison.Ordinal)))
21+
{
22+
options.OutputPath = args[++i];
23+
}
24+
else
25+
{
26+
options.OutputPath = null;
27+
}
2028
break;
2129
case "--clean":
2230
options.Clean = true;

src/PluginReflector.cs

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,7 @@ private static List<SpecificationMetadata> ExtractSpecificationMetadata(IPlugin
8989
t.IsClass
9090
) ?? throw new InvalidOperationException("No concrete Specifications class found.");
9191

92-
// Create an instance to get default values
93-
specInstance = Activator.CreateInstance(specType);
92+
specInstance = CreateInstanceBestEffort(specType, plugin);
9493
}
9594

9695
// Iterate over properties of the concrete type
@@ -126,13 +125,15 @@ private static List<PluginOperationMetadata> ExtractOperations(IPlugin plugin)
126125
continue;
127126

128127
var opInterface = type.GetInterfaces()
129-
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition().Name == "IPluginOperation`2");
128+
.FirstOrDefault(i => i.IsGenericType &&
129+
i.GetGenericTypeDefinition().Name == "IPluginOperation`2");
130130

131131
if (opInterface == null)
132132
continue;
133133

134-
object? opInstance = null;
135-
try { opInstance = Activator.CreateInstance(type); } catch { continue; }
134+
var opInstance = CreateInstanceBestEffort(type, plugin);
135+
if (opInstance == null)
136+
continue;
136137

137138
var nameProp = type.GetProperty("Name", BindingFlags.Instance | BindingFlags.Public);
138139
var descProp = type.GetProperty("Description", BindingFlags.Instance | BindingFlags.Public);
@@ -146,20 +147,23 @@ private static List<PluginOperationMetadata> ExtractOperations(IPlugin plugin)
146147
var paramType = opInterface.GetGenericArguments()[0];
147148
List<PluginOperationParameterMetadata> paramMetas = new();
148149

150+
// Try creating parameter objects too
151+
var paramTypeInstance = CreateInstanceBestEffort(paramType);
152+
149153
foreach (var prop in paramType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
150154
{
151155
var attr = prop.GetCustomAttribute<OperationParameterMetadataAttribute>();
152-
if (attr == null) continue;
153-
154-
object? paramInstance = null;
155-
try { paramInstance = Activator.CreateInstance(paramType); } catch { }
156+
if (attr == null)
157+
continue;
156158

157159
paramMetas.Add(new PluginOperationParameterMetadata
158160
{
159161
Name = prop.Name,
160162
Description = attr.Description,
161163
Type = TypeExtensions.GetCanonicalAiTypeName(prop.PropertyType),
162-
DefaultValue = paramInstance != null ? prop.GetValue(paramInstance)?.ToString() : null,
164+
DefaultValue = paramTypeInstance != null
165+
? prop.GetValue(paramTypeInstance)?.ToString()
166+
: null,
163167
IsRequired = attr.IsRequired
164168
});
165169
}
@@ -171,6 +175,83 @@ private static List<PluginOperationMetadata> ExtractOperations(IPlugin plugin)
171175
return operations;
172176
}
173177

178+
private static object? CreateInstanceBestEffort(Type type, object? plugin = null)
179+
{
180+
// Try simplest path first
181+
try
182+
{
183+
return Activator.CreateInstance(type);
184+
}
185+
catch
186+
{
187+
// ignore and try advanced logic
188+
}
189+
190+
// Choose the "best" constructor: most parameters, public only
191+
var ctors = type.GetConstructors()
192+
.OrderByDescending(c => c.GetParameters().Length)
193+
.ToList();
194+
195+
foreach (var ctor in ctors)
196+
{
197+
var parameters = ctor.GetParameters();
198+
var args = new object?[parameters.Length];
199+
bool failed = false;
200+
201+
for (int i = 0; i < parameters.Length; i++)
202+
{
203+
var p = parameters[i];
204+
205+
// 1. If plugin instance is assignable to parameter → use it
206+
if (plugin != null && p.ParameterType.IsAssignableFrom(plugin.GetType()))
207+
{
208+
args[i] = plugin;
209+
continue;
210+
}
211+
212+
// 2. If parameter has default value → use it
213+
if (p.HasDefaultValue)
214+
{
215+
args[i] = p.DefaultValue;
216+
continue;
217+
}
218+
219+
// 3. If nullable reference/value → pass null
220+
if (!p.ParameterType.IsValueType || Nullable.GetUnderlyingType(p.ParameterType) != null)
221+
{
222+
args[i] = null;
223+
continue;
224+
}
225+
226+
// 4. If parameter type has parameterless constructor → build it
227+
try
228+
{
229+
args[i] = Activator.CreateInstance(p.ParameterType);
230+
continue;
231+
}
232+
catch
233+
{
234+
failed = true;
235+
break;
236+
}
237+
}
238+
239+
if (failed)
240+
continue;
241+
242+
try
243+
{
244+
return ctor.Invoke(args);
245+
}
246+
catch
247+
{
248+
// try next constructor
249+
}
250+
}
251+
252+
return null; // No usable constructor
253+
}
254+
174255
public static string SaveMetadataToFile(PluginMetadata metadata, string outputDirectory)
175256
{
176257
var json = JsonSerializer.Serialize(metadata, new JsonSerializerOptions { WriteIndented = true });

src/TypeExtensions.cs

Lines changed: 24 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,97 +2,48 @@
22

33
public static class TypeExtensions
44
{
5-
private static readonly Dictionary<Type, string> _csharpAliases = new()
6-
{
7-
{ typeof(void), "void" },
8-
{ typeof(bool), "bool" },
9-
{ typeof(byte), "byte" },
10-
{ typeof(sbyte), "sbyte" },
11-
{ typeof(char), "char" },
12-
{ typeof(decimal), "decimal" },
13-
{ typeof(double), "double" },
14-
{ typeof(float), "float" },
15-
{ typeof(int), "int" },
16-
{ typeof(uint), "uint" },
17-
{ typeof(long), "long" },
18-
{ typeof(ulong), "ulong" },
19-
{ typeof(object), "object" },
20-
{ typeof(short), "short" },
21-
{ typeof(ushort), "ushort" },
22-
{ typeof(string), "string" }
23-
};
5+
private const string Prefix = "CSharp:";
246

7+
/// <summary>
8+
/// Returns the canonical AI type name with prefix "CSharp:", avoiding nested prefixes.
9+
/// </summary>
2510
public static string GetCanonicalAiTypeName(Type type)
2611
{
27-
if (type == null)
28-
throw new ArgumentNullException(nameof(type));
12+
if (type == null) throw new ArgumentNullException(nameof(type));
13+
return Prefix + GetCanonicalInternal(type);
14+
}
2915

16+
private static string GetCanonicalInternal(Type type)
17+
{
3018
// Nullable<T> → T?
31-
var underlyingNullable = Nullable.GetUnderlyingType(type);
32-
if (underlyingNullable != null)
33-
return $"CSharp:{GetCanonicalAiTypeName(underlyingNullable)}?";
19+
if (Nullable.GetUnderlyingType(type) is Type underlyingNullable)
20+
return $"{GetCanonicalInternal(underlyingNullable)}?";
3421

3522
// Arrays
3623
if (type.IsArray)
3724
{
38-
var elem = GetCanonicalAiTypeName(type.GetElementType()!);
39-
return $"CSharp:{elem}{new string('[', type.GetArrayRank())}{new string(']', type.GetArrayRank())}";
25+
string elem = GetCanonicalInternal(type.GetElementType()!);
26+
// C#-style multi-dim array: [,,]
27+
string commas = new string(',', type.GetArrayRank() - 1);
28+
//string brackets = new string('[', type.GetArrayRank()) + new string(']', type.GetArrayRank());
29+
return $"{elem}[{commas}]";
4030
}
4131

4232
// Generic types
4333
if (type.IsGenericType)
4434
{
45-
string typeName = type.GetGenericTypeDefinition().FullName!;
46-
int backtick = typeName.IndexOf('`');
47-
if (backtick > 0)
48-
typeName = typeName[..backtick];
35+
string fullName = type.GetGenericTypeDefinition().FullName!;
36+
int idx = fullName.IndexOf('`');
37+
if (idx > 0)
38+
fullName = fullName[..idx];
4939

5040
var args = type.GetGenericArguments()
51-
.Select(GetCanonicalAiTypeName);
41+
.Select(GetCanonicalInternal);
5242

53-
return $"CSharp:{typeName}[{string.Join(", ", args)}]";
54-
}
55-
56-
// Non-generic, non-nullable, non-array
57-
return $"CSharp:{type.FullName}";
58-
}
59-
60-
public static string GetFriendlyTypeName(Type type)
61-
{
62-
if (type == null)
63-
throw new ArgumentNullException(nameof(type));
64-
65-
// Handle arrays
66-
if (type.IsArray)
67-
{
68-
return $"{GetFriendlyTypeName(type.GetElementType()!)}[{new string(',', type.GetArrayRank() - 1)}]";
43+
return $"{fullName}[{string.Join(", ", args)}]";
6944
}
7045

71-
// Handle nullable types
72-
var underlyingNullable = Nullable.GetUnderlyingType(type);
73-
if (underlyingNullable != null)
74-
{
75-
return $"{GetFriendlyTypeName(underlyingNullable)}?";
76-
}
77-
78-
// Handle generic types
79-
if (type.IsGenericType)
80-
{
81-
var typeDef = type.GetGenericTypeDefinition();
82-
var genericArgs = type.GetGenericArguments().Select(GetFriendlyTypeName).ToArray();
83-
84-
var typeName = type.Name;
85-
var backtickIndex = typeName.IndexOf('`');
86-
if (backtickIndex > 0)
87-
typeName = typeName.Substring(0, backtickIndex);
88-
89-
return $"{typeName}<{string.Join(", ", genericArgs)}>";
90-
}
91-
92-
// Map to C# alias if possible
93-
if (_csharpAliases.TryGetValue(type, out var alias))
94-
return alias;
95-
96-
return type.Name; // fallback to CLR type name
46+
// Simple type
47+
return type.FullName!;
9748
}
9849
}

tests/FlowPack.UnitTests.csproj

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<IsPackable>false</IsPackable>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="xunit" Version="2.6.2" />
12+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
13+
<PrivateAssets>all</PrivateAssets>
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
</PackageReference>
16+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
17+
<PackageReference Include="coverlet.collector" Version="6.0.2">
18+
<PrivateAssets>all</PrivateAssets>
19+
</PackageReference>
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\src\FlowPack.csproj" />
24+
</ItemGroup>
25+
26+
</Project>

0 commit comments

Comments
 (0)