Skip to content

Commit cdfb79e

Browse files
Successfully migrate core OpenAPI parsing from NSwag to Microsoft.OpenApi
Co-authored-by: christianhelle <[email protected]>
1 parent 53f9a2e commit cdfb79e

File tree

6 files changed

+166
-94
lines changed

6 files changed

+166
-94
lines changed

src/CurlGenerator.Core/CurlGenerator.Core.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212

1313
<ItemGroup>
1414
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
15-
<PackageReference Include="NSwag.CodeGeneration.CSharp" Version="14.4.0" />
16-
<PackageReference Include="NSwag.Core.Yaml" Version="14.4.0" />
15+
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.25" />
1716
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.12.0" />
17+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
1818
</ItemGroup>
1919

2020
<ItemGroup>

src/CurlGenerator.Core/OpenApiDocumentFactory.cs

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Net;
2-
using NSwag;
2+
using Microsoft.OpenApi.Models;
3+
using Microsoft.OpenApi.Readers;
34

45
namespace CurlGenerator.Core;
56

@@ -14,33 +15,20 @@ public static class OpenApiDocumentFactory
1415
/// <returns>A new instance of the <see cref="OpenApiDocument"/> class.</returns>
1516
public static async Task<OpenApiDocument> CreateAsync(string openApiPath)
1617
{
17-
OpenApiDocument document;
1818
if (IsHttp(openApiPath))
1919
{
2020
var content = await GetHttpContent(openApiPath);
21-
22-
if (IsYaml(openApiPath))
23-
{
24-
document = await OpenApiYamlDocument.FromYamlAsync(content);
25-
}
26-
else
27-
{
28-
document = await OpenApiDocument.FromJsonAsync(content);
29-
}
21+
var reader = new OpenApiStringReader();
22+
var readResult = reader.Read(content, out var diagnostic);
23+
return readResult;
3024
}
3125
else
3226
{
33-
if (IsYaml(openApiPath))
34-
{
35-
document = await OpenApiYamlDocument.FromFileAsync(openApiPath);
36-
}
37-
else
38-
{
39-
document = await OpenApiDocument.FromFileAsync(openApiPath);
40-
}
27+
using var stream = File.OpenRead(openApiPath);
28+
var reader = new OpenApiStreamReader();
29+
var readResult = reader.Read(stream, out var diagnostic);
30+
return readResult;
4131
}
42-
43-
return document;
4432
}
4533

4634
/// <summary>

src/CurlGenerator.Core/OperationNameGenerator.cs

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
using System.Diagnostics;
22
using System.Diagnostics.CodeAnalysis;
3-
using NSwag;
4-
using NSwag.CodeGeneration.OperationNameGenerators;
3+
using Microsoft.OpenApi.Models;
54

65
namespace CurlGenerator.Core;
76

8-
internal class OperationNameGenerator : IOperationNameGenerator
7+
internal interface IOperationNameGenerator
98
{
10-
private readonly IOperationNameGenerator defaultGenerator =
11-
new MultipleClientsFromOperationIdOperationNameGenerator();
9+
string GetOperationName(
10+
OpenApiDocument document,
11+
string path,
12+
string httpMethod,
13+
OpenApiOperation operation);
14+
}
1215

