Skip to content

Commit 14f4517

Browse files
Fixes for source gen
1 parent b17421d commit 14f4517

File tree

9 files changed

+1903
-1773
lines changed

9 files changed

+1903
-1773
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
global using GeneratorError = Outcome.Result<
2+
RestClient.Net.OpenApiGenerator.GeneratorResult,
3+
string
4+
>.Error<RestClient.Net.OpenApiGenerator.GeneratorResult, string>;
5+
global using GeneratorOk = Outcome.Result<
6+
RestClient.Net.OpenApiGenerator.GeneratorResult,
7+
string
8+
>.Ok<RestClient.Net.OpenApiGenerator.GeneratorResult, string>;

RestClient.Net.OpenApiGenerator.Cli/Program.cs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#pragma warning disable CA1502 // Cyclomatic complexity - TODO: Refactor Program.cs
2+
13
using Microsoft.Extensions.DependencyInjection;
24
using RestClient.Net;
35
using RestClient.Net.OpenApiGenerator;
@@ -6,14 +8,6 @@
68
Outcome.HttpError<string>
79
>;
810
using ExceptionErrorString = Outcome.HttpError<string>.ExceptionError;
9-
using GeneratorError = Outcome.Result<
10-
RestClient.Net.OpenApiGenerator.GeneratorResult,
11-
string
12-
>.Error<RestClient.Net.OpenApiGenerator.GeneratorResult, string>;
13-
using GeneratorOk = Outcome.Result<RestClient.Net.OpenApiGenerator.GeneratorResult, string>.Ok<
14-
RestClient.Net.OpenApiGenerator.GeneratorResult,
15-
string
16-
>;
1711
using OkString = Outcome.Result<string, Outcome.HttpError<string>>.Ok<
1812
string,
1913
Outcome.HttpError<string>
@@ -52,6 +46,12 @@ static void PrintUsage()
5246
Console.WriteLine(" -c, --class-name <name> The class name (default: 'ApiExtensions')");
5347
Console.WriteLine(" -b, --base-url <url> Optional base URL override");
5448
Console.WriteLine(" -v, --version <version> OpenAPI version override (e.g., '3.1.0')");
49+
Console.WriteLine(
50+
" --json-naming <policy> JSON naming policy: camelCase, PascalCase, snake_case (default: camelCase)"
51+
);
52+
Console.WriteLine(
53+
" --case-insensitive <bool> Enable case-insensitive JSON deserialization (default: true)"
54+
);
5555
Console.WriteLine(" -h, --help Show this help message");
5656
}
5757

@@ -63,6 +63,8 @@ static void PrintUsage()
6363
var className = "ApiExtensions";
6464
string? baseUrl = null;
6565
string? version = null;
66+
var jsonNamingPolicy = "camelCase";
67+
var caseInsensitive = true;
6668

6769
for (var i = 0; i < args.Length; i++)
6870
{
@@ -92,6 +94,19 @@ static void PrintUsage()
9294
or "--version":
9395
version = GetNextArg(args, i++, "version");
9496
break;
97+
case "--json-naming":
98+
jsonNamingPolicy = GetNextArg(args, i++, "json-naming") ?? jsonNamingPolicy;
99+
break;
100+
case "--case-insensitive":
101+
var caseInsensitiveValue = GetNextArg(args, i++, "case-insensitive");
102+
if (
103+
caseInsensitiveValue != null
104+
&& bool.TryParse(caseInsensitiveValue, out var parsed)
105+
)
106+
{
107+
caseInsensitive = parsed;
108+
}
109+
break;
95110
default:
96111
break;
97112
}
@@ -111,7 +126,16 @@ static void PrintUsage()
111126
return null;
112127
}
113128

114-
return new Config(openApiUrl, outputPath, namespaceName, className, baseUrl, version);
129+
return new Config(
130+
openApiUrl,
131+
outputPath,
132+
namespaceName,
133+
className,
134+
baseUrl,
135+
version,
136+
jsonNamingPolicy,
137+
caseInsensitive
138+
);
115139
}
116140

