Skip to content

Commit a00ba14

Browse files
authored
refactor: complete csharp extraction pipeline (#187)
1 parent 836438a commit a00ba14

104 files changed

Lines changed: 5867 additions & 2905 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/libs/AutoSDK.CLI/Commands/CliCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.CommandLine;
22
using AutoSDK.Extensions;
3+
using AutoSDK.Generation;
34
using AutoSDK.Helpers;
45
using AutoSDK.Models;
56

@@ -135,4 +136,4 @@ private async Task HandleAsync(ParseResult parseResult)
135136

136137
Console.WriteLine("Done.");
137138
}
138-
}
139+
}

src/libs/AutoSDK.CLI/Commands/GenerateCommand.cs

Lines changed: 27 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.CommandLine;
33
using AutoSDK.Extensions;
44
using AutoSDK.Generation;
5+
using AutoSDK.Helpers;
56
using AutoSDK.Models;
67
using AutoSDK.Naming.Methods;
78

@@ -168,6 +169,13 @@ internal sealed class GenerateCommand : Command
168169
Description = "Generate model classes, enums, and JSON converters. Set to false when referencing types from an existing namespace via --types-namespace.",
169170
};
170171

172+
private Option<string> Language { get; } = new(
173+
name: "--language")
174+
{
175+
DefaultValueFactory = _ => "csharp",
176+
Description = "Generation backend. Currently supported: csharp.",
177+
};
178+
171179
public GenerateCommand() : base(name: "generate", description: "Generates client sdk using an OpenAPI or AsyncAPI spec.")
172180
{
173181
Arguments.Add(Input);
@@ -191,6 +199,7 @@ internal sealed class GenerateCommand : Command
191199
Options.Add(JsonSerializerContextName);
192200
Options.Add(TypesNamespace);
193201
Options.Add(GenerateModels);
202+
Options.Add(Language);
194203

195204
SetAction(HandleAsync);
196205
}
@@ -200,6 +209,7 @@ private async Task HandleAsync(ParseResult parseResult)
200209
string input = parseResult.GetRequiredValue(Input);
201210
string output = parseResult.GetRequiredValue(Output);
202211
bool singleFile = parseResult.GetRequiredValue(SingleFile);
212+
string language = parseResult.GetRequiredValue(Language);
203213

204214
var namespaceValue = parseResult.GetRequiredValue(Namespace);
205215
var contextName = parseResult.GetRequiredValue(JsonSerializerContextName);
@@ -268,104 +278,23 @@ private async Task HandleAsync(ParseResult parseResult)
268278
};
269279
}
270280