16+
public class OperationNameGenerator : IOperationNameGenerator
17+
{
1318
[ExcludeFromCodeCoverage]
14-
public bool SupportsMultipleClients => throw new NotImplementedException();
19+
public bool SupportsMultipleClients => false;
1520

1621
[ExcludeFromCodeCoverage]
1722
public string GetClientName(OpenApiDocument document, string path, string httpMethod, OpenApiOperation operation)
1823
{
19-
return defaultGenerator.GetClientName(document, path, httpMethod, operation);
24+
return "ApiClient";
2025
}
2126

2227
public string GetOperationName(
@@ -27,16 +32,24 @@ public string GetOperationName(
2732
{
2833
try
2934
{
30-
return defaultGenerator
31-
.GetOperationName(document, path, httpMethod, operation)
32-
.CapitalizeFirstCharacter()
33-
.ConvertKebabCaseToPascalCase()
34-
.ConvertRouteToCamelCase()
35-
.ConvertSpacesToPascalCase()
36-
.Prefix(
37-
httpMethod
38-
.ToLowerInvariant()
39-
.CapitalizeFirstCharacter());
35+
// Try to use operationId if available
36+
if (!string.IsNullOrWhiteSpace(operation.OperationId))
37+
{
38+
return operation.OperationId
39+
.CapitalizeFirstCharacter()
40+
.ConvertKebabCaseToPascalCase()
41+
.ConvertRouteToCamelCase()
42+
.ConvertSpacesToPascalCase()
43+
.Prefix(
44+
httpMethod
45+
.ToLowerInvariant()
46+
.CapitalizeFirstCharacter());
47+
}
48+
49+
// Fallback to generating from path and method
50+
return httpMethod.CapitalizeFirstCharacter() +
51+
path.ConvertRouteToCamelCase()
52+
.ConvertSpacesToPascalCase();
4053
}
4154
catch (Exception e)
4255
{
@@ -53,14 +66,14 @@ public bool CheckForDuplicateOperationIds(
5366
List<string> operationNames = new();
5467
foreach (var kv in document.Paths)
5568
{
56-
foreach (var operations in kv.Value)
69+
foreach (var operations in kv.Value.Operations)
5770
{
5871
var operation = operations.Value;
5972
operationNames.Add(
6073
GetOperationName(
6174
document,
6275
kv.Key,
63-
operations.Key,
76+
operations.Key.ToString(),
6477
operation));
6578
}
6679
}

src/CurlGenerator.Core/ScriptFileGenerator.cs

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
using System.Linq;
77
using System.Text;
88
using System.Threading.Tasks;
9-
using NSwag;
10-
using NSwag.CodeGeneration.CSharp;
9+
using Microsoft.OpenApi.Models;
1110

1211
public static class ScriptFileGenerator
1312
{
@@ -21,8 +20,7 @@ public static async Task<GeneratorResult> Generate(GeneratorSettings settings)
2120
var document = await OpenApiDocumentFactory.CreateAsync(settings.OpenApiPath);
2221
TryLog($"Document: {SerializeObject(document)}");
2322

24-
var generator = new CSharpClientGenerator(document, new CSharpClientGeneratorSettings());
25-
generator.BaseSettings.OperationNameGenerator = new OperationNameGenerator();
23+
var generator = new OperationNameGenerator();
2624

2725
var baseUrl = settings.BaseUrl + document.Servers?.FirstOrDefault()?.Url;
2826
if (!Uri.IsWellFormedUriString(baseUrl, UriKind.Absolute) &&
@@ -41,23 +39,20 @@ public static async Task<GeneratorResult> Generate(GeneratorSettings settings)
4139
private static GeneratorResult GenerateCode(
4240
GeneratorSettings settings,
4341
OpenApiDocument document,
44-
CSharpClientGenerator generator,
42+
OperationNameGenerator generator,
4543
string baseUrl)
4644
{
4745
var files = new List<ScriptFile>();
4846
foreach (var kv in document.Paths)
4947
{
5048
TryLog($"Processing path: {kv.Key}");
51-
foreach (var operations in kv.Value)
49+
foreach (var operations in kv.Value.Operations)
5250
{
5351
TryLog($"Processing operation: {operations.Key}");
5452

5553
var operation = operations.Value;
56-
var verb = operations.Key.CapitalizeFirstCharacter();
57-
var name = generator
58-
.BaseSettings
59-
.OperationNameGenerator
60-
.GetOperationName(document, kv.Key, verb, operation);
54+
var verb = operations.Key.ToString().CapitalizeFirstCharacter();
55+
var name = generator.GetOperationName(document, kv.Key, verb, operation);
6156

6257
var filename = !settings.GenerateBashScripts
6358
? $"{name.CapitalizeFirstCharacter()}.ps1"
@@ -99,7 +94,7 @@ private static string GenerateBashRequest(
9994

10095
// Add query parameters directly to the URL if there are any
10196
var queryParams = operation.Parameters
102-
.Where(p => p.Kind == OpenApiParameterKind.Query)
97+
.Where(p => p.In == ParameterLocation.Query)
10398
.Select(p => $"{p.Name}=${{{p.Name.ConvertKebabCaseToSnakeCase()}}}")
10499
.ToList();
105100

@@ -108,9 +103,8 @@ private static string GenerateBashRequest(
108103

109104
code.AppendLine($" -H \"Accept: application/json\" \\");
110105

111-
// Determine content type based on consumes or request body
112-
var contentType = operation.Consumes?.FirstOrDefault()
113-
?? operation.RequestBody?.Content?.Keys.FirstOrDefault()
106+
// Determine content type based on request body
107+
var contentType = operation.RequestBody?.Content?.Keys.FirstOrDefault()
114108
?? "application/json";
115109

116110
TryLog($"Content type for operation {operation.OperationId}: {contentType}");
@@ -138,8 +132,8 @@ private static string GenerateBashRequest(
138132
}
139133
else
140134
{
141-
var requestBodySchema = operation.RequestBody.Content[contentType].Schema.ActualSchema;
142-
var requestBodyJson = requestBodySchema?.ToSampleJson()?.ToString() ?? string.Empty;
135+
var requestBodySchema = operation.RequestBody.Content[contentType].Schema;
136+
var requestBodyJson = GenerateSampleJsonFromSchema(requestBodySchema);
143137
code.AppendLine($" -d '{requestBodyJson}'");
144138
}
145139
}
@@ -162,10 +156,10 @@ private static void AppendBashParameters(
162156
{
163157
var parameters = operation.Parameters
164158
.Where(p =>
165-
p.Kind == OpenApiParameterKind.Path ||
166-
p.Kind == OpenApiParameterKind.Query ||
167-
p.Kind == OpenApiParameterKind.Header ||
168-
p.Kind == OpenApiParameterKind.Cookie)
159+
p.In == ParameterLocation.Path ||
160+
p.In == ParameterLocation.Query ||
161+
p.In == ParameterLocation.Header ||
162+
p.In == ParameterLocation.Cookie)
169163
.ToArray();
170164

171165
if (parameters.Length == 0)
@@ -181,7 +175,7 @@ private static void AppendBashParameters(
181175
var name = parameter.Name.ConvertKebabCaseToSnakeCase();
182176
code.AppendLine(
183177
parameter.Description is null
184-
? $"# {parameter.Kind.ToString().ToLowerInvariant()} parameter: {name}"
178+
? $"# {parameter.In.ToString().ToLowerInvariant()} parameter: {name}"
185179
: $"# {parameter.Description}");
186180

187181
code.AppendLine($"{name}=\"\""); // Initialize the parameter
@@ -246,6 +240,90 @@ private static string SerializeObject(object obj)
246240
return Newtonsoft.Json.JsonConvert.SerializeObject(obj, Newtonsoft.Json.Formatting.Indented);
247241
}
248242

243+
private static string GenerateSampleJsonFromSchema(OpenApiSchema? schema)
244+
{
245+
if (schema == null)
246+
return "{}";
247+
248+
try
249+
{
250+
var sampleObject = GenerateSampleObjectFromSchema(schema);
251+
return Newtonsoft.Json.JsonConvert.SerializeObject(sampleObject, Newtonsoft.Json.Formatting.Indented);
252+
}
253+
catch
254+
{
255+
return "{}";
256+
}
257+
}
258+
259+
private static object GenerateSampleObjectFromSchema(OpenApiSchema schema)
260+
{
261+
if (schema.Example != null)
262+
{
263+
return ConvertOpenApiAnyToObject(schema.Example);
264+
}
265+
266+
switch (schema.Type)
267+
{
268+
case "object":
269+
var obj = new Dictionary<string, object>();
270+
if (schema.Properties != null)
271+
{
272+
foreach (var prop in schema.Properties)
273+
{
274+
obj[prop.Key] = GenerateSampleObjectFromSchema(prop.Value);
275+
}
276+
}
277+
return obj;
278+
279+
case "array":
280+
if (schema.Items != null)
281+
{
282+
return new[] { GenerateSampleObjectFromSchema(schema.Items) };
283+
}
284+
return new object[0];
285+
286+
case "string":
287+
return schema.Format switch
288+
{
289+
"date" => DateTime.Today.ToString("yyyy-MM-dd"),
290+
"date-time" => DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ssZ"),
291+
"email" => "[email protected]",
292+
"uri" => "https://example.com",
293+
_ => "string"
294+
};
295+
296+
case "integer":
297+
return 0;
298+
299+
case "number":
300+
return 0.0;
301+
302+
case "boolean":
303+
return false;
304+
305+
default:
306+
return "value";
307+
}
308+
}
309+
310+
private static object ConvertOpenApiAnyToObject(Microsoft.OpenApi.Any.IOpenApiAny openApiAny)
311+
{
312+
return openApiAny switch
313+
{
314+
Microsoft.OpenApi.Any.OpenApiString str => str.Value,
315+
Microsoft.OpenApi.Any.OpenApiInteger integer => integer.Value,
316+
Microsoft.OpenApi.Any.OpenApiLong longVal => longVal.Value,
317+
Microsoft.OpenApi.Any.OpenApiFloat floatVal => floatVal.Value,
318+
Microsoft.OpenApi.Any.OpenApiDouble doubleVal => doubleVal.Value,
319+
Microsoft.OpenApi.Any.OpenApiBoolean boolVal => boolVal.Value,
320+
Microsoft.OpenApi.Any.OpenApiDateTime dateTime => dateTime.Value,
321+
Microsoft.OpenApi.Any.OpenApiObject obj => obj.ToDictionary(kv => kv.Key, kv => ConvertOpenApiAnyToObject(kv.Value)),
322+
Microsoft.OpenApi.Any.OpenApiArray array => array.Select(ConvertOpenApiAnyToObject).ToArray(),
323+
_ => openApiAny.ToString() ?? "value"
324+
};
325+
}
326+
249327

250328
private static string GenerateRequest(
251329
GeneratorSettings settings,
@@ -295,8 +373,8 @@ private static string GenerateRequest(
295373
return code.ToString();
296374

297375
var requestBody = operation.RequestBody;
298-
var requestBodySchema = requestBody.Content[contentType].Schema.ActualSchema;
299-
var requestBodyJson = requestBodySchema?.ToSampleJson()?.ToString() ?? string.Empty;
376+
var requestBodySchema = requestBody.Content[contentType].Schema;
377+
var requestBodyJson = GenerateSampleJsonFromSchema(requestBodySchema);
300378

301379
code.AppendLine($" -d '{requestBodyJson}'");
302380
return code.ToString();
@@ -310,7 +388,7 @@ private static Dictionary<string, string> AppendParameters(
310388
{
311389
var parameters = operation
312390
.Parameters
313-
.Where(c => c.Kind is OpenApiParameterKind.Path or OpenApiParameterKind.Query)
391+
.Where(c => c.In is ParameterLocation.Path or ParameterLocation.Query)
314392
.ToArray();
315393

316394
if (parameters.Length == 0)

src/CurlGenerator.Tests/CurlGenerator.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<PackageReference Include="Atc.Test" Version="1.1.18" />
1414
<PackageReference Include="FluentAssertions" Version="7.2.0" />
1515
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
16+
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.25" />
1617
<PackageReference Include="xunit" Version="2.9.3" />
1718
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
1819
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

0 commit comments

Comments
 (0)