117141
static string? GetNextArg(string[] args, int currentIndex, string optionName)
@@ -199,7 +223,9 @@ await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false),
199223
@namespace: config.Namespace,
200224
className: config.ClassName,
201225
outputPath: config.OutputPath,
202-
baseUrlOverride: config.BaseUrl
226+
baseUrlOverride: config.BaseUrl,
227+
jsonNamingPolicy: config.JsonNamingPolicy,
228+
caseInsensitive: config.CaseInsensitive
203229
);
204230

205231
#pragma warning disable IDE0010
@@ -236,5 +262,7 @@ internal sealed record Config(
236262
string Namespace,
237263
string ClassName,
238264
string? BaseUrl,
239-
string? Version
265+
string? Version,
266+
string JsonNamingPolicy,
267+
bool CaseInsensitive
240268
);

RestClient.Net.OpenApiGenerator/ExtensionMethodGenerator.cs

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using Microsoft.OpenApi;
2-
31
namespace RestClient.Net.OpenApiGenerator;
42

53
/// <summary>Generates C# extension methods from OpenAPI operations.</summary>
@@ -11,17 +9,21 @@ internal static class ExtensionMethodGenerator
119
/// <param name="className">The class name for extension methods.</param>
1210
/// <param name="baseUrl">The base URL for API requests.</param>
1311
/// <param name="basePath">The base path for API requests.</param>
12+
/// <param name="jsonNamingPolicy">JSON naming policy (camelCase, PascalCase, snake_case).</param>
13+
/// <param name="caseInsensitive">Enable case-insensitive JSON deserialization.</param>
1414
/// <returns>Tuple containing the extension methods code and type aliases code.</returns>
1515
public static (string ExtensionMethods, string TypeAliases) GenerateExtensionMethods(
1616
OpenApiDocument document,
1717
string @namespace,
1818
string className,
1919
string baseUrl,
20-
string basePath
20+
string basePath,
21+
string jsonNamingPolicy = "camelCase",
22+
bool caseInsensitive = true
2123
)
2224
{
23-
var publicMethods = new List<string>();
24-
var privateDelegates = new List<string>();
25+
var groupedMethods =
26+
new Dictionary<string, List<(string PublicMethod, string PrivateDelegate)>>();
2527
var responseTypes = new HashSet<string>();
2628

2729
foreach (var path in document.Paths)
@@ -31,6 +33,8 @@ string basePath
3133
continue;
3234
}
3335

36+
var resourceName = GetResourceNameFromPath(path.Key);
37+
3438
foreach (var operation in path.Value.Operations)
3539
{
3640
var responseType = GetResponseType(operation.Value);
@@ -46,16 +50,30 @@ string basePath
4650
);
4751
if (!string.IsNullOrEmpty(publicMethod))
4852
{
49-
publicMethods.Add(publicMethod);
50-
privateDelegates.Add(privateDelegate);
53+
if (!groupedMethods.TryGetValue(resourceName, out var methods))
54+
{
55+
methods = [];
56+
groupedMethods[resourceName] = methods;
57+
}
58+
methods.Add((publicMethod, privateDelegate));
5159
}
5260
}
5361
}
5462

55-
var publicMethodsCode = string.Join("\n\n", publicMethods);
56-
var privateDelegatesCode = string.Join("\n\n", privateDelegates);
63+
var publicMethodsCode = GenerateGroupedCode(groupedMethods, isPublic: true);
64+
var privateDelegatesCode = GenerateGroupedCode(groupedMethods, isPublic: false);
5765
var typeAliases = GenerateTypeAliasesFile(responseTypes, @namespace);
5866