271-
var data = Generation.Data.Prepare(((yaml, settings), GlobalSettings: settings));
272-
var files = settings.GenerateCli
273-
? data.Methods
274-
.SelectMany(x => new []
275-
{
276-
Sources.Command(x),
277-
}).Concat(data.Methods.GroupBy(x => x.Tag)
278-
.SelectMany(x => new []
279-
{
280-
Sources.GroupCommand(x.Key, x.ToImmutableArray()),
281-
}))
282-
.Concat([Sources.MainCommand(data.Tags)])
283-
.Concat([Sources.AddCommands(data.Methods, data.Tags)])
284-
.Where(x => !x.IsEmpty)
285-
.ToArray()
286-
: generateModels
287-
? data.Enums
288-
.SelectMany(x => new []
289-
{
290-
Sources.Enum(x),
291-
Sources.EnumJsonConverter(x),
292-
Sources.EnumNullableJsonConverter(x),
293-
})
294-
.Concat(data.Classes
295-
.SelectMany(x => new []
296-
{
297-
Sources.Class(x),
298-
Sources.ClassJsonExtensions(x),
299-
Sources.ClassValidation(x),
300-
}))
301-
.Concat(data.Methods
302-
.SelectMany(x => new []
303-
{
304-
Sources.Method(x),
305-
Sources.MethodInterface(x),
306-
}))
307-
.Concat(data.Clients
308-
.SelectMany(x => new []
309-
{
310-
Sources.Client(x),
311-
Sources.ClientInterface(x),
312-
}))
313-
.Concat(data.Authorizations
314-
.SelectMany(x => new []
315-
{
316-
Sources.Authorization(x),
317-
Sources.AuthorizationInterface(x),
318-
}))
319-
.Concat([Sources.MainAuthorizationConstructor(data.Authorizations)])
320-
.Concat(data.AnyOfs
321-
.SelectMany(x => new []
322-
{
323-
Sources.AnyOf(x),
324-
Sources.AnyOfJsonExtensions(x),
325-
Sources.AnyOfJsonConverter(x),
326-
//Sources.AnyOfJsonConverterFactory(x),
327-
Sources.AnyOfValidation(x),
328-
}))
329-
.Concat([Sources.JsonSerializerContext(data.Converters, data.Types)])
330-
.Concat([Sources.JsonSerializerContextTypes(data.Types)])
331-
.Concat([Sources.Polyfills(settings)])
332-
.Concat([Sources.Exceptions(settings)])
333-
.Concat([Sources.PathBuilder(settings)])
334-
.Concat(data.Methods.Any(static x => x.RawStream)
335-
? [Sources.ResponseStream(data.Converters.Settings)]
336-
: [])
337-
.Concat([Sources.UnixTimestampJsonConverter(settings)])
338-
// WebSocket client generation (from AsyncAPI specs)
339-
.Concat(data.WebSocketClients
340-
.SelectMany(x => new []
341-
{
342-
Sources.WebSocketClient(x),
343-
Sources.WebSocketReceiveMethod(x),
344-
}))
345-
// PathBuilder for WebSocket namespaces with query parameters
346-
.Concat(data.WebSocketClients
347-
.Where(x => x.QueryParameters.Length > 0 &&
348-
x.Settings.Namespace != settings.Namespace)
349-
.Select(x => x.Settings)
350-
.Distinct()
351-
.Select(s => Sources.PathBuilder(s)))
352-
.Concat(data.WebSocketOperations
353-
.Where(x => x.Direction == AutoSDK.Models.WebSocketDirection.Send)
354-
.Select(x => Sources.WebSocketSendMethod(x)))
355-
.Where(x => !x.IsEmpty)
356-
.ToArray()
357-
// When generate-models is false, only output WebSocket client files
358-
: data.WebSocketClients
359-
.SelectMany(x => new []
360-
{
361-
Sources.WebSocketClient(x),
362-
Sources.WebSocketReceiveMethod(x),
363-
})
364-
.Concat(data.WebSocketOperations
365-
.Where(x => x.Direction == AutoSDK.Models.WebSocketDirection.Send)
366-
.Select(x => Sources.WebSocketSendMethod(x)))
367-
.Where(x => !x.IsEmpty)
368-
.ToArray();
281+
if (!string.Equals(language, "csharp", StringComparison.OrdinalIgnoreCase))
282+
{
283+
throw new NotSupportedException($"Unsupported language '{language}'. Currently only 'csharp' is supported.");
284+
}
285+
286+
var coreResult = CorePipeline.Prepare(
287+
((yaml, settings), GlobalSettings: settings),
288+
static (document, currentSettings) => document.GetSchemas((CSharpSettings)currentSettings),
289+
CSharpPipeline.ApplyModelNaming,
290+
static text => text.ToClassName(),
291+
static text => text.ToPropertyName());
292+
var plugin = CSharpLanguagePlugin.Instance;
293+
var data = plugin.Enrich(coreResult);
294+
var files = plugin
295+
.GenerateFiles(data)
296+
.Where(x => !x.IsEmpty)
297+
.ToArray();
369298

