Skip to content

Commit d29e6a3

Browse files
committed
feat: add support for yaml formatted OpenAPI specifications
1 parent b5e96c0 commit d29e6a3

9 files changed

Lines changed: 178 additions & 48 deletions

File tree

src/OpenAPI.WebApiGenerator/ApiGenerator.cs

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4-
using System.IO;
54
using System.Linq;
65
using System.Net.Http;
7-
using System.Text.Json;
86
using Corvus.Json;
97
using Microsoft.CodeAnalysis;
108
using Microsoft.CodeAnalysis.Text;
@@ -22,12 +20,13 @@ public sealed class ApiGenerator : IIncrementalGenerator
2220
public void Initialize(IncrementalGeneratorInitializationContext context)
2321
{
2422
// Debugger.Launch();
25-
2623
var provider = context.AdditionalTextsProvider
27-
.Where(additionalText => Path.GetFileName(additionalText.Path).EndsWith(".json"))
24+
.Where(additionalText => additionalText.IsOpenApiFileFormat())
2825
.Collect();
2926

30-
var openapiDocumentProvider = provider.Select((array, _) => array.First());
27+
var openapiDocumentProvider = provider.Select((array, _) =>
28+
array.FirstOrDefault() ??
29+
throw new InvalidOperationException($"No OpenAPI specification found in AdditionalFiles. Expected any file with extension {string.Join(" ,", AdditionalTextExtensions.OpenApiFileExtensions)}"));
3130

3231
var openApiProvider = openapiDocumentProvider
3332
.Combine(context.CompilationProvider)
@@ -48,40 +47,21 @@ private static void GenerateCode(SourceProductionContext context, (
4847
var rootNamespace = compilation.Assembly.Name;
4948

5049
var openApiDocumentFile = generatorContext.OpenApiDocument;
51-
var jsonValidationExceptionGenerator = new JsonValidationExceptionGenerator(rootNamespace);
52-
jsonValidationExceptionGenerator.GenerateJsonValidationExceptionClass().AddTo(context);
50+
var openApiDocumentStream = openApiDocumentFile.AsOpenApiStream();
5351

54-
var endpointGenerator = new OperationGenerator(compilation, jsonValidationExceptionGenerator);
55-
var openApiResult = OpenApiDocument.Load(openApiDocumentFile.AsStream(), "json");
56-
var openApiVersion = openApiResult.Diagnostic?.SpecificationVersion ??
57-
throw new InvalidOperationException("Unknown openapi version");
58-
if (openApiResult.Diagnostic.Errors.Any())
59-
{
60-
throw new InvalidOperationException(
61-
openApiResult.Diagnostic.Errors.AggregateToString(
62-
"Errors while parsing OpenAPI specification: ",
63-
error => $"{(error.Pointer == null ? "" : $"{error.Pointer}: ")}{error.Message}"));
64-
}
65-
var openApi = openApiResult.Document ??
66-
throw new InvalidOperationException(
67-
$"Could not load OpenAPI document {openApiDocumentFile.Path}");
52+
var openApiSpecification = openApiDocumentStream.LoadOpenApiDocument();
53+
var openApiVersion = openApiSpecification.Version;
54+
var openApi = openApiSpecification.Document;
6855

69-
70-
var openApiUri = new JsonReference(openApi.BaseUri.ToString());
71-
var documentResolver = new PrepopulatedDocumentResolver();
72-
var openApiDocument = JsonDocument.Parse(generatorContext.OpenApiDocument.AsStream());
73-
if (!documentResolver.AddDocument(openApiUri, openApiDocument))
74-
{
75-
throw new InvalidOperationException("Could not add OpenApi document");
76-
}
7756
var schemaGenerator = SchemaGenerator.For(
78-
openApiVersion,
79-
documentResolver,
80-
rootNamespace,
81-
context);
57+
openApiSpecification, rootNamespace, context);
8258

83-
var openApiReference = new OpenApiReference<OpenApiDocument>(openApi, openApiDocument, openApiUri);
84-
var openApiVisitor = OpenApiVisitor.V(openApiVersion, openApiReference);
59+
var openApiVisitor = OpenApiVisitor.V(openApiSpecification);
60+
61+
var jsonValidationExceptionGenerator = new JsonValidationExceptionGenerator(rootNamespace);
62+
jsonValidationExceptionGenerator.GenerateJsonValidationExceptionClass().AddTo(context);
63+
64+
var endpointGenerator = new OperationGenerator(compilation, jsonValidationExceptionGenerator);
8565

8666
var httpRequestExtensionsGenerator = new HttpRequestExtensionsGenerator(
8767
openApiVersion,

src/OpenAPI.WebApiGenerator/CodeGeneration/SchemaGenerator.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Immutable;
44
using System.IO;
55
using System.Linq;
6+
using System.Text.Json;
67
using Corvus.Json;
78
using Corvus.Json.CodeGeneration;
89
using Corvus.Json.CodeGeneration.CSharp;
@@ -25,12 +26,11 @@ internal sealed class SchemaGenerator(
2526
private readonly HashSet<string> _fileCache = [];
2627

2728
internal static SchemaGenerator For(
28-
OpenApiSpecVersion openApiSpecVersion,
29-
IDocumentResolver documentResolver,
29+
OpenApiSpecification openApiSpecification,
3030
string rootNamespace,
3131
SourceProductionContext context)
3232
{
33-
var vocabulary = openApiSpecVersion switch
33+
var vocabulary = openApiSpecification.Version switch
3434
{
3535
OpenApiSpecVersion.OpenApi2_0 =>
3636
Corvus.Json.CodeGeneration.Draft4.VocabularyAnalyser.DefaultVocabulary,
@@ -40,8 +40,14 @@ internal static SchemaGenerator For(
4040
Corvus.Json.CodeGeneration.Draft202012.VocabularyAnalyser.DefaultVocabulary,
4141
OpenApiSpecVersion.OpenApi3_2 =>
4242
Corvus.Json.CodeGeneration.Draft202012.VocabularyAnalyser.DefaultVocabulary,
43-
_ => throw new InvalidOperationException($"OpenAPI specification {openApiSpecVersion} is not supported")
43+
_ => throw new InvalidOperationException($"OpenAPI specification {openApiSpecification.Version} is not supported")
4444
};
45+
var documentResolver = new PrepopulatedDocumentResolver();
46+
if (!documentResolver.AddDocument(openApiSpecification.Url, openApiSpecification.JsonDocument))
47+
{
48+
throw new InvalidOperationException("Could not add OpenApi document");
49+
}
50+
4551
var globalOptions =
4652
new SourceGeneratorHelpers.GlobalOptions(
4753
fallbackVocabulary: vocabulary,

src/OpenAPI.WebApiGenerator/Extensions/AdditionalTextExtensions.cs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,44 @@
1-
using System.IO;
1+
using System;
2+
using System.IO;
3+
using System.Linq;
24
using System.Text;
35
using Microsoft.CodeAnalysis;
6+
using OpenAPI.WebApiGenerator.OpenApi;
7+
using Path = System.IO.Path;
48

59
namespace OpenAPI.WebApiGenerator.Extensions;
610

711
internal static class AdditionalTextExtensions
812
{
9-
internal static MemoryStream AsStream(this AdditionalText text)
13+
internal static readonly string[] OpenApiFileExtensions =
14+
Enum.GetNames(typeof(OpenApiFileFormat))
15+
.Select(openApiFileFormat =>
16+
$".{openApiFileFormat.ToLowerInvariant()}")
17+
.ToArray();
18+
19+
internal static bool IsOpenApiFileFormat(this AdditionalText text)
20+
{
21+
var extension = text.GetExtension();
22+
return OpenApiFileExtensions.Contains(extension);
23+
}
24+
25+
private static OpenApiFileFormat GetOpenApiFileFormat(this AdditionalText text)
26+
{
27+
var format = text.GetExtension().TrimStart('.');
28+
if (Enum.TryParse<OpenApiFileFormat>(format, true, out var openApiFileFormat))
29+
{
30+
return openApiFileFormat;
31+
}
32+
33+
throw new InvalidOperationException(
34+
$"{text.Path} is not a recognized OpenAPI file format. Expected one of {string.Join(", ", Enum.GetNames(typeof(OpenApiFileFormat)))}");
35+
}
36+
37+
internal static OpenApiStream AsOpenApiStream(this AdditionalText text)
1038
{
1139
var content = text.GetText();
12-
var stream = new MemoryStream();
40+
var format = text.GetOpenApiFileFormat();
41+
var stream = new OpenApiStream(format);
1342
if (content is null)
1443
{
1544
return stream;
@@ -23,4 +52,7 @@ internal static MemoryStream AsStream(this AdditionalText text)
2352
stream.Position = 0;
2453
return stream;
2554
}
55+
56+
private static string GetExtension(this AdditionalText text) =>
57+
Path.GetExtension(text.Path);
2658
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text.Json;
6+
using Corvus.Json;
7+
using Microsoft.OpenApi;
8+
using Microsoft.OpenApi.Reader;
9+
using Microsoft.OpenApi.YamlReader;
10+
using OpenAPI.WebApiGenerator.OpenApi;
11+
using SharpYaml.Serialization;
12+
13+
namespace OpenAPI.WebApiGenerator.Extensions;
14+
15+
internal static class StreamExtensions
16+
{
17+
private static JsonDocument LoadJsonDocument(this OpenApiStream stream)
18+
{
19+
stream.Position = 0;
20+
return stream.Format switch
21+
{
22+
OpenApiFileFormat.Json => JsonDocument.Parse(stream),
23+
OpenApiFileFormat.Yml or OpenApiFileFormat.Yaml => GetFromYaml(),
24+
_ => throw new ArgumentOutOfRangeException(nameof(stream.Format), stream.Format, "Supported formats are json, yml and yaml")
25+
};
26+
27+
JsonDocument GetFromYaml()
28+
{
29+
var yamlStream = new YamlStream();
30+
yamlStream.Load(new StreamReader(stream));
31+
return JsonDocument.Parse(yamlStream.First().ToJsonNode().ToJsonString());
32+
}
33+
}
34+
35+
private static readonly OpenApiJsonReader JsonDocumentReader = new();
36+
private static readonly OpenApiYamlReader YamlDocumentReader = new();
37+
private static readonly Dictionary<string, IOpenApiReader> DocumentReaders = new()
38+
{
39+
{ "json", JsonDocumentReader },
40+
{ "yaml", YamlDocumentReader },
41+
{ "yml", YamlDocumentReader }
42+
};
43+
44+
internal static OpenApiSpecification LoadOpenApiDocument(this OpenApiStream stream)
45+
{
46+
stream.Position = 0;
47+
var openApiResult = OpenApiDocument.Load(
48+
stream,
49+
Enum.GetName(typeof(OpenApiFileFormat), stream.Format)?.ToLowerInvariant(),
50+
new OpenApiReaderSettings
51+
{
52+
Readers = DocumentReaders,
53+
LeaveStreamOpen = true
54+
});
55+
var version = openApiResult.Diagnostic?.SpecificationVersion ??
56+
throw new InvalidOperationException("Unknown openapi version");
57+
if (openApiResult.Diagnostic.Errors.Any())
58+
{
59+
throw new InvalidOperationException(
60+
openApiResult.Diagnostic.Errors.AggregateToString(
61+
"Errors while parsing OpenAPI specification: ",
62+
error => $"{(error.Pointer == null ? "" : $"{error.Pointer}: ")}{error.Message}"));
63+
}
64+
var document = openApiResult.Document ??
65+
throw new InvalidOperationException(
66+
"OpenAPI document is empty");
67+
var openApiUri = new JsonReference(document.BaseUri.ToString());
68+
var jsonDocument = stream.LoadJsonDocument();
69+
return new OpenApiSpecification(document, version, openApiUri, jsonDocument);
70+
}
71+
}

src/OpenAPI.WebApiGenerator/OpenAPI.WebApiGenerator.csproj

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@
3939
PackagePath="analyzers/dotnet/cs" Visible="false" />
4040
<None Include="$(PkgCorvus_Json_SourceGeneratorTools)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
4141
<None Include="$(PkgMicrosoft_OpenApi)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
42+
<None Include="$(PkgMicrosoft_OpenApi_YamlReader)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
4243
<None Include="$(PkgMicrosoft_Bcl_HashCode)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
43-
<None Include="$(PkgYaml2JsonNode)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
4444
<None Include="$(PkgSystem_Text_Json)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
45+
<None Include="$(PkgYamlDotNet)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
4546
</ItemGroup>
4647

4748
<ItemGroup>
@@ -58,21 +59,23 @@
5859
</PackageReference>
5960
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all" />
6061
<PackageReference Include="Microsoft.OpenApi" Version="3.1.3" OutputItemType="Analyzer" PrivateAssets="all" GeneratePathProperty="true" />
62+
<PackageReference Include="Microsoft.OpenApi.YamlReader" Version="3.1.3" OutputItemType="Analyzer" PrivateAssets="all" GeneratePathProperty="true" />
6163
<PackageReference Include="Nullable" Version="1.3.1">
6264
<PrivateAssets>all</PrivateAssets>
6365
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
6466
</PackageReference>
6567
<PackageReference Include="System.Text.Json" Version="9.0.12" OutputItemType="Analyzer" PrivateAssets="all" GeneratePathProperty="true" />
66-
<PackageReference Include="Yaml2JsonNode" Version="2.2.0" OutputItemType="Analyzer" PrivateAssets="all" GeneratePathProperty="true" />
68+
<PackageReference Include="YamlDotNet" Version="16.2.0" OutputItemType="Analyzer" PrivateAssets="all" GeneratePathProperty="true" />
6769
</ItemGroup>
6870

6971
<Target Name="GetDependencyTargetPaths">
7072
<ItemGroup>
7173
<TargetPathWithTargetPlatformMoniker Include="$(PkgCorvus_Json_SourceGeneratorTools)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
7274
<TargetPathWithTargetPlatformMoniker Include="$(PkgMicrosoft_OpenApi)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
75+
<TargetPathWithTargetPlatformMoniker Include="$(PkgMicrosoft_OpenApi_YamlReader)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
7376
<TargetPathWithTargetPlatformMoniker Include="$(PkgMicrosoft_Bcl_HashCode)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
74-
<TargetPathWithTargetPlatformMoniker Include="$(PkgYaml2JsonNode)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
7577
<TargetPathWithTargetPlatformMoniker Include="$(PkgSystem_Text_Json)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
78+
<TargetPathWithTargetPlatformMoniker Include="$(PkgYamlDotNet)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
7679
</ItemGroup>
7780
</Target>
7881

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace OpenAPI.WebApiGenerator.OpenApi;
2+
3+
internal enum OpenApiFileFormat
4+
{
5+
Json,
6+
Yml,
7+
Yaml
8+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Text.Json;
2+
using Corvus.Json;
3+
using Microsoft.OpenApi;
4+
5+
namespace OpenAPI.WebApiGenerator.OpenApi;
6+
7+
internal sealed class OpenApiSpecification(
8+
OpenApiDocument document,
9+
OpenApiSpecVersion version,
10+
JsonReference url,
11+
JsonDocument jsonDocument)
12+
{
13+
public OpenApiDocument Document { get; } = document;
14+
public OpenApiSpecVersion Version { get; } = version;
15+
public JsonReference Url { get; } = url;
16+
public JsonDocument JsonDocument { get; } = jsonDocument;
17+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System.IO;
2+
3+
namespace OpenAPI.WebApiGenerator.OpenApi;
4+
5+
internal sealed class OpenApiStream(OpenApiFileFormat format) : MemoryStream
6+
{
7+
public OpenApiFileFormat Format { get; } = format;
8+
}

src/OpenAPI.WebApiGenerator/OpenApi/Visitor/OpenApiVisitor.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@ namespace OpenAPI.WebApiGenerator.OpenApi.Visitor;
1111

1212
internal abstract class OpenApiVisitor
1313
{
14-
public static IOpenApiVisitor V(OpenApiSpecVersion version, OpenApiReference<OpenApiDocument> openApiReference) =>
15-
version switch
14+
public static IOpenApiVisitor V(OpenApiSpecification openApiSpecification)
15+
{
16+
var openApiReference = new OpenApiReference<OpenApiDocument>(openApiSpecification.Document,
17+
openApiSpecification.JsonDocument, openApiSpecification.Url);
18+
19+
return openApiSpecification.Version switch
1620
{
1721
OpenApiSpecVersion.OpenApi2_0 => OpenApiV2Visitor.Visit(openApiReference),
1822
OpenApiSpecVersion.OpenApi3_0 => OpenApiV3Visitor.Visit(openApiReference),
1923
OpenApiSpecVersion.OpenApi3_1 => OpenApiV3Visitor.Visit(openApiReference),
2024
OpenApiSpecVersion.OpenApi3_2 => OpenApiV3Visitor.Visit(openApiReference),
21-
_ => throw new InvalidOperationException($"OpenAPI version {version} not supported")
25+
_ => throw new InvalidOperationException($"OpenAPI version {openApiSpecification.Version} not supported")
2226
};
27+
}
2328
}
2429

2530
internal abstract class OpenApiVisitor<T>(

0 commit comments

Comments
 (0)