67+
var namingPolicyCode = jsonNamingPolicy switch
68+
{
69+
var s when s.Equals("PascalCase", StringComparison.OrdinalIgnoreCase) => "null",
70+
var s when s.Equals("snake_case", StringComparison.OrdinalIgnoreCase)
71+
|| s.Equals("snakecase", StringComparison.OrdinalIgnoreCase) => "JsonNamingPolicy.SnakeCaseLower",
72+
_ => "JsonNamingPolicy.CamelCase",
73+
};
74+
75+
var caseInsensitiveCode = caseInsensitive ? "true" : "false";
76+
5977
var extensionMethodsCode = $$"""
6078
using System;
6179
using System.Collections.Generic;
@@ -74,26 +92,24 @@ namespace {{@namespace}};
7492
/// <summary>Extension methods for API operations.</summary>
7593
public static class {{className}}
7694
{
95+
#region Configuration
96+
7797
private static readonly AbsoluteUrl BaseUrl = "{{baseUrl}}".ToAbsoluteUrl();
7898
7999
private static readonly JsonSerializerOptions JsonOptions = new()
80100
{
81-
PropertyNameCaseInsensitive = true,
82-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
101+
PropertyNameCaseInsensitive = {{caseInsensitiveCode}},
102+
PropertyNamingPolicy = {{namingPolicyCode}}
83103
};
84104
85-
#region Public Extension Methods
86-
87-
{{CodeGenerationHelpers.Indent(publicMethodsCode, 1)}}
88-
89105
#endregion
90106
91-
#region Private Members
107+
{{publicMethodsCode}}
92108
93109
private static readonly Deserialize<Unit> _deserializeUnit = static (_, _) =>
94110
Task.FromResult(Unit.Value);
95111
96-
{{CodeGenerationHelpers.Indent(privateDelegatesCode, 1)}}
112+
{{privateDelegatesCode}}
97113
98114
private static ProgressReportingHttpContent CreateJsonContent<T>(T data)
99115
{
@@ -127,14 +143,50 @@ private static async Task<string> DeserializeError(
127143
var content = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
128144
return string.IsNullOrEmpty(content) ? "Unknown error" : content;
129145
}
130-
131-
#endregion
132146
}
133147
""";
134148

135149
return (extensionMethodsCode, typeAliases);
136150
}
137151

152+
private static string GetResourceNameFromPath(string path)
153+
{
154+
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
155+
if (segments.Length == 0)
156+
{
157+
return "General";
158+
}
159+
160+
var resourceSegment = segments[0];
161+
return CodeGenerationHelpers.ToPascalCase(resourceSegment);
162+
}
163+
164+
private static string GenerateGroupedCode(
165+
Dictionary<string, List<(string PublicMethod, string PrivateDelegate)>> groupedMethods,
166+
bool isPublic
167+
)
168+
{
169+
var sections = new List<string>();
170+
171+
foreach (var group in groupedMethods.OrderBy(g => g.Key))
172+
{
173+
var methods = isPublic
174+
? group.Value.Select(m => m.PublicMethod)
175+
: group.Value.Select(m => m.PrivateDelegate);
176+
177+
var methodsCode = string.Join("\n\n", methods);
178+
var indentedContent = CodeGenerationHelpers.Indent(methodsCode, 1);
179+
var regionName = $"{group.Key} Operations";
180+
181+
// #region markers at column 0, content indented by 4 spaces
182+
var section = $" #region {regionName}\n\n{indentedContent}\n\n #endregion";
183+
184+
sections.Add(section);
185+
}
186+
187+
return string.Join("\n\n", sections);
188+
}
189+
138190
private static (string PublicMethod, string PrivateDelegate) GenerateMethod(
139191
string path,
140192
HttpMethod operationType,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
global using Microsoft.OpenApi;
2+
global using Microsoft.OpenApi.Reader;
3+
global using ErrorUrl = Outcome.Result<(string, string), string>.Error<(string, string), string>;
4+
global using GeneratorError = Outcome.Result<
5+
RestClient.Net.OpenApiGenerator.GeneratorResult,
6+
string
7+
>.Error<RestClient.Net.OpenApiGenerator.GeneratorResult, string>;
8+
global using GeneratorOk = Outcome.Result<
9+
RestClient.Net.OpenApiGenerator.GeneratorResult,
10+
string
11+
>.Ok<RestClient.Net.OpenApiGenerator.GeneratorResult, string>;
12+
global using OkUrl = Outcome.Result<(string, string), string>.Ok<(string, string), string>;

RestClient.Net.OpenApiGenerator/ModelGenerator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using Microsoft.OpenApi;
2-
31
namespace RestClient.Net.OpenApiGenerator;
42

53
/// <summary>Generates C# model classes from OpenAPI schemas.</summary>
@@ -46,7 +44,9 @@ private static string GenerateModel(string name, OpenApiSchema schema)
4644
{
4745
var propName = CodeGenerationHelpers.ToPascalCase(p.Key);
4846
var propType = MapOpenApiType(p.Value);
49-
var propDesc = SanitizeDescription((p.Value as OpenApiSchema)?.Description ?? propName);
47+
var propDesc = SanitizeDescription(
48+
(p.Value as OpenApiSchema)?.Description ?? propName
49+
);
5050
return $" /// <summary>{propDesc}</summary>\n public {propType} {propName} {{ get; set; }}";
5151
})
5252
.ToList();