370299
Directory.CreateDirectory(output);
371300

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
using System.Collections.Immutable;
2+
using AutoSDK.Extensions;
3+
using AutoSDK.Helpers;
4+
using AutoSDK.Models;
5+
using AutoSDK.Naming.Parameters;
6+
7+
namespace AutoSDK.Enrichment;
8+
9+
public static class CSharpModelDataFactory
10+
{
11+
public static ModelData CreateModelData(SchemaContext context)
12+
{
13+
context = context ?? throw new ArgumentNullException(nameof(context));
14+
15+
var parentCount = 0;
16+
var parent = context.Parent;
17+
while (parent != null)
18+
{
19+
if (parent.ClassData.HasValue)
20+
{
21+
parentCount++;
22+
}
23+
24+
parent = parent.Parent;
25+
}
26+
27+
var boxedParents = ImmutableArray<Box>.Empty;
28+
if (parentCount > 0)
29+
{
30+
var builder = ImmutableArray.CreateBuilder<Box>(parentCount);
31+
var parentArray = new ModelData[parentCount];
32+
var index = parentCount - 1;
33+
parent = context.Parent;
34+
while (parent != null)
35+
{
36+
if (parent.ClassData.HasValue)
37+
{
38+
parentArray[index--] = parent.ClassData!.Value;
39+
}
40+
41+
parent = parent.Parent;
42+
}
43+
44+
foreach (var item in parentArray)
45+
{
46+
builder.Add(item.Box());
47+
}
48+
49+
boxedParents = builder.MoveToImmutable();
50+
}
51+
52+
var mapping = context.Schema.Discriminator?.Mapping;
53+
EquatableArray<(string ClassName, string Discriminator)> derivedTypes = default;
54+
if (mapping is { Count: > 0 })
55+
{
56+
var builder = ImmutableArray.CreateBuilder<(string ClassName, string Discriminator)>(mapping.Count);
57+
foreach (var kvp in mapping.OrderBy(x => x.Key, StringComparer.Ordinal))
58+
{
59+
builder.Add((ClassName: kvp.Value.Reference?.Id ?? string.Empty, Discriminator: kvp.Key));
60+
}
61+
62+
derivedTypes = builder.MoveToImmutable().AsEquatableArray();
63+
}
64+
65+
var className = context.Id;
66+
var externalClassName = context.Settings.NamingConvention switch
67+
{
68+
NamingConvention.ConcatNames => className,
69+
NamingConvention.InnerClasses => string.Join(
70+
".",
71+
boxedParents.Select(x => x.Unbox<ModelData>().ClassName).Concat([className])),
72+
_ => string.Empty,
73+
};
74+
75+
return new ModelData(
76+
SchemaContext: context,
77+
Id: context.Id,
78+
Parents: boxedParents,
79+
Namespace: context.Settings.Namespace,
80+
Settings: context.Settings.ToEmitterSettings(),
81+
Style: context.Schema.IsEnum() ? ModelStyle.Enumeration : context.Settings.ModelStyle,
82+
Properties: GetVisibleProperties(context),
83+
EnumValues: context.Schema.IsEnum()
84+
? ToImmutablePropertyDataArray(context.ComputeEnum())
85+
: [],
86+
Summary: context.Schema.GetSummary(),
87+
Description: context.Schema.Description ?? string.Empty,
88+
IsDeprecated: context.Schema.IsDeprecated(),
89+
DeprecationMessage: context.Schema.GetDeprecationMessage(),
90+
BaseClass: context.IsDerivedClass
91+
? context.BaseClassContext.Id
92+
: string.Empty,
93+
IsBaseClass: context.IsBaseClass,
94+
IsDerivedClass: context.IsDerivedClass,
95+
DiscriminatorPropertyName: context.Schema.Discriminator?.PropertyName ?? string.Empty,
96+
DerivedTypes: derivedTypes,
97+
ClassName: className,
98+
GlobalClassName: $"global::{context.Settings.Namespace}.{className}",
99+
ExternalClassName: externalClassName,
100+
FileNameWithoutExtension: $"{context.Settings.Namespace}.Models.{externalClassName}");
101+
}
102+
103+
private static ImmutableArray<PropertyData> GetVisibleProperties(SchemaContext context)
104+
{
105+
var source = context.IsDerivedClass
106+
? context.DerivedClassContext.Children
107+
: !context.Schema.IsEnum()
108+
? context.Children
109+
: null;
110+
111+
if (source == null || source.Count == 0)
112+
{
113+
return [];
114+
}
115+
116+
var discriminatorPropertyName = context.IsBaseClass
117+
? context.Schema.Discriminator?.PropertyName
118+
: null;
119+
var hasDiscriminator = !string.IsNullOrWhiteSpace(discriminatorPropertyName);
120+
121+
var builder = ImmutableArray.CreateBuilder<PropertyData>(source.Count);
122+
for (var i = 0; i < source.Count; i++)
123+
{
124+
var child = source[i];
125+
if (child is not { IsProperty: true, PropertyData: not null })
126+
{
127+
continue;
128+
}
129+
130+
foreach (var prop in child.ComputedProperties)
131+
{
132+
if (!hasDiscriminator || prop.Id != discriminatorPropertyName)
133+
{
134+
builder.Add(prop);
135+
}
136+
}
137+
}
138+
139+
return DeduplicatePropertyNamesCaseInsensitive(builder.ToImmutable());
140+
}
141+
142+
private static ImmutableArray<PropertyData> DeduplicatePropertyNamesCaseInsensitive(
143+
ImmutableArray<PropertyData> properties)
144+
{
145+
if (properties.Length <= 1)
146+
{
147+
return properties;
148+
}
149+
150+
var nameCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
151+
for (var i = 0; i < properties.Length; i++)
152+
{
153+
nameCounts.TryGetValue(properties[i].Name, out var count);
154+
nameCounts[properties[i].Name] = count + 1;
155+
}
156+
157+
var hasCaseCollision = false;
158+
foreach (var kvp in nameCounts)
159+
{
160+
if (kvp.Value > 1)
161+
{
162+
hasCaseCollision = true;
163+
break;
164+
}
165+
}
166+
167+
if (!hasCaseCollision)
168+
{
169+
return properties;
170+
}
171+
172+
var suffixCounters = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
173+
var builder = ImmutableArray.CreateBuilder<PropertyData>(properties.Length);
174+
for (var i = 0; i < properties.Length; i++)
175+
{
176+
var prop = properties[i];
177+
if (nameCounts[prop.Name] > 1)
178+
{
179+
suffixCounters.TryGetValue(prop.Name, out var suffix);
180+
suffix++;
181+
suffixCounters[prop.Name] = suffix;
182+
builder.Add(suffix == 1
183+
? prop
184+
: (prop with { Name = $"{prop.Name}{suffix}" }).WithCSharpParameterName());
185+
}
186+
else
187+
{
188+
builder.Add(prop);
189+
}
190+
}
191+
192+
return builder.MoveToImmutable();
193+
}
194+
195+
private static ImmutableArray<PropertyData> ToImmutablePropertyDataArray(Dictionary<string, PropertyData> dict)
196+
{
197+
if (dict.Count == 0)
198+
{
199+
return [];
200+
}
201+
202+
var builder = ImmutableArray.CreateBuilder<PropertyData>(dict.Count);
203+
foreach (var kvp in dict.OrderBy(x => x.Key, StringComparer.Ordinal))
204+
{
205+
builder.Add(kvp.Value);
206+
}
207+
208+
return builder.MoveToImmutable();
209+
}
210+
}

0 commit comments

Comments
 (0)