RestClient.Net.OpenApiGenerator/OpenApiCodeGenerator.cs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
using Microsoft.OpenApi;
2-
using Microsoft.OpenApi.Reader;
3-
using ErrorUrl = Outcome.Result<(string, string), string>.Error<(string, string), string>;
4-
using GeneratorError = Outcome.Result<
5-
RestClient.Net.OpenApiGenerator.GeneratorResult,
6-
string
7-
>.Error<RestClient.Net.OpenApiGenerator.GeneratorResult, string>;
8-
using GeneratorOk = Outcome.Result<RestClient.Net.OpenApiGenerator.GeneratorResult, string>.Ok<
9-
RestClient.Net.OpenApiGenerator.GeneratorResult,
10-
string
11-
>;
12-
using OkUrl = Outcome.Result<(string, string), string>.Ok<(string, string), string>;
13-
141
#pragma warning disable CS8509
152

163
namespace RestClient.Net.OpenApiGenerator;
@@ -27,14 +14,18 @@ public static class OpenApiCodeGenerator
2714
/// <param name="className">The class name for extension methods.</param>
2815
/// <param name="outputPath">The directory path where generated files will be saved.</param>
2916
/// <param name="baseUrlOverride">Optional base URL override. Use this when the OpenAPI spec has a relative server URL.</param>
17+
/// <param name="jsonNamingPolicy">JSON naming policy (camelCase, PascalCase, snake_case).</param>
18+
/// <param name="caseInsensitive">Enable case-insensitive JSON deserialization.</param>
3019
/// <returns>A Result containing either the generated code or an error message with exception details.</returns>
3120
#pragma warning disable CA1054
3221
public static Outcome.Result<GeneratorResult, string> Generate(
3322
string openApiContent,
3423
string @namespace,
3524
string className,
3625
string outputPath,
37-
string? baseUrlOverride = null
26+
string? baseUrlOverride = null,
27+
string jsonNamingPolicy = "camelCase",
28+
bool caseInsensitive = true
3829
)
3930
#pragma warning restore CA1054
4031
{
@@ -62,7 +53,9 @@ public static Outcome.Result<GeneratorResult, string> Generate(
6253
className,
6354
outputPath,
6455
baseUrl,
65-
basePath
56+
basePath,
57+
jsonNamingPolicy,
58+
caseInsensitive
6659
),
6760
ErrorUrl(var error) => new GeneratorError($"Error: {error}"),
6861
};
@@ -81,15 +74,19 @@ private static GeneratorOk GenerateCodeFiles(
8174
string className,
8275
string outputPath,
8376
string baseUrl,
84-
string basePath
77+
string basePath,
78+
string jsonNamingPolicy,
79+
bool caseInsensitive
8580
)
8681
{
8782
var (extensionMethods, typeAliases) = ExtensionMethodGenerator.GenerateExtensionMethods(
8883
document,
8984
@namespace,
9085
className,
9186
baseUrl,
92-
basePath
87+
basePath,
88+
jsonNamingPolicy,
89+
caseInsensitive
9390
);
9491
var models = ModelGenerator.GenerateModels(document, @namespace);
9592

0 commit comments

Comments
 (0)