diff --git a/.github/workflows/lint-openapi.yml b/.github/workflows/lint-openapi.yml
new file mode 100644
index 0000000..3324bcd
--- /dev/null
+++ b/.github/workflows/lint-openapi.yml
@@ -0,0 +1,21 @@
+name: Lint OpenAPI Specs
+
+on:
+ push:
+ branches:
+ - "**"
+ tags-ignore:
+ - '**'
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install vacuum
+ run: curl -fsSL https://quobix.com/scripts/install_vacuum.sh | sh
+
+ - name: Lint OpenAPI specs
+ run: |
+ vacuum lint --globbed-files="*/**/openapi.json"
diff --git a/OpenAPI.WebApiGenerator.sln b/OpenAPI.WebApiGenerator.sln
index b66a631..97dda50 100644
--- a/OpenAPI.WebApiGenerator.sln
+++ b/OpenAPI.WebApiGenerator.sln
@@ -2,40 +2,138 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAPI.WebApiGenerator", "src\OpenAPI.WebApiGenerator\OpenAPI.WebApiGenerator.csproj", "{E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.Api", "tests\Example.Api\Example.Api.csproj", "{790AE9B7-F3EA-459C-BAB2-D75E903D9B39}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi20", "tests\Example.OpenApi20\Example.OpenApi20.csproj", "{790AE9B7-F3EA-459C-BAB2-D75E903D9B39}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.Api.IntegrationTests", "tests\Example.Api.IntegrationTests\Example.Api.IntegrationTests.csproj", "{2A585540-1B80-4848-9A93-E0286758E2E0}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi20.IntegrationTests", "tests\Example.OpenApi20.IntegrationTests\Example.OpenApi20.IntegrationTests.csproj", "{2A585540-1B80-4848-9A93-E0286758E2E0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAPI.WebApiGenerator.Tests", "tests\OpenAPI.WebApiGenerator.Tests\OpenAPI.WebApiGenerator.Tests.csproj", "{8044D11A-B0D2-400A-B2A1-8C50E396073A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi30", "tests\Example.OpenApi30\Example.OpenApi30.csproj", "{B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi30.IntegrationTests", "tests\Example.OpenApi30.IntegrationTests\Example.OpenApi30.IntegrationTests.csproj", "{C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{F4FDC271-0CCB-4171-87B8-937A6E1CBF9A}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
.github\workflows\cd.yml = .github\workflows\cd.yml
LICENSE = LICENSE
+ .github\workflows\lint-openapi.yml = .github\workflows\lint-openapi.yml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi31", "tests\Example.OpenApi31\Example.OpenApi31.csproj", "{FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi31.IntegrationTests", "tests\Example.OpenApi31.IntegrationTests\Example.OpenApi31.IntegrationTests.csproj", "{FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Debug|x64.Build.0 = Debug|Any CPU
+ {E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Debug|x86.Build.0 = Debug|Any CPU
{E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Release|x64.ActiveCfg = Release|Any CPU
+ {E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Release|x64.Build.0 = Release|Any CPU
+ {E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Release|x86.ActiveCfg = Release|Any CPU
+ {E2935A8A-ED91-4A1D-BEF4-08D916A7ED07}.Release|x86.Build.0 = Release|Any CPU
{790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Debug|x64.Build.0 = Debug|Any CPU
+ {790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Debug|x86.Build.0 = Debug|Any CPU
{790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Release|Any CPU.Build.0 = Release|Any CPU
+ {790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Release|x64.ActiveCfg = Release|Any CPU
+ {790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Release|x64.Build.0 = Release|Any CPU
+ {790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Release|x86.ActiveCfg = Release|Any CPU
+ {790AE9B7-F3EA-459C-BAB2-D75E903D9B39}.Release|x86.Build.0 = Release|Any CPU
{2A585540-1B80-4848-9A93-E0286758E2E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A585540-1B80-4848-9A93-E0286758E2E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2A585540-1B80-4848-9A93-E0286758E2E0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2A585540-1B80-4848-9A93-E0286758E2E0}.Debug|x64.Build.0 = Debug|Any CPU
+ {2A585540-1B80-4848-9A93-E0286758E2E0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2A585540-1B80-4848-9A93-E0286758E2E0}.Debug|x86.Build.0 = Debug|Any CPU
{2A585540-1B80-4848-9A93-E0286758E2E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A585540-1B80-4848-9A93-E0286758E2E0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2A585540-1B80-4848-9A93-E0286758E2E0}.Release|x64.ActiveCfg = Release|Any CPU
+ {2A585540-1B80-4848-9A93-E0286758E2E0}.Release|x64.Build.0 = Release|Any CPU
+ {2A585540-1B80-4848-9A93-E0286758E2E0}.Release|x86.ActiveCfg = Release|Any CPU
+ {2A585540-1B80-4848-9A93-E0286758E2E0}.Release|x86.Build.0 = Release|Any CPU
{8044D11A-B0D2-400A-B2A1-8C50E396073A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8044D11A-B0D2-400A-B2A1-8C50E396073A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8044D11A-B0D2-400A-B2A1-8C50E396073A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8044D11A-B0D2-400A-B2A1-8C50E396073A}.Debug|x64.Build.0 = Debug|Any CPU
+ {8044D11A-B0D2-400A-B2A1-8C50E396073A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8044D11A-B0D2-400A-B2A1-8C50E396073A}.Debug|x86.Build.0 = Debug|Any CPU
{8044D11A-B0D2-400A-B2A1-8C50E396073A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8044D11A-B0D2-400A-B2A1-8C50E396073A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8044D11A-B0D2-400A-B2A1-8C50E396073A}.Release|x64.ActiveCfg = Release|Any CPU
+ {8044D11A-B0D2-400A-B2A1-8C50E396073A}.Release|x64.Build.0 = Release|Any CPU
+ {8044D11A-B0D2-400A-B2A1-8C50E396073A}.Release|x86.ActiveCfg = Release|Any CPU
+ {8044D11A-B0D2-400A-B2A1-8C50E396073A}.Release|x86.Build.0 = Release|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Debug|x64.Build.0 = Debug|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Debug|x86.Build.0 = Debug|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Release|x64.ActiveCfg = Release|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Release|x64.Build.0 = Release|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Release|x86.ActiveCfg = Release|Any CPU
+ {B1C2D3E4-F5A6-47B8-9C0D-1E2F3A4B5C6D}.Release|x86.Build.0 = Release|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Debug|x64.Build.0 = Debug|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Debug|x86.Build.0 = Debug|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Release|x64.ActiveCfg = Release|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Release|x64.Build.0 = Release|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Release|x86.ActiveCfg = Release|Any CPU
+ {C2D3E4F5-A6B7-48C9-0D1E-2F3A4B5C6D7E}.Release|x86.Build.0 = Release|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Debug|x64.Build.0 = Debug|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Debug|x86.Build.0 = Debug|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Release|x64.ActiveCfg = Release|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Release|x64.Build.0 = Release|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Release|x86.ActiveCfg = Release|Any CPU
+ {FF8E3B7A-20A5-4702-871C-D8ABC6D82F09}.Release|x86.Build.0 = Release|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Debug|x64.Build.0 = Debug|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Debug|x86.Build.0 = Debug|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Release|x64.ActiveCfg = Release|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Release|x64.Build.0 = Release|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Release|x86.ActiveCfg = Release|Any CPU
+ {FE9CE314-77B1-4064-B4E3-F2FCE3EDB63C}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index f9d14b9..74fc4ee 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,17 @@ Generates scaffolding for Web APIs from OpenAPI specifications.
The generated functionality will route, serialize/deserialize and validate payloads according to the specification.
+Supported OpenAPI version:
+- [2.0](https://spec.openapis.org/oas/v2.0.html)
+- [3.0.0](https://spec.openapis.org/oas/v3.0.0.html)
+- [3.0.1](https://spec.openapis.org/oas/v3.0.1.html)
+- [3.0.2](https://spec.openapis.org/oas/v3.0.2.html)
+- [3.0.3](https://spec.openapis.org/oas/v3.0.3.html)
+- [3.0.4](https://spec.openapis.org/oas/v3.0.4.html)
+- [3.1.0](https://spec.openapis.org/oas/v3.1.0.html)
+- [3.1.1](https://spec.openapis.org/oas/v3.1.1.html)
+- [3.1.2](https://spec.openapis.org/oas/v3.1.2.html)
+
API frameworks supported:
- [Minimal API](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis)
@@ -34,11 +45,11 @@ https://www.nuget.org/packages/WebApiGenerator.OpenAPI
```
-
+
```
* Corvus.Json.ExtendedTypes >= 4.0.0
-* ParameterStyleParsers.OpenAPI >= 1.1.0
+* ParameterStyleParsers.OpenAPI >= 1.4.0
4. Compile the project.
@@ -52,9 +63,14 @@ app.MapOperations();
app.Run();
```
-See [Example.Api](tests/Example.Api) as an example.
+Examples:
+- [OpenAPI 2.0](tests/Example.OpenApi20)
+- [OpenAPI 3.0](tests/Example.OpenApi30)
+- [OpenAPI 3.1](tests/Example.OpenApi31)
+
+All specifications mostly generate similar abstractions. What might differ is the location of generated resources, which follows the respective structure of the OpenAPI specification, and the JSON types, which are based on the respective schema version.
-**Note**: The Example.Api references the generator through a project reference. Use a package reference instead as described above.
+**Note**: The Examples reference the generator through a project reference. Use a package reference instead as described above.
## Implementing an [API Operation](https://swagger.io/specification/#operation-object)
The generator generates stubbed partial classes for any operation handlers (`Foo.Bar.Operation.Handler.cs`) if there are none existing in the project and logs it with a compiler warning (AF1001). The classes should be copied into source control and the operation methods implemented. The operation methods have a familiar request/response design:
diff --git a/src/OpenAPI.WebApiGenerator/AnalyzerReleases.Unshipped.md b/src/OpenAPI.WebApiGenerator/AnalyzerReleases.Unshipped.md
index 6860b5e..4e89ed1 100644
--- a/src/OpenAPI.WebApiGenerator/AnalyzerReleases.Unshipped.md
+++ b/src/OpenAPI.WebApiGenerator/AnalyzerReleases.Unshipped.md
@@ -3,5 +3,4 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
AF0001 | Compiler | Error | ApiGenerator
-AF1001 | Api | Warning | EndpointGenerator
-CRV1001 | JsonSchemaCodeGenerator | Error | ApiGenerator
\ No newline at end of file
+AF1001 | Api | Warning | EndpointGenerator
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/ApiGenerator.cs b/src/OpenAPI.WebApiGenerator/ApiGenerator.cs
index b5ea589..a7058df 100644
--- a/src/OpenAPI.WebApiGenerator/ApiGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/ApiGenerator.cs
@@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using Corvus.Json;
-using Corvus.Json.SourceGeneratorTools;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Microsoft.OpenApi;
@@ -31,35 +29,21 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var openapiDocumentProvider = provider.Select((array, _) => array.First());
- // Get global options
- var globalOptions =
- context.AnalyzerConfigOptionsProvider.Select((optionsProvider, token) =>
- new SourceGeneratorHelpers.GlobalOptions(
- fallbackVocabulary: Corvus.Json.CodeGeneration.Draft4.VocabularyAnalyser.DefaultVocabulary,
- optionalAsNullable: true,
- useOptionalNameHeuristics: true,
- alwaysAssertFormat: true,
- ImmutableArray.Empty));
-
- var openApiProvider = globalOptions
- .Combine(openapiDocumentProvider)
+ var openApiProvider = openapiDocumentProvider
.Combine(context.CompilationProvider)
.Select((tuple, _) => (
- Options: tuple.Left.Left,
- OpenApiDocument: tuple.Left.Right,
+ OpenApiDocument: tuple.Left,
Compilation: tuple.Right
));
context.RegisterSourceOutput(openApiProvider,
- WithExceptionReporting<(SourceGeneratorHelpers.GlobalOptions, AdditionalText, Compilation)>(GenerateCode));
+ WithExceptionReporting<(AdditionalText, Compilation)>(GenerateCode));
}
private static void GenerateCode(SourceProductionContext context, (
- SourceGeneratorHelpers.GlobalOptions Options,
AdditionalText OpenApiDocument,
Compilation Compilation) generatorContext)
{
- var globalOptions = generatorContext.Options;
var compilation = generatorContext.Compilation;
var rootNamespace = compilation.Assembly.Name;
@@ -68,9 +52,20 @@ private static void GenerateCode(SourceProductionContext context, (
jsonValidationExceptionGenerator.GenerateJsonValidationExceptionClass().AddTo(context);
var endpointGenerator = new OperationGenerator(compilation, jsonValidationExceptionGenerator);
- var openApi = OpenApiDocument.Load(openApiDocumentFile.AsStream(), "json").Document ??
+ var openApiResult = OpenApiDocument.Load(openApiDocumentFile.AsStream(), "json");
+ var openApiVersion = openApiResult.Diagnostic?.SpecificationVersion ??
+ throw new InvalidOperationException("Unknown openapi version");
+ if (openApiResult.Diagnostic.Errors.Any())
+ {
+ throw new InvalidOperationException(
+ openApiResult.Diagnostic.Errors.AggregateToString(
+ "Errors while parsing OpenAPI specification: ",
+ error => $"{(error.Pointer == null ? "" : $"{error.Pointer}: ")}{error.Message}"));
+ }
+ var openApi = openApiResult.Document ??
throw new InvalidOperationException(
$"Could not load OpenAPI document {openApiDocumentFile.Path}");
+
var openApiUri = new JsonReference(openApi.BaseUri.ToString());
var documentResolver = new PrepopulatedDocumentResolver();
@@ -79,19 +74,22 @@ private static void GenerateCode(SourceProductionContext context, (
{
throw new InvalidOperationException("Could not add OpenApi document");
}
- var generationContext = new SourceGeneratorHelpers.GenerationContext(documentResolver, globalOptions);
- var schemaGenerator = new SchemaGenerator(
- rootNamespace,
- context,
- generationContext);
+ var schemaGenerator = SchemaGenerator.For(
+ openApiVersion,
+ documentResolver,
+ rootNamespace,
+ context);
var openApiReference = new OpenApiReference(openApi, openApiDocument, openApiUri);
- var openApiVisitor = OpenApiVisitor.V2(openApiReference);
+ var openApiVisitor = OpenApiVisitor.V(openApiVersion, openApiReference);
- var httpRequestExtensionsGenerator = new HttpRequestExtensionsGenerator(rootNamespace);
+ var httpRequestExtensionsGenerator = new HttpRequestExtensionsGenerator(
+ openApiVersion,
+ rootNamespace);
httpRequestExtensionsGenerator.GenerateHttpRequestExtensionsClass().AddTo(context);
- var httpResponseExtensionsGenerator = new HttpResponseExtensionsGenerator(rootNamespace);
+ var httpResponseExtensionsGenerator = new HttpResponseExtensionsGenerator(rootNamespace,
+ openApiVersion);
httpResponseExtensionsGenerator.GenerateHttpResponseExtensionsClass().AddTo(context);
var apiConfigurationGenerator = new ApiConfigurationGenerator(rootNamespace);
@@ -111,8 +109,10 @@ private static void GenerateCode(SourceProductionContext context, (
{
var schemaReference = openApiPathVisitor.GetSchemaReference(parameter);
var typeDeclaration = schemaGenerator.Generate(schemaReference);
- pathParameterGenerators[$"{parameter.GetName()}_{parameter.GetLocation()}"] = new ParameterGenerator(typeDeclaration, parameter,
- httpRequestExtensionsGenerator);
+ pathParameterGenerators[$"{parameter.GetName()}_{parameter.GetLocation()}"] =
+ new ParameterGenerator(typeDeclaration,
+ parameter,
+ httpRequestExtensionsGenerator);
}
foreach (var openApiOperation in path.Value.GetOperations())
@@ -129,8 +129,10 @@ private static void GenerateCode(SourceProductionContext context, (
{
var schemaReference = openApiOperationVisitor.GetSchemaReference(parameter);
var typeDeclaration = schemaGenerator.Generate(schemaReference);
- operationParameterGenerators[$"{parameter.GetName()}_{parameter.GetLocation()}"] = new ParameterGenerator(typeDeclaration, parameter,
- httpRequestExtensionsGenerator);
+ operationParameterGenerators[$"{parameter.GetName()}_{parameter.GetLocation()}"] =
+ new ParameterGenerator(typeDeclaration,
+ parameter,
+ httpRequestExtensionsGenerator);
}
var body = operation.RequestBody;
@@ -169,10 +171,10 @@ private static void GenerateCode(SourceProductionContext context, (
var openApiResponseVisitor = openApiOperationVisitor.Visit(response);
var responseContent =
- // OpenAPI.NET is incorrectly adding content when there is none defined.
+ // OpenAPI.NET is incorrectly adding content where there is none defined.
// No content definition means NO content.
- (openApiResponseVisitor.HasContent() ? response.Content : null) ??
- new Dictionary();
+ response.Content?.Where(content =>
+ openApiResponseVisitor.HasContent(content.Value)) ?? [];
var responseBodyGenerators = responseContent.Select(valuePair =>
{
var content = valuePair.Value;
@@ -188,18 +190,18 @@ private static void GenerateCode(SourceProductionContext context, (
var responseHeaderSchema = openApiResponseVisitor.GetSchemaReference(header);
var typeDeclaration = schemaGenerator.Generate(responseHeaderSchema);
return new ResponseHeaderGenerator(name, header, typeDeclaration,
- httpResponseExtensionsGenerator);
+ openApiVersion);
}).ToList() ?? [];
return new ResponseContentGenerator(
responseStatusCodePattern,
responseBodyGenerators,
- responseHeaderGenerators,
- httpResponseExtensionsGenerator);
+ responseHeaderGenerators);
}).ToList();
var responseGenerator = new ResponseGenerator(
- responseBodyGenerators, httpResponseExtensionsGenerator);
+ responseBodyGenerators,
+ httpResponseExtensionsGenerator);
var responseSourceCode =
responseGenerator.GenerateResponseClass(
operationNamespace,
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/HttpRequestExtensionsGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/HttpRequestExtensionsGenerator.cs
index 510ee23..9b14792 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/HttpRequestExtensionsGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/HttpRequestExtensionsGenerator.cs
@@ -1,36 +1,34 @@
-namespace OpenAPI.WebApiGenerator.CodeGeneration;
+using Microsoft.OpenApi;
+using OpenAPI.WebApiGenerator.OpenApi;
+
+namespace OpenAPI.WebApiGenerator.CodeGeneration;
internal sealed class HttpRequestExtensionsGenerator(
+ OpenApiSpecVersion openApiVersion,
string @namespace)
{
private const string HttpRequestExtensionsClassName = "HttpRequestExtensions";
-
+
internal string CreateBindParameterInvocation(
string requestVariableName,
string bindingTypeName,
- string parameterSpecificationAsJson,
- bool isRequired)
- {
- return
- $""""
- {@namespace}.{HttpRequestExtensionsClassName}.Bind<{bindingTypeName}>(
- {requestVariableName},
- """
- {parameterSpecificationAsJson}
- """,
- {isRequired.ToString().ToLowerInvariant()})
- """";
- }
-
+ IOpenApiParameter parameter) =>
+ $""""
+ {@namespace}.{HttpRequestExtensionsClassName}.Bind<{bindingTypeName}>(
+ {requestVariableName},
+ """
+ {parameter.Serialize(openApiVersion)}
+ """)
+ """";
+
internal string CreateBindBodyInvocation(
string requestVariableName,
- string bindingTypeName,
- bool isRequired)
+ string bindingTypeName)
{
return
$"""
await {@namespace}.{HttpRequestExtensionsClassName}.BindBodyAsync<{bindingTypeName}>(
- {requestVariableName}, {isRequired.ToString().ToLowerInvariant()}, cancellationToken)
+ {requestVariableName}, cancellationToken)
.ConfigureAwait(false)
""";
}
@@ -40,44 +38,51 @@ internal SourceCode GenerateHttpRequestExtensionsClass() =>
$$$""""
#nullable enable
using System.Collections.Concurrent;
+ using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Corvus.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
- using OpenAPI.ParameterStyleParsers.OpenApi20;
- using OpenAPI.ParameterStyleParsers.OpenApi20.ParameterParsers;
+ using OpenAPI.ParameterStyleParsers;
namespace {{{@namespace}}};
internal static class {{{HttpRequestExtensionsClassName}}}
{
- private static readonly ConcurrentDictionary ParserCache = new();
+ private const string ParameterValueParserVersion = "{{{openApiVersion.GetParameterVersion()}}}";
+
+ private static readonly ConcurrentDictionary ParserCache = new();
+ private static IParameterValueParser GetParser(IParameter parameter) =>
+ ParserCache.GetOrAdd(parameter, _ =>
+ parameter.CreateParameterValueParser());
+
+ private static readonly ConcurrentDictionary ParameterCache = new();
+ private static IParameter GetParameter(string parameterSpecificationAsJson) =>
+ ParameterCache.GetOrAdd(parameterSpecificationAsJson, _ =>
+ ParameterFactory.OpenApi(ParameterValueParserVersion, parameterSpecificationAsJson));
///
/// Binds an http parameter to a json type
///
///
- ///
- ///
- ///
+ /// OpenAPI parameter specification formatted as json
+ /// The type to bind
+ /// The bound instance
///
internal static T Bind(this HttpRequest request,
- string parameterSpecificationAsJson,
- bool isRequired)
+ string parameterSpecificationAsJson)
where T : struct, IJsonValue
{
- var parameter = Parameter.FromOpenApi20ParameterSpecification(parameterSpecificationAsJson);
+ var parameter = GetParameter(parameterSpecificationAsJson);
return parameter switch
{
_ when parameter.InBody => T.Parse(request.BodyReader.AsStream()),
- _ when TryGetValue(request, parameter, out var stringValue) =>
- Parse(parameter, stringValue),
+ _ when TryParse(request, parameter, out var value) => value.Value,
_ => T.Undefined
};
}
- internal static async Task BindBodyAsync(this HttpRequest request,
- bool isRequired,
+ internal static async Task BindBodyAsync(this HttpRequest request,
CancellationToken cancellationToken)
where T : struct, IJsonValue
{
@@ -87,79 +92,90 @@ internal static async Task BindBodyAsync(this HttpRequest request,
return T.FromJson(document.RootElement.Clone());
}
-
- private static T Parse(Parameter parameter, string? stringValue)
- where T : struct, IJsonValue
- {
- var parser = ParserCache.GetOrAdd(parameter, ParameterValueParser.Create);
- if (!parser.TryParse(stringValue, out var instance, out var error))
- {
- throw new BadHttpRequestException(error);
- }
-
- return instance == null ? T.Null : T.Parse(instance.ToJsonString());
- }
-
- private static bool TryGetValue(this HttpRequest request, Parameter parameter, out string? stringValue) =>
+ private static bool TryParse(this HttpRequest request, IParameter parameter, [NotNullWhen(true)] out T? value)
+ where T : struct, IJsonValue =>
parameter switch
{
- _ when parameter.InHeader => TryGetHeaderValue(request.Headers, parameter, out stringValue),
- _ when parameter.InFormData => TryGetFormDataValue(request.Form, parameter, out stringValue),
- _ when parameter.InPath => TryGetPathValue(request.RouteValues, parameter, out stringValue),
- _ when parameter.InQuery => TryGetQueryValue(request.Query, parameter, out stringValue),
+ _ when parameter.InHeader => TryParseHeader(request.Headers, parameter, out value),
+ _ when parameter.InFormData => TryParseForm(request.Form, parameter, out value),
+ _ when parameter.InPath => TryParsePath(request.RouteValues, parameter, out value),
+ _ when parameter.InQuery => TryParseQuery(request.Query, parameter, out value),
_ => throw new InvalidOperationException($"Parameter {parameter.Name} has an unknown location")
};
- private static bool TryGetQueryValue(IQueryCollection query, Parameter parameter, out string? stringValue)
+ private static bool TryParseQuery(IQueryCollection query, IParameter parameter, [NotNullWhen(true)] out T? value)
+ where T : struct, IJsonValue
{
- stringValue = null;
+ value = null;
return query.TryGetValue(parameter.Name, out var values) &&
- TryGetValue(values, parameter, out stringValue);
+ TryParse(values, parameter, out value);
}
- private static bool TryGetPathValue(RouteValueDictionary requestPath, Parameter parameter, out string? stringValue)
+ private static bool TryParsePath(RouteValueDictionary requestPath, IParameter parameter, [NotNullWhen(true)] out T? value)
+ where T : struct, IJsonValue
{
- if (!requestPath.TryGetValue(parameter.Name, out var value))
+ if (!requestPath.TryGetValue(parameter.Name, out var objValue))
{
- stringValue = null;
+ value = default;
return false;
}
- stringValue = value switch
+ var stringValue = objValue switch
{
null => null,
string strValue => strValue,
_ => throw new InvalidOperationException(
- $"Route value of '{value}' with type '{value.GetType()}' is not supported")
+ $"Route value of '{objValue}' with type '{objValue.GetType()}' is not supported")
};
+
+ var parser = GetParser(parameter);
+ value = Parse(parser, stringValue);
return true;
}
- private static bool TryGetFormDataValue(IFormCollection requestForm, Parameter parameter, out string? stringValue)
+ private static bool TryParseForm(IFormCollection requestForm, IParameter parameter, [NotNullWhen(true)] out T? value)
+ where T : struct, IJsonValue
{
- stringValue = null;
- return requestForm.TryGetValue(parameter.Name, out var values) && TryGetValue(values, parameter, out stringValue);
+ value = default;
+ return requestForm.TryGetValue(parameter.Name, out var values) && TryParse(values, parameter, out value);
}
- private static bool TryGetHeaderValue(IHeaderDictionary headers, Parameter parameter, out string? stringValue)
+ private static bool TryParseHeader(IHeaderDictionary headers, IParameter parameter, [NotNullWhen(true)] out T? value)
+ where T : struct, IJsonValue
{
- stringValue = null;
+ value = default;
return headers.TryGetValue(parameter.Name, out var values) &&
- TryGetValue(values, parameter, out stringValue);
+ TryParse(values, parameter, out value);
}
- private static bool TryGetValue(StringValues values, Parameter parameter, out string? stringValue)
+ private static bool TryParse(StringValues values, IParameter parameter, [NotNullWhen(true)] out T? value)
+ where T : struct, IJsonValue
{
if (values.Count == 0)
{
- stringValue = null;
+ value = default;
return false;
}
- stringValue = parameter.ValueIncludesKey
- ? string.Join('&', values.Select(value => $"{parameter.Name}=${value}"))
+
+ var parser = GetParser(parameter);
+ var stringValue = parser.ValueIncludesParameterName
+ ? string.Join('&', values.Select(value => $"{parameter.Name}={value}"))
: values.Single();
+
+ value = Parse(parser, stringValue);
return true;
}
+
+ private static T Parse(IParameterValueParser parser, string? value)
+ where T : struct, IJsonValue
+ {
+ if (!parser.TryParse(value, out var instance, out var error))
+ {
+ throw new BadHttpRequestException(error);
+ }
+
+ return instance == null ? T.Null : T.Parse(instance.ToJsonString());
+ }
}
#nullable restore
"""");
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/HttpResponseExtensionsGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/HttpResponseExtensionsGenerator.cs
index ba484fc..3405be0 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/HttpResponseExtensionsGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/HttpResponseExtensionsGenerator.cs
@@ -1,42 +1,22 @@
-using OpenAPI.WebApiGenerator.Extensions;
+using Microsoft.OpenApi;
+using OpenAPI.WebApiGenerator.OpenApi;
namespace OpenAPI.WebApiGenerator.CodeGeneration;
internal sealed class HttpResponseExtensionsGenerator(
- string @namespace)
+ string @namespace,
+ OpenApiSpecVersion openApiSpecVersion)
{
private const string HttpResponseExtensionsClassName = "HttpResponseExtensions";
public string Namespace => @namespace;
-
- internal string CreateWriteHeaderInvocation(
- string responseVariableName,
- string headerSpecificationAsJson,
- string headerName,
- string headerValueVariableName,
- bool isRequired)
- {
- return
- $""""
- {responseVariableName}.WriteResponseHeader(
- """
- {headerSpecificationAsJson.Indent(4)}
- """,
- "{headerName}",
- {headerValueVariableName},
- {isRequired.ToString().ToLowerInvariant()})
- """";
- }
- internal string CreateWriteBodyInvocation(
+ internal static string CreateWriteBodyInvocation(
string responseVariableName,
- string contentVariableName)
- {
- return
- $"""
- {responseVariableName}.WriteResponseBody({contentVariableName})
- """;
- }
-
+ string contentVariableName) =>
+ $"""
+ {responseVariableName}.WriteResponseBody({contentVariableName})
+ """;
+
internal SourceCode GenerateHttpResponseExtensionsClass() =>
new($"{HttpResponseExtensionsClassName}.g.cs",
$$$""""
@@ -47,64 +27,40 @@ internal SourceCode GenerateHttpResponseExtensionsClass() =>
using Corvus.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
- using OpenAPI.ParameterStyleParsers.OpenApi20;
- using OpenAPI.ParameterStyleParsers.OpenApi20.ParameterParsers;
+ using OpenAPI.ParameterStyleParsers;
using JsonObject = System.Text.Json.Nodes.JsonObject;
namespace {{{@namespace}}};
internal static class {{{HttpResponseExtensionsClassName}}}
{
- private static readonly ConcurrentDictionary ParserCache = new();
-
- internal static void WriteResponseHeader(this HttpResponse response,
+ private static readonly ConcurrentDictionary ParserCache = new();
+ private const string ParameterValueParserVersion = "{{{openApiSpecVersion.GetParameterVersion()}}}";
+
+ internal static void WriteResponseHeader(this HttpResponse response,
string headerSpecificationAsJson,
string name,
- TValue value,
- bool isRequired)
+ TValue value)
where TValue : struct, IJsonValue
{
- if (!isRequired && value.IsUndefined())
+ if (value.IsUndefined())
{
return;
}
-
- Validate(value);
-
- var parameter = Parameter.FromOpenApi20ParameterSpecification(headerSpecificationAsJson);
- var serializedValue = Serialize(parameter, name, value);
+
+ var parser = ParserCache.GetOrAdd(headerSpecificationAsJson,
+ _ => ParameterValueParserFactory.OpenApi(ParameterValueParserVersion, headerSpecificationAsJson));
+ var jsonValue = value.Serialize();
+ var serializedValue = parser.Serialize(JsonNode.Parse(jsonValue));
response.Headers[name] = serializedValue;
}
internal static void WriteResponseBody(this HttpResponse response, TValue value)
where TValue : struct, IJsonValue
{
- Validate(value);
-
using var jsonWriter = new Utf8JsonWriter(response.BodyWriter);
value.WriteTo(jsonWriter);
}
-
- private static string? Serialize(Parameter parameter, string name, TValue jsonValue)
- where TValue : struct, IJsonValue
- {
- var parser = ParserCache.GetOrAdd(parameter, ParameterValueParser.Create);
- var value = jsonValue.Serialize();
-
- return parser.Serialize(JsonNode.Parse(value));
- }
-
- private static void Validate(T value)
- where T : struct, IJsonValue
- {
- var validationContext = ValidationContext.ValidContext;
- var validationLevel = ValidationLevel.Detailed;
- validationContext = value.Validate(validationContext, validationLevel);
- if (!validationContext.IsValid)
- {
- throw new JsonValidationException($"Object of type {typeof(T)} is not valid", validationContext.Results);
- }
- }
}
#nullable restore
"""");
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/OperationGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/OperationGenerator.cs
index 084203e..45d8561 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/OperationGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/OperationGenerator.cs
@@ -3,6 +3,7 @@
using System.Net.Http;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
+using OpenAPI.WebApiGenerator.Extensions;
namespace OpenAPI.WebApiGenerator.CodeGeneration;
@@ -27,11 +28,30 @@ internal partial class Operation
internal const string PathTemplate = "{{pathTemplate}}";
internal const string Method = "{{method.Method}}";
- {{HandleMethodSignature}};
+ ///
+ /// Set validation level for requests and responses
+ ///
+ internal ValidationLevel ValidationLevel { get; init; } = ValidationLevel.Detailed;
- private Func, Response> HandleValidationError { get; } = validationResult =>
+ ///
+ /// Should responses be validated?
+ /// If the response has already been validated, this can be disabled to avoid redundant validation.
+ ///
+ internal bool ValidateResponse { get; init; } = true;
+
+{{HandleMethodSignature.Indent(4)}};
+
+ ///
+ /// Set a custom delegate to handle request validation errors.
+ ///
+ ///
+ private Func, Response> HandleRequestValidationError { get; } = validationResult =>
{{jsonValidationExceptionGenerator.CreateThrowJsonValidationExceptionInvocation("Request is not valid", "validationResult")}};
+ ///
+ /// Handle a operation.
+ ///
+ ///
internal static async Task HandleAsync(
HttpContext context,
[FromServices] Operation operation,
@@ -41,16 +61,25 @@ internal static async Task HandleAsync(
var request = await Request.BindAsync(context, cancellationToken)
.ConfigureAwait(false);
- var validationContext = request.Validate(ValidationLevel.Detailed);
+ var validationContext = request.Validate(operation.ValidationLevel);
if (!validationContext.IsValid)
{
- operation.HandleValidationError(validationContext.Results.WithLocation(configuration.OpenApiSpecificationUri))
+ operation.HandleRequestValidationError(validationContext.Results.WithLocation(configuration.OpenApiSpecificationUri))
.WriteTo(context.Response);
return;
}
var response = await operation.HandleAsync(request, cancellationToken)
.ConfigureAwait(false);
+ if (operation.ValidateResponse)
+ {
+ validationContext = response.Validate(operation.ValidationLevel);
+ if (!validationContext.IsValid)
+ {
+ var validationResult = validationContext.Results.WithLocation(configuration.OpenApiSpecificationUri);
+ {{jsonValidationExceptionGenerator.CreateThrowJsonValidationExceptionInvocation("Response is not valid", "validationResult")}};
+ }
+ }
response.WriteTo(context.Response);
}
}
@@ -71,7 +100,12 @@ internal static async Task HandleAsync(
}
private const string HandleMethodSignature =
- "internal partial Task HandleAsync(Request request, CancellationToken cancellationToken)";
+ """
+ ///
+ /// Handles a request for this operation.
+ ///
+ internal partial Task HandleAsync(Request request, CancellationToken cancellationToken)
+ """;
private static bool HasImplementedHandleMethod(INamedTypeSymbol typeSymbol)
{
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/ParameterGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/ParameterGenerator.cs
index fbd757e..6bc3992 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/ParameterGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/ParameterGenerator.cs
@@ -1,5 +1,4 @@
-using System.IO;
-using Corvus.Json.CodeGeneration;
+using Corvus.Json.CodeGeneration;
using Corvus.Json.CodeGeneration.CSharp;
using Microsoft.OpenApi;
using OpenAPI.WebApiGenerator.Extensions;
@@ -20,30 +19,19 @@ internal sealed class ParameterGenerator(
internal string PropertyName { get; } = parameter.GetName().ToPascalCase();
internal bool IsParameterRequired { get; } = parameter.Required;
internal string Location { get; } = parameter.GetLocation();
+ internal string SchemaLocation { get; } = typeDeclaration.RelativeSchemaLocation;
- internal string GenerateRequestProperty()
- {
- return $$"""
- internal {{(IsParameterRequired ? "required " : "")}}{{FullyQualifiedTypeName}} {{PropertyName}} { get; init; }
- """;
- }
+ internal string GenerateRequestProperty() =>
+ $$"""
+ internal {{(IsParameterRequired ? "required " : "")}}{{FullyQualifiedTypeName}} {{PropertyName}} { get; init; }
+ """;
internal string AsRequired(string variableName) => $"{variableName}{(IsParameterRequired ? "" : $" ?? {FullyQualifiedTypeDeclarationIdentifier}.Undefined")}";
- internal string GenerateRequestBindingDirective(string requestVariableName)
- {
- using var textWriter = new StringWriter();
- var jsonWriter = new OpenApiJsonWriter(textWriter, new OpenApiJsonWriterSettings()
- {
- InlineLocalReferences = true
- });
- parameter.SerializeAsV2(jsonWriter);
- textWriter.Flush();
-
- return $"{PropertyName} = {httpRequestExtensionsGenerator.CreateBindParameterInvocation(
- requestVariableName,
- FullyQualifiedTypeDeclarationIdentifier,
- textWriter.GetStringBuilder().ToString(),
- IsParameterRequired).Indent(4).TrimStart()}{(IsParameterRequired ? "" : ".AsOptional()")},";
- }
+ internal string GenerateRequestBindingDirective(string requestVariableName) =>
+ $"{PropertyName} = {httpRequestExtensionsGenerator.CreateBindParameterInvocation(
+ requestVariableName,
+ FullyQualifiedTypeDeclarationIdentifier,
+ parameter)
+ .Indent(4).TrimStart()}{(IsParameterRequired ? "" : ".AsOptional()")},";
}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyContentGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyContentGenerator.cs
index 759e10a..9794196 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyContentGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyContentGenerator.cs
@@ -1,5 +1,4 @@
-using System.Linq;
-using Corvus.Json.CodeGeneration;
+using Corvus.Json.CodeGeneration;
using Corvus.Json.CodeGeneration.CSharp;
using OpenAPI.WebApiGenerator.Extensions;
@@ -18,21 +17,20 @@ internal sealed class RequestBodyContentGenerator(
internal string PropertyName { get; } = contentType.ToPascalCase();
internal string ContentType => contentType;
-
- internal string GenerateRequestBindingDirective(bool isRequired) =>
+
+ internal string SchemaLocation => typeDeclaration.RelativeSchemaLocation;
+ internal string GenerateRequestBindingDirective() =>
$"""
{PropertyName} =
({httpRequestExtensionsGenerator.CreateBindBodyInvocation(
- "request",
- FullyQualifiedTypeDeclarationIdentifier,
- isRequired).Indent(8).Trim()})
+ "request",
+ FullyQualifiedTypeDeclarationIdentifier)
+ .Indent(8).Trim()})
.AsOptional()
""";
- public string GenerateRequestProperty()
- {
- return $$"""
- internal {{FullyQualifiedTypeName}} {{PropertyName}} { get; private set; }
- """;
- }
+ public string GenerateRequestProperty() =>
+ $$"""
+ internal {{FullyQualifiedTypeName}} {{PropertyName}} { get; private set; }
+ """;
}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyGenerator.cs
index 76bc47b..40588a4 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyGenerator.cs
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
using Microsoft.OpenApi;
using OpenAPI.WebApiGenerator.Extensions;
@@ -33,15 +31,12 @@ public RequestBodyGenerator(
internal string GenerateRequestBindingDirective(string propertyName, string requestVariableName, out bool isAsync)
{
isAsync = _body is not null;
- if (_body is null)
- {
- return string.Empty;
- }
-
- return $"""
- {propertyName} = await RequestContent.BindAsync({requestVariableName}, cancellationToken)
- .ConfigureAwait(false)
- """;
+ return _body is null
+ ? string.Empty
+ : $"""
+ {propertyName} = await RequestContent.BindAsync({requestVariableName}, cancellationToken)
+ .ConfigureAwait(false)
+ """;
}
internal string GenerateValidateDirective(string propertyName, string validationContextVariableName, string validationLevelVariableName)
@@ -87,7 +82,7 @@ internal sealed class RequestContent
case "{{content.ContentType.ToLower()}}":
return new RequestContent
{
-{{content.GenerateRequestBindingDirective(_body.Required).Indent(20)}}
+{{content.GenerateRequestBindingDirective().Indent(20)}}
};
""")}}{{(_body.Required ? "" :
"""
@@ -105,7 +100,7 @@ internal ValidationContext Validate(ValidationContext validationContext, Validat
{{{_contentGenerators.AggregateToString(content =>
$"""
case true when {content.PropertyName} is not null:
- return {content.PropertyName}!.Value.Validate(validationContext, validationLevel);
+ return {content.PropertyName}!.Value.Validate("{content.SchemaLocation}", true, validationContext, validationLevel);
""")}}
default:
{{(_body.Required ?
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestGenerator.cs
index 1e68965..db72992 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/RequestGenerator.cs
@@ -61,24 +61,14 @@ internal partial class Request
internal ValidationContext Validate(ValidationLevel validationLevel)
{
- var validationContext = ValidationContext.ValidContext;{{
+ var validationContext = ValidationContext.ValidContext.UsingStack().UsingResults();{{
bodyGenerator.GenerateValidateDirective("Body", "validationContext", "validationLevel").Indent(8)
}}{{_parameterGeneratorsGroupedByLocation.AggregateToString(group =>
group.AggregateToString(generator =>
- $"validationContext = Validate({group.Key}.{generator.AsRequired(generator.PropertyName)}, {generator.IsParameterRequired.ToString().ToLowerInvariant()});").Trim()).Indent(8)}}
+ $"""
+ validationContext = ({group.Key}.{generator.AsRequired(generator.PropertyName)}).Validate("{generator.SchemaLocation}", {generator.IsParameterRequired.ToString().ToLowerInvariant()}, validationContext, validationLevel);
+ """).Trim()).Indent(8)}}
return validationContext;
-
- ValidationContext Validate(T value,
- bool isRequired)
- where T : struct, IJsonValue
- {
- if (!isRequired && value.IsUndefined())
- {
- return validationContext;
- }
-
- return value.Validate(validationContext, validationLevel);
- }
}{{_parameterGeneratorsGroupedByLocation.AggregateToString(group =>
$$"""
internal sealed class {{group.Key}}Parameters
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseBodyContentGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseBodyContentGenerator.cs
index cf7b11e..315a468 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseBodyContentGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseBodyContentGenerator.cs
@@ -8,6 +8,7 @@ internal sealed class ResponseBodyContentGenerator(string contentType, TypeDecla
{
private readonly string _contentVariableName = contentType.ToCamelCase();
public string ContentPropertyName { get; } = contentType.ToPascalCase();
+ internal string SchemaLocation => typeDeclaration.RelativeSchemaLocation;
public string GenerateConstructor(string className, string contentTypeFieldName) =>
$$"""
public {{className}}({{typeDeclaration.FullyQualifiedDotnetTypeName()}} {{_contentVariableName}})
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseContentGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseContentGenerator.cs
index f9864df..050ea30 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseContentGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseContentGenerator.cs
@@ -10,15 +10,12 @@ internal sealed class ResponseContentGenerator
{
private readonly List _contentGenerators = [];
private readonly List _headerGenerators = [];
- private readonly HttpResponseExtensionsGenerator _httpResponseExtensionsGenerator;
private readonly string _responseClassName;
private readonly string _responseStatusCodePattern;
private ResponseContentGenerator(
- string responseStatusCodePattern,
- HttpResponseExtensionsGenerator httpResponseExtensionsGenerator)
+ string responseStatusCodePattern)
{
- _httpResponseExtensionsGenerator = httpResponseExtensionsGenerator;
var classNamePrefix = Enum.TryParse(responseStatusCodePattern, out var statusCode)
? statusCode.ToString()
: responseStatusCodePattern.First() switch
@@ -39,8 +36,7 @@ var chr when char.IsDigit(chr) => "X",
public ResponseContentGenerator(
string responseStatusCodePattern,
List contentGenerators,
- List headerGenerators,
- HttpResponseExtensionsGenerator httpResponseExtensionsGenerator) : this(responseStatusCodePattern, httpResponseExtensionsGenerator)
+ List headerGenerators) : this(responseStatusCodePattern)
{
_contentGenerators = contentGenerators;
_headerGenerators = headerGenerators;
@@ -60,26 +56,26 @@ public string GenerateResponseContentClass()
var needsStatusCodeValidation = !hasExplicitStatusCode && !hasDefaultStatusCode;
return
-$$"""
-internal sealed class {{_responseClassName}} : Response
+$$$"""
+internal sealed class {{{_responseClassName}}} : Response
{
- private string? {{contentTypeFieldName}} = null;{{
+ private string? {{{contentTypeFieldName}}} = null;{{{
_contentGenerators.AggregateToString(generator =>
generator.GenerateConstructor(_responseClassName, contentTypeFieldName)).Indent(4)
- }}{{
+ }}}{{{
_contentGenerators.AggregateToString(generator =>
generator.GenerateContentProperty()).Indent(4)
- }}
+ }}}
- private int _statusCode{{(hasExplicitStatusCode ? $" = {_responseStatusCodePattern}" : string.Empty)}};
+ private int _statusCode{{{(hasExplicitStatusCode ? $" = {_responseStatusCodePattern}" : string.Empty)}}};
internal int StatusCode
{
- get => _statusCode;{{(hasExplicitStatusCode ? "" :
+ get => _statusCode;{{{(hasExplicitStatusCode ? "" :
$"""
init => _statusCode = {(needsStatusCodeValidation ? $"Validate{_responseStatusCodePattern.First()}xxStatusCode(value)" : "value")};
-""")}}
+""")}}}
}
-{{(anyHeaders ?
+{{{(anyHeaders ?
$$"""
internal {{headerRequiredDirective}}ResponseHeaders Headers { get; init; }{{defaultHeadersValueAssignment}}
@@ -90,16 +86,16 @@ internal sealed class ResponseHeaders
generator.GenerateProperty()).Indent(8)}}
}
-""" : "")}}
- internal override void WriteTo(HttpResponse {{responseVariableName}})
- {{{(_contentGenerators.Any() ?
+""" : "")}}}
+ internal override void WriteTo(HttpResponse {{{responseVariableName}}})
+ {{{{(_contentGenerators.Any() ?
$$"""
switch (true)
{{{_contentGenerators.AggregateToString(generator =>
$"""
case true when {generator.ContentPropertyName} is not null:
- {_httpResponseExtensionsGenerator.CreateWriteBodyInvocation(
+ {HttpResponseExtensionsGenerator.CreateWriteBodyInvocation(
responseVariableName,
$"{generator.ContentPropertyName}.Value")};
break;
@@ -108,11 +104,27 @@ internal override void WriteTo(HttpResponse {{responseVariableName}})
throw new InvalidOperationException("No content was defined");
}
-""" : "")}}
- {{responseVariableName}}.ContentType = {{contentTypeFieldName}};
- {{responseVariableName}}.StatusCode = StatusCode;{{
+""" : "")}}}
+ {{{responseVariableName}}}.ContentType = {{{contentTypeFieldName}}};
+ {{{responseVariableName}}}.StatusCode = StatusCode;{{{
_headerGenerators.AggregateToString(generator =>
- generator.GenerateWriteDirective(responseVariableName)).Indent(8)}}
+ generator.GenerateWriteDirective(responseVariableName)).Indent(8)}}}
+ }
+
+ internal override ValidationContext Validate(ValidationLevel validationLevel)
+ {
+ var validationContext = ValidationContext.ValidContext.UsingStack().UsingResults();
+ validationContext = true switch
+ {{{{_contentGenerators.AggregateToString(generator =>
+$"""
+ true when {generator.ContentPropertyName} is not null =>
+ {generator.ContentPropertyName}.Value.Validate("{generator.SchemaLocation}", true, validationContext, validationLevel),
+""")}}}
+ _ => validationContext
+ };
+ {{{_headerGenerators.AggregateToString(generator =>
+ generator.GenerateValidateDirective()).Indent(8)}}}
+ return validationContext;
}
}
""";
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseGenerator.cs
index c1bf868..eb4beae 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseGenerator.cs
@@ -4,7 +4,9 @@
namespace OpenAPI.WebApiGenerator.CodeGeneration;
-internal sealed class ResponseGenerator(List responseBodyGenerators, HttpResponseExtensionsGenerator httpResponseExtensionsGenerator)
+internal sealed class ResponseGenerator(
+ List responseBodyGenerators,
+ HttpResponseExtensionsGenerator httpResponseExtensionsGenerator)
{
public SourceCode GenerateResponseClass(string @namespace, string path)
{
@@ -25,6 +27,7 @@ internal abstract partial class Response
""")}}
internal abstract void WriteTo(HttpResponse httpResponse);
+ internal abstract ValidationContext Validate(ValidationLevel validationLevel);
{{
responseBodyGenerators.AggregateToString(generator =>
generator.GenerateResponseContentClass()).Indent(4)
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseHeaderGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseHeaderGenerator.cs
index e03728b..59365da 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseHeaderGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseHeaderGenerator.cs
@@ -1,9 +1,8 @@
-using System.IO;
-using System.Linq;
-using Corvus.Json.CodeGeneration;
+using Corvus.Json.CodeGeneration;
using Corvus.Json.CodeGeneration.CSharp;
using Microsoft.OpenApi;
using OpenAPI.WebApiGenerator.Extensions;
+using OpenAPI.WebApiGenerator.OpenApi;
namespace OpenAPI.WebApiGenerator.CodeGeneration;
@@ -11,10 +10,10 @@ internal sealed class ResponseHeaderGenerator(
string name,
IOpenApiHeader header,
TypeDeclaration typeDeclaration,
- HttpResponseExtensionsGenerator httpResponseExtensionsGenerator)
+ OpenApiSpecVersion openApiSpecVersion)
{
private readonly string _propertyName = name.ToPascalCase();
- private readonly string _requiredDirective = header.Required ? "required" : string.Empty;
+ private readonly string _requiredDirective = header.Required ? "required " : string.Empty;
private string DefaultValueAssignment => header.Required ? "" : $" = {FullyQualifiedTypeName}.Undefined;";
private string FullyQualifiedTypeName =>
$"{_fullyQualifiedTypeDeclarationIdentifier}";
@@ -24,33 +23,33 @@ internal sealed class ResponseHeaderGenerator(
internal string GenerateProperty() =>
$$"""
- internal {{_requiredDirective}} {{FullyQualifiedTypeName}} {{_propertyName}} { get; init; }{{DefaultValueAssignment}}
+ internal {{_requiredDirective}}{{FullyQualifiedTypeName}} {{_propertyName}} { get; init; }{{DefaultValueAssignment}}
""";
internal string GenerateWriteDirective(string responseVariableName)
{
- using var textWriter = new StringWriter();
- var jsonWriter = new OpenApiJsonWriter(textWriter, new OpenApiJsonWriterSettings
- {
- InlineLocalReferences = true
- });
- header.SerializeAsV2(jsonWriter);
- textWriter.Flush();
-
// Response header specification is a subset of the parameter specification, so we add the missing properties to be able to use the parameter value parser
var headerSpecificationAsJson =
$$"""
{
"name": "{{name}}",
"in": "header",
- {{textWriter.GetStringBuilder().ToString().TrimStart('{').TrimStart()}}
+ {{header.Serialize(openApiSpecVersion).ToString().TrimStart('{').TrimStart()}}
""";
-
- return $"{httpResponseExtensionsGenerator.CreateWriteHeaderInvocation(
- responseVariableName,
- headerSpecificationAsJson,
- name,
- $"Headers.{_propertyName}",
- header.Required)};";
+
+ return
+ $""""
+ {responseVariableName}.WriteResponseHeader(
+ """
+ {headerSpecificationAsJson.Indent(4).TrimStart()}
+ """,
+ "{name}",
+ Headers.{_propertyName});
+ """";
}
+
+ internal string GenerateValidateDirective() =>
+ $"""
+ validationContext = Headers.{_propertyName}.Validate("{typeDeclaration.RelativeSchemaLocation}", {header.Required.ToString().ToLowerInvariant()}, validationContext, validationLevel);
+ """;
}
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/SchemaGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/SchemaGenerator.cs
index 04e3e08..ce2556e 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/SchemaGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/SchemaGenerator.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.IO;
using System.Linq;
using Corvus.Json;
@@ -7,12 +8,14 @@
using Corvus.Json.CodeGeneration.CSharp;
using Corvus.Json.SourceGeneratorTools;
using Microsoft.CodeAnalysis;
+using Microsoft.OpenApi;
using OpenAPI.WebApiGenerator.OpenApi;
using JsonPointer = OpenAPI.WebApiGenerator.Json.JsonPointer;
namespace OpenAPI.WebApiGenerator.CodeGeneration;
-internal sealed class SchemaGenerator(string rootNamespace,
+internal sealed class SchemaGenerator(
+ string rootNamespace,
SourceProductionContext context,
SourceGeneratorHelpers.GenerationContext generationContext)
{
@@ -20,6 +23,33 @@ internal sealed class SchemaGenerator(string rootNamespace,
private static readonly VocabularyRegistry VocabularyRegistry = SourceGeneratorHelpers.CreateVocabularyRegistry(MetaSchemaResolver);
private readonly Dictionary _typeCache = new();
private readonly HashSet _fileCache = [];
+
+ internal static SchemaGenerator For(
+ OpenApiSpecVersion openApiSpecVersion,
+ IDocumentResolver documentResolver,
+ string rootNamespace,
+ SourceProductionContext context)
+ {
+ var vocabulary = openApiSpecVersion switch
+ {
+ OpenApiSpecVersion.OpenApi2_0 =>
+ Corvus.Json.CodeGeneration.Draft4.VocabularyAnalyser.DefaultVocabulary,
+ OpenApiSpecVersion.OpenApi3_0 =>
+ Corvus.Json.CodeGeneration.OpenApi30.VocabularyAnalyser.DefaultVocabulary,
+ OpenApiSpecVersion.OpenApi3_1 =>
+ Corvus.Json.CodeGeneration.Draft202012.VocabularyAnalyser.DefaultVocabulary,
+ _ => throw new InvalidOperationException($"OpenAPI specification {openApiSpecVersion} is not supported")
+ };
+ var globalOptions =
+ new SourceGeneratorHelpers.GlobalOptions(
+ fallbackVocabulary: vocabulary,
+ optionalAsNullable: true,
+ useOptionalNameHeuristics: true,
+ alwaysAssertFormat: true,
+ ImmutableArray.Empty);
+ var generationContext = new SourceGeneratorHelpers.GenerationContext(documentResolver, globalOptions);
+ return new SchemaGenerator(rootNamespace, context, generationContext);
+ }
internal TypeDeclaration Generate(JsonReference reference)
{
@@ -72,24 +102,9 @@ private List GenerateCode(SourceGeneratorHelpers.TypesToGenerat
return [];
}
- string schemaFile = spec.Location;
+ var schemaFile = spec.Location;
JsonReference reference = new(schemaFile);
- TypeDeclaration rootType;
- try
- {
- rootType = typeBuilder.AddTypeDeclarations(reference, typesToGenerate.FallbackVocabulary, spec.RebaseToRootPath, context.CancellationToken);
- }
- catch (Exception ex)
- {
- context.ReportDiagnostic(
- Diagnostic.Create(
- Crv1001ErrorGeneratingCSharpCode,
- Location.None,
- reference,
- ex.Message));
-
- return [];
- }
+ var rootType = typeBuilder.AddTypeDeclarations(reference, typesToGenerate.FallbackVocabulary, spec.RebaseToRootPath, context.CancellationToken);
typeDeclarationsToGenerate.Add(rootType);
@@ -134,27 +149,11 @@ private List GenerateCode(SourceGeneratorHelpers.TypesToGenerat
var languageProvider = CSharpLanguageProvider.DefaultWithOptions(options);
- IReadOnlyCollection generatedCode;
-
- try
- {
- generatedCode =
- typeBuilder.GenerateCodeUsing(
- languageProvider,
- context.CancellationToken,
- typeDeclarationsToGenerate);
- }
- catch (Exception ex)
- {
- context.ReportDiagnostic(
- Diagnostic.Create(
- Crv1001ErrorGeneratingCSharpCode,
- Location.None,
- ex.Message));
-
- return [];
- }
-
+ var generatedCode = typeBuilder.GenerateCodeUsing(
+ languageProvider,
+ context.CancellationToken,
+ typeDeclarationsToGenerate);
+
foreach (var codeFile in generatedCode)
{
context.CancellationToken.ThrowIfCancellationRequested();
@@ -179,14 +178,4 @@ private List GenerateCode(SourceGeneratorHelpers.TypesToGenerat
.Select(declaration => declaration.ReducedTypeDeclaration().ReducedType)
.ToList();
}
-
- private static readonly DiagnosticDescriptor Crv1001ErrorGeneratingCSharpCode =
- new(
- id: "CRV1001",
- title: "JSON Schema Type Generator Error",
- messageFormat: "Error generating C# code: {0}: {1}",
- category: "JsonSchemaCodeGenerator",
- DiagnosticSeverity.Error,
- isEnabledByDefault: true);
-
}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/CodeGeneration/ValidationExtensionsGenerator.cs b/src/OpenAPI.WebApiGenerator/CodeGeneration/ValidationExtensionsGenerator.cs
index 3a060cc..3a6871a 100644
--- a/src/OpenAPI.WebApiGenerator/CodeGeneration/ValidationExtensionsGenerator.cs
+++ b/src/OpenAPI.WebApiGenerator/CodeGeneration/ValidationExtensionsGenerator.cs
@@ -2,7 +2,7 @@
internal sealed class ValidationExtensionsGenerator(string @namespace)
{
- private const string ClassName = "ValidationResultsExtensions";
+ private const string ClassName = "ValidationExtensions";
internal SourceCode GenerateClass() => new($"{ClassName}.g.cs",
$$"""
#nullable enable
@@ -35,6 +35,23 @@ private static (JsonReference ValidationLocation, JsonReference SchemaLocation,
var schemaLocation = new JsonReference(uri.AsSpan(), location.Value.SchemaLocation.Fragment);
return (location.Value.ValidationLocation, schemaLocation, location.Value.DocumentLocation);
}
+
+ internal static ValidationContext Validate(this T value,
+ string schemaLocation,
+ bool isRequired,
+ ValidationContext validationContext,
+ ValidationLevel validationLevel)
+ where T : struct, IJsonValue
+ {
+ if (!isRequired && value.IsUndefined())
+ {
+ return validationContext;
+ }
+
+ var context = validationContext.PushSchemaLocation(schemaLocation);
+ context = value.Validate(context, validationLevel);
+ return context.PopLocation();
+ }
}
#nullable restore
""");
diff --git a/src/OpenAPI.WebApiGenerator/Extensions/EnumerableExtensions.cs b/src/OpenAPI.WebApiGenerator/Extensions/EnumerableExtensions.cs
index 403c536..eb0a13d 100644
--- a/src/OpenAPI.WebApiGenerator/Extensions/EnumerableExtensions.cs
+++ b/src/OpenAPI.WebApiGenerator/Extensions/EnumerableExtensions.cs
@@ -8,8 +8,12 @@ namespace OpenAPI.WebApiGenerator.Extensions;
internal static class EnumerableExtensions
{
internal static string AggregateToString(this IEnumerable items, Func convert) =>
+ items.AggregateToString(new StringBuilder().AppendLine(), convert);
+ internal static string AggregateToString(this IEnumerable items, string firstLine, Func convert) =>
+ items.AggregateToString(new StringBuilder(firstLine), convert);
+ private static string AggregateToString(this IEnumerable items, StringBuilder stringBuilder, Func convert) =>
items
- .Aggregate(new StringBuilder().AppendLine(), (builder, item) =>
+ .Aggregate(stringBuilder, (builder, item) =>
builder.AppendLine(convert(item)))
.ToString()
.TrimEnd();
diff --git a/src/OpenAPI.WebApiGenerator/Extensions/OpenApiSchemaExtensions.cs b/src/OpenAPI.WebApiGenerator/Extensions/OpenApiSchemaExtensions.cs
deleted file mode 100644
index 7200644..0000000
--- a/src/OpenAPI.WebApiGenerator/Extensions/OpenApiSchemaExtensions.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.IO;
-using Microsoft.OpenApi;
-
-namespace OpenAPI.WebApiGenerator.Extensions;
-
-internal static class OpenApiSchemaExtensions
-{
- internal static string SerializeToJson(this IOpenApiSchema? schema)
- {
- if (schema is null)
- return "{}";
-
- using var schemaWriter = new StringWriter();
- var openApiSchemaWriter = new OpenApiJsonWriter(schemaWriter, new OpenApiWriterSettings
- {
- InlineLocalReferences = true
- });
- schema.SerializeAsV2(openApiSchemaWriter);
- return schemaWriter.ToString();
- }
-}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/Json/JsonPointer.cs b/src/OpenAPI.WebApiGenerator/Json/JsonPointer.cs
index e45459d..5720f46 100644
--- a/src/OpenAPI.WebApiGenerator/Json/JsonPointer.cs
+++ b/src/OpenAPI.WebApiGenerator/Json/JsonPointer.cs
@@ -20,9 +20,9 @@ internal static JsonPointer ParseFrom(string pointer)
internal string[] Segments => segments ?? [];
- internal JsonPointer Append(string segment)
+ internal JsonPointer Append(params string[] segmentList)
{
- return new JsonPointer(Segments.Append(segment).ToArray());
+ return new JsonPointer(Segments.Concat(segmentList).ToArray());
}
public override string ToString() =>
diff --git a/src/OpenAPI.WebApiGenerator/OpenAPI.WebApiGenerator.csproj b/src/OpenAPI.WebApiGenerator/OpenAPI.WebApiGenerator.csproj
index cdd17d0..edda743 100644
--- a/src/OpenAPI.WebApiGenerator/OpenAPI.WebApiGenerator.csproj
+++ b/src/OpenAPI.WebApiGenerator/OpenAPI.WebApiGenerator.csproj
@@ -52,7 +52,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/OpenApiVersionExtensions.cs b/src/OpenAPI.WebApiGenerator/OpenApi/OpenApiVersionExtensions.cs
new file mode 100644
index 0000000..3e7aba3
--- /dev/null
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/OpenApiVersionExtensions.cs
@@ -0,0 +1,39 @@
+using System;
+using System.IO;
+using System.Text;
+using Microsoft.OpenApi;
+
+namespace OpenAPI.WebApiGenerator.OpenApi;
+
+internal static class OpenApiVersionExtensions
+{
+ internal static string GetParameterVersion(this OpenApiSpecVersion version) => version switch
+ {
+ OpenApiSpecVersion.OpenApi2_0 => "2.0",
+ OpenApiSpecVersion.OpenApi3_0 => "3.0",
+ OpenApiSpecVersion.OpenApi3_1 => "3.1",
+ _ => throw new NotSupportedException($"OpenAPI version {Enum.GetName(typeof(OpenApiSpecVersion), version)} not supported")
+ };
+
+ internal static Action GetSerializer(this IOpenApiSerializable parameter, OpenApiSpecVersion version) => version switch
+ {
+ OpenApiSpecVersion.OpenApi3_1 => parameter.SerializeAsV31,
+ OpenApiSpecVersion.OpenApi3_0 => parameter.SerializeAsV3,
+ OpenApiSpecVersion.OpenApi2_0 => parameter.SerializeAsV2,
+ _ => throw new NotSupportedException(
+ $"OpenAPI version {Enum.GetName(typeof(OpenApiSpecVersion), version)} not supported")
+ };
+
+ internal static StringBuilder Serialize(this IOpenApiSerializable serializable, OpenApiSpecVersion version)
+ {
+ using var textWriter = new StringWriter();
+ var jsonWriter = new OpenApiJsonWriter(textWriter, new OpenApiJsonWriterSettings
+ {
+ InlineLocalReferences = true
+ });
+ var serialize = serializable.GetSerializer(version);
+ serialize(jsonWriter);
+ textWriter.Flush();
+ return textWriter.GetStringBuilder();
+ }
+}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/IOpenApiPathItemVisitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/IOpenApiPathItemVisitor.cs
index 4c3bbcb..081450b 100644
--- a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/IOpenApiPathItemVisitor.cs
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/IOpenApiPathItemVisitor.cs
@@ -6,6 +6,7 @@ namespace OpenAPI.WebApiGenerator.OpenApi.Visitor;
internal interface IOpenApiPathItemVisitor : IVisitor
{
- public JsonReference GetSchemaReference(IOpenApiParameter parameter);
+ JsonReference GetSchemaReference(IOpenApiParameter parameter);
IOpenApiOperationVisitor Visit(HttpMethod parameter);
+
}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/IOpenApiResponseVisitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/IOpenApiResponseVisitor.cs
index 2b8ba0e..b37f090 100644
--- a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/IOpenApiResponseVisitor.cs
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/IOpenApiResponseVisitor.cs
@@ -6,6 +6,6 @@ namespace OpenAPI.WebApiGenerator.OpenApi.Visitor;
internal interface IOpenApiResponseVisitor
{
public JsonReference GetSchemaReference(OpenApiMediaType mediaType);
- public bool HasContent();
+ public bool HasContent(OpenApiMediaType mediaType);
public JsonReference GetSchemaReference(IOpenApiHeader header);
}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/OpenApiVisitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/OpenApiVisitor.cs
index 286d412..d9e2c6e 100644
--- a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/OpenApiVisitor.cs
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/OpenApiVisitor.cs
@@ -11,10 +11,14 @@ namespace OpenAPI.WebApiGenerator.OpenApi.Visitor;
internal abstract class OpenApiVisitor
{
- public static IOpenApiVisitor V3(OpenApiReference openApiReference) =>
- OpenApiV3Visitor.Visit(openApiReference);
- public static IOpenApiVisitor V2(OpenApiReference openApiReference) =>
- OpenApiV2Visitor.Visit(openApiReference);
+ public static IOpenApiVisitor V(OpenApiSpecVersion version, OpenApiReference openApiReference) =>
+ version switch
+ {
+ OpenApiSpecVersion.OpenApi2_0 => OpenApiV2Visitor.Visit(openApiReference),
+ OpenApiSpecVersion.OpenApi3_0 => OpenApiV3Visitor.Visit(openApiReference),
+ OpenApiSpecVersion.OpenApi3_1 => OpenApiV3Visitor.Visit(openApiReference),
+ _ => throw new InvalidOperationException($"OpenAPI version {version} not supported")
+ };
}
internal abstract class OpenApiVisitor(
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.OperationVisitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.OperationVisitor.cs
new file mode 100644
index 0000000..8eb8a40
--- /dev/null
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.OperationVisitor.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using Corvus.Json;
+using Microsoft.OpenApi;
+
+namespace OpenAPI.WebApiGenerator.OpenApi.Visitor.V2;
+
+internal sealed partial class OpenApiV2Visitor
+{
+ private sealed partial class PathItemVisitor
+ {
+ private sealed class OperationVisitor :
+ OpenApiVisitor, IOpenApiOperationVisitor
+ {
+ private Dictionary _parameterSchemaReferences = new();
+ private JsonReference? _bodySchemaReference;
+ private readonly Dictionary _responseVisitors = new();
+
+ private OperationVisitor(OpenApiReference openApiReference) : base(openApiReference)
+ {
+ VisitParameters();
+ VisitResponses();
+ }
+
+ private void VisitParameters()
+ {
+ if (OpenApiDocument.Parameters == null)
+ {
+ return;
+ }
+ var parametersPointer = Visit("parameters");
+ var parametersVisitor = ParametersVisitor.Visit(
+ new OpenApiReference>(
+ OpenApiDocument.Parameters,
+ Document,
+ new JsonReference(Reference.Uri, parametersPointer.ToString().AsSpan())));
+ _parameterSchemaReferences = parametersVisitor.Schemas;
+ _bodySchemaReference = parametersVisitor.BodySchema;
+ }
+
+ private void VisitResponses()
+ {
+ foreach (var response in OpenApiDocument.Responses ?? [])
+ {
+ var responsePointer = Visit("responses", response.Key);
+ var responseReference = new JsonReference(Reference.Uri, responsePointer.ToString().AsSpan());
+ var responseVisitor =
+ ResponseVisitor.Visit(
+ new OpenApiReference(response.Value, Document, responseReference));
+ _responseVisitors.Add(response.Value, responseVisitor);
+ }
+ }
+
+ internal static OperationVisitor Visit(
+ OpenApiReference openApiReference) =>
+ new(openApiReference);
+
+ public JsonReference GetSchemaReference(IOpenApiParameter parameter) =>
+ _parameterSchemaReferences[parameter];
+
+ public JsonReference GetSchemaReference(OpenApiMediaType requestBodyContent) =>
+ _bodySchemaReference ?? throw new InvalidOperationException("Operation doesn't define a body");
+
+ public IOpenApiResponseVisitor Visit(IOpenApiResponse response) =>
+ _responseVisitors[response];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.PathItemVisitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.PathItemVisitor.cs
index 2e59f0e..d809510 100644
--- a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.PathItemVisitor.cs
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.PathItemVisitor.cs
@@ -8,7 +8,7 @@ namespace OpenAPI.WebApiGenerator.OpenApi.Visitor.V2;
internal sealed partial class OpenApiV2Visitor
{
- private sealed class PathItemVisitor :
+ private sealed partial class PathItemVisitor :
OpenApiVisitor, IOpenApiPathItemVisitor
{
private Dictionary _parameterSchemaReferences = new();
@@ -58,61 +58,5 @@ public JsonReference GetSchemaReference(IOpenApiParameter parameter) =>
public IOpenApiOperationVisitor Visit(HttpMethod httpMethod) =>
_operations[httpMethod];
-
- private sealed class OperationVisitor :
- OpenApiVisitor, IOpenApiOperationVisitor
- {
- private Dictionary _parameterSchemaReferences = new();
- private JsonReference? _bodySchemaReference;
- private readonly Dictionary _responseVisitors = new();
-
- private OperationVisitor(OpenApiReference openApiReference) : base(openApiReference)
- {
- VisitParameters();
- VisitResponses();
- }
-
- private void VisitParameters()
- {
- if (OpenApiDocument.Parameters == null)
- {
- return;
- }
- var parametersPointer = Visit("parameters");
- var parametersVisitor = ParametersVisitor.Visit(
- new OpenApiReference>(
- OpenApiDocument.Parameters,
- Document,
- new JsonReference(Reference.Uri, parametersPointer.ToString().AsSpan())));
- _parameterSchemaReferences = parametersVisitor.Schemas;
- _bodySchemaReference = parametersVisitor.BodySchema;
- }
-
- private void VisitResponses()
- {
- foreach (var response in OpenApiDocument.Responses ?? [])
- {
- var responsePointer = Visit("responses", response.Key);
- var responseReference = new JsonReference(Reference.Uri, responsePointer.ToString().AsSpan());
- var responseVisitor =
- ResponseVisitor.Visit(
- new OpenApiReference(response.Value, Document, responseReference));
- _responseVisitors.Add(response.Value, responseVisitor);
- }
- }
-
- internal static OperationVisitor Visit(
- OpenApiReference openApiReference) =>
- new(openApiReference);
-
- public JsonReference GetSchemaReference(IOpenApiParameter parameter) =>
- _parameterSchemaReferences[parameter];
-
- public JsonReference GetSchemaReference(OpenApiMediaType requestBodyContent) =>
- _bodySchemaReference ?? throw new InvalidOperationException("Operation doesn't define a body");
-
- public IOpenApiResponseVisitor Visit(IOpenApiResponse response) =>
- _responseVisitors[response];
- }
}
}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.ResponseVisitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.ResponseVisitor.cs
index f599624..ab6c8fc 100644
--- a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.ResponseVisitor.cs
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V2/OpenApiV2Visitor.ResponseVisitor.cs
@@ -47,7 +47,8 @@ private void VisitHeaders()
public JsonReference GetSchemaReference(OpenApiMediaType mediaType) =>
_contentSchemaReference ?? throw new InvalidOperationException("Response has no content defined");
- public bool HasContent() => _contentSchemaReference != null;
+ public bool HasContent(OpenApiMediaType mediaType) =>
+ _contentSchemaReference != null;
public JsonReference GetSchemaReference(IOpenApiHeader header) =>
_headerReferences[header];
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.OperationVisitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.OperationVisitor.cs
new file mode 100644
index 0000000..a186e11
--- /dev/null
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.OperationVisitor.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using Corvus.Json;
+using Microsoft.OpenApi;
+
+namespace OpenAPI.WebApiGenerator.OpenApi.Visitor.V3;
+
+internal sealed partial class OpenApiV3Visitor
+{
+ private sealed partial class PathItemVisitor
+ {
+ private sealed class OperationVisitor :
+ OpenApiVisitor, IOpenApiOperationVisitor
+ {
+ private Dictionary _parameterSchemaReferences = new();
+ private readonly Dictionary _responseVisitors = new();
+ private readonly Dictionary _requestContentSchemaReferences = new();
+
+ private OperationVisitor(OpenApiReference openApiReference) : base(openApiReference)
+ {
+ VisitParameters();
+ VisitResponses();
+ VisitRequestBody();
+ }
+
+ private void VisitParameters()
+ {
+ if (OpenApiDocument.Parameters == null)
+ {
+ return;
+ }
+ var parametersPointer = Visit("parameters");
+ var parametersVisitor = ParametersVisitor.Visit(
+ new OpenApiReference>(
+ OpenApiDocument.Parameters,
+ Document,
+ new JsonReference(Reference.Uri, parametersPointer.ToString().AsSpan())));
+ _parameterSchemaReferences = parametersVisitor.Schemas;
+ }
+
+ private void VisitResponses()
+ {
+ foreach (var response in OpenApiDocument.Responses ?? [])
+ {
+ var responsePointer = Visit("responses", response.Key);
+ var responseReference = new JsonReference(Reference.Uri, responsePointer.ToString().AsSpan());
+ var responseVisitor =
+ ResponseVisitor.Visit(
+ new OpenApiReference(response.Value, Document, responseReference));
+ _responseVisitors.Add(response.Value, responseVisitor);
+ }
+ }
+
+ private void VisitRequestBody()
+ {
+ if (OpenApiDocument.RequestBody?.Content == null)
+ {
+ return;
+ }
+
+ var requestContentPointer = Visit("requestBody", "content");
+ foreach (var content in OpenApiDocument.RequestBody.Content)
+ {
+ _requestContentSchemaReferences.Add(content.Value,
+ new JsonReference(Reference.Uri,
+ requestContentPointer
+ .Append(content.Key)
+ .Append("schema")
+ .ToString()
+ .AsSpan()));
+ }
+ }
+
+ internal static OperationVisitor Visit(
+ OpenApiReference openApiReference) =>
+ new(openApiReference);
+
+ public JsonReference GetSchemaReference(IOpenApiParameter parameter) =>
+ _parameterSchemaReferences[parameter];
+
+ public JsonReference GetSchemaReference(OpenApiMediaType requestBodyContent) =>
+ _requestContentSchemaReferences[requestBodyContent];
+
+ public IOpenApiResponseVisitor Visit(IOpenApiResponse response) =>
+ _responseVisitors[response];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.ParametersVisitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.ParametersVisitor.cs
new file mode 100644
index 0000000..06cbdc8
--- /dev/null
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.ParametersVisitor.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Corvus.Json;
+using Microsoft.OpenApi;
+
+namespace OpenAPI.WebApiGenerator.OpenApi.Visitor.V3;
+
+internal sealed partial class OpenApiV3Visitor
+{
+ private sealed class ParametersVisitor :
+ OpenApiVisitor>
+ {
+ private ParametersVisitor(OpenApiReference> openApiReference) : base(openApiReference)
+ {
+ VisitParameters();
+ }
+
+ internal Dictionary Schemas { get; } = new();
+
+ internal static ParametersVisitor Visit(OpenApiReference> openApiReference) =>
+ new(openApiReference);
+
+ private void VisitParameters()
+ {
+ var parameterIndex = 0;
+ while (TryVisit([parameterIndex.ToString()], out var parameterPointer))
+ {
+ var parameterNameElement = JsonPointerUtilities.ResolvePointer(
+ Document,
+ parameterPointer.Append("name").ToString().AsSpan());
+ var parameterName = parameterNameElement.GetString() ??
+ throw new InvalidOperationException("parameter doesn't have a name");
+ var parameterLocationElement = JsonPointerUtilities.ResolvePointer(
+ Document,
+ parameterPointer.Append("in").ToString().AsSpan());
+ var parameterLocation = parameterLocationElement.GetString() ??
+ throw new InvalidOperationException("parameter doesn't have a location");
+
+ var parameter = OpenApiDocument.Single(apiParameter =>
+ apiParameter.GetName() == parameterName &&
+ apiParameter.GetLocation() == parameterLocation);
+
+ if (!TryVisit([parameterIndex.ToString(), "schema"], out var schemaPointer))
+ {
+ schemaPointer = Visit(
+ "content",
+ parameter.Content?.Single().Key ??
+ throw new InvalidOperationException("Parameter doesn't contain a schema"),
+ "schema");
+ }
+
+ Schemas.Add(parameter,
+ new JsonReference(Reference.Uri, schemaPointer.ToString().AsSpan()));
+
+ parameterIndex++;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.PathItemVisitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.PathItemVisitor.cs
new file mode 100644
index 0000000..906fc4f
--- /dev/null
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.PathItemVisitor.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using Corvus.Json;
+using Microsoft.OpenApi;
+
+namespace OpenAPI.WebApiGenerator.OpenApi.Visitor.V3;
+
+internal sealed partial class OpenApiV3Visitor
+{
+ private sealed partial class PathItemVisitor :
+ OpenApiVisitor, IOpenApiPathItemVisitor
+ {
+ private Dictionary _parameterSchemaReferences = new();
+ private readonly Dictionary _operations = new();
+
+ private PathItemVisitor(OpenApiReference openApiReference) : base(openApiReference)
+ {
+ VisitParameters();
+ VisitOperations();
+ }
+
+ private void VisitParameters()
+ {
+ if (OpenApiDocument.Parameters == null)
+ {
+ return;
+ }
+
+ var parametersPointer = Visit("parameters");
+ var parametersVisitor = ParametersVisitor.Visit(
+ new OpenApiReference>(
+ OpenApiDocument.Parameters,
+ Document,
+ new JsonReference(Reference.Uri, parametersPointer.ToString().AsSpan())));
+ _parameterSchemaReferences = parametersVisitor.Schemas;
+ }
+
+ private void VisitOperations()
+ {
+ foreach (var openApiOperation in OpenApiDocument.Operations ?? [])
+ {
+ var method = openApiOperation.Key;
+ var operation = openApiOperation.Value;
+ var operationPointer = Visit(method.Method.ToLowerInvariant());
+ var operationReference = new JsonReference(Reference.Uri, operationPointer.ToString().AsSpan());
+ _operations.Add(method,
+ OperationVisitor.Visit(
+ new OpenApiReference(operation, Document, operationReference)));
+ }
+ }
+
+ public JsonReference GetSchemaReference(IOpenApiParameter parameter) =>
+ _parameterSchemaReferences[parameter];
+
+ internal static PathItemVisitor Visit(OpenApiReference openApiReference) =>
+ new(openApiReference);
+
+ public IOpenApiOperationVisitor Visit(HttpMethod httpMethod) =>
+ _operations[httpMethod];
+ }
+}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.ResponseVisitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.ResponseVisitor.cs
new file mode 100644
index 0000000..49bd034
--- /dev/null
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.ResponseVisitor.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using Corvus.Json;
+using Microsoft.OpenApi;
+
+namespace OpenAPI.WebApiGenerator.OpenApi.Visitor.V3;
+
+internal sealed partial class OpenApiV3Visitor
+{
+ private sealed class ResponseVisitor :
+ OpenApiVisitor, IOpenApiResponseVisitor
+ {
+ private ResponseVisitor(OpenApiReference openApiReference) : base(openApiReference)
+ {
+ VisitContent();
+ VisitHeaders();
+ }
+
+ private readonly Dictionary _headerReferences = new();
+ private readonly Dictionary _contentReferences = new();
+
+ internal static ResponseVisitor Visit(OpenApiReference openApiReference) =>
+ new(openApiReference);
+
+ private void VisitContent()
+ {
+ if (OpenApiDocument.Content == null)
+ {
+ return;
+ }
+
+ foreach (var content in OpenApiDocument.Content)
+ {
+ if (TryVisit(["content", content.Key, "schema"], out var schemaPointer))
+ {
+ _contentReferences.Add(content.Value, new JsonReference(Reference.Uri,
+ schemaPointer.ToString().AsSpan()));
+ }
+ }
+ }
+
+ private void VisitHeaders()
+ {
+ if (OpenApiDocument.Headers == null)
+ {
+ return;
+ }
+
+ foreach (var openApiHeader in OpenApiDocument.Headers)
+ {
+ if (TryVisit(["headers", openApiHeader.Key, "schema"], out var schemaPointer))
+ {
+ _headerReferences.Add(openApiHeader.Value, new JsonReference(Reference.Uri,
+ schemaPointer.ToString().AsSpan()));
+ }
+ }
+ }
+
+ public JsonReference GetSchemaReference(OpenApiMediaType mediaType) =>
+ _contentReferences[mediaType];
+
+ public bool HasContent(OpenApiMediaType mediaType) =>
+ _contentReferences.ContainsKey(mediaType);
+
+ public JsonReference GetSchemaReference(IOpenApiHeader header) =>
+ _headerReferences[header];
+ }
+}
\ No newline at end of file
diff --git a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.cs b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.cs
index dbb5eca..0b7b1e3 100644
--- a/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.cs
+++ b/src/OpenAPI.WebApiGenerator/OpenApi/Visitor/V3/OpenApiV3Visitor.cs
@@ -1,14 +1,11 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
using Corvus.Json;
using Microsoft.OpenApi;
-using OpenAPI.WebApiGenerator.Extensions;
namespace OpenAPI.WebApiGenerator.OpenApi.Visitor.V3;
-internal sealed class OpenApiV3Visitor :
+internal sealed partial class OpenApiV3Visitor :
OpenApiVisitor, IOpenApiVisitor
{
private OpenApiV3Visitor(OpenApiReference openApiReference) : base(openApiReference)
@@ -32,66 +29,4 @@ private void VisitPathItems()
public IOpenApiPathItemVisitor Visit(IOpenApiPathItem pathItem) =>
PathItemVisitor.Visit(new OpenApiReference(pathItem, Document, _pathItems[pathItem]));
-
- private sealed class PathItemVisitor :
- OpenApiVisitor, IOpenApiPathItemVisitor
- {
- private readonly Dictionary _parameterVisitors = new();
-
- private PathItemVisitor(OpenApiReference openApiReference) : base(openApiReference)
- {
- VisitParameters();
- }
-
- public JsonReference GetSchemaReference(IOpenApiParameter parameter) =>
- _parameterVisitors[parameter].Reference;
-
- private void VisitParameters()
- {
- foreach (var (parameter, i) in (OpenApiDocument.Parameters ?? []).WithIndex())
- {
- var parameterPointer = Visit("parameters", i.ToString());
- var parameterReference = new JsonReference(Reference.Uri, parameterPointer.ToString().AsSpan());
- _parameterVisitors.Add(parameter, ParameterVisitor.Visit(new OpenApiReference(
- parameter,
- Document,
- parameterReference)));
- }
- }
-
- internal static PathItemVisitor Visit(OpenApiReference openApiReference) =>
- new(openApiReference);
-
- public IOpenApiOperationVisitor Visit(HttpMethod parameter)
- {
- throw new NotImplementedException();
- }
- }
-
- private sealed class ParameterVisitor :
- OpenApiVisitor
- {
- internal JsonReference SchemaReference { get; }
-
- private ParameterVisitor(OpenApiReference openApiReference) : base(openApiReference)
- {
- SchemaReference = VisitSchema();
- }
-
- internal static ParameterVisitor Visit(OpenApiReference reference) => new(reference);
-
- private JsonReference VisitSchema()
- {
- if (!TryVisit(["schema"], out var schemaPointer))
- {
- schemaPointer = Visit(
- "content",
- OpenApiDocument.Content?.Single().Key ??
- throw new InvalidOperationException("Parameter doesn't contain a schema"),
- "schema");
- }
-
- return new JsonReference(Reference.Uri, schemaPointer.ToString().AsSpan());
- }
- }
}
\ No newline at end of file
diff --git a/tests/Example.Api.IntegrationTests/DeleteFooTests.cs b/tests/Example.OpenApi20.IntegrationTests/DeleteFooTests.cs
similarity index 94%
rename from tests/Example.Api.IntegrationTests/DeleteFooTests.cs
rename to tests/Example.OpenApi20.IntegrationTests/DeleteFooTests.cs
index 211da06..86d63e3 100644
--- a/tests/Example.Api.IntegrationTests/DeleteFooTests.cs
+++ b/tests/Example.OpenApi20.IntegrationTests/DeleteFooTests.cs
@@ -1,7 +1,7 @@
using System.Net;
using AwesomeAssertions;
-namespace Example.Api.IntegrationTests;
+namespace Example.OpenApi20.IntegrationTests;
public class DeleteFooTests(FooApplicationFactory app) : FooTestSpecification, IClassFixture
{
diff --git a/tests/Example.Api.IntegrationTests/Example.Api.IntegrationTests.csproj b/tests/Example.OpenApi20.IntegrationTests/Example.OpenApi20.IntegrationTests.csproj
similarity index 93%
rename from tests/Example.Api.IntegrationTests/Example.Api.IntegrationTests.csproj
rename to tests/Example.OpenApi20.IntegrationTests/Example.OpenApi20.IntegrationTests.csproj
index 6f45236..7171e05 100644
--- a/tests/Example.Api.IntegrationTests/Example.Api.IntegrationTests.csproj
+++ b/tests/Example.OpenApi20.IntegrationTests/Example.OpenApi20.IntegrationTests.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/tests/Example.Api.IntegrationTests/FooApplicationFactory.cs b/tests/Example.OpenApi20.IntegrationTests/FooApplicationFactory.cs
similarity index 77%
rename from tests/Example.Api.IntegrationTests/FooApplicationFactory.cs
rename to tests/Example.OpenApi20.IntegrationTests/FooApplicationFactory.cs
index 61fade6..29594a4 100644
--- a/tests/Example.Api.IntegrationTests/FooApplicationFactory.cs
+++ b/tests/Example.OpenApi20.IntegrationTests/FooApplicationFactory.cs
@@ -1,7 +1,7 @@
using JetBrains.Annotations;
using Microsoft.AspNetCore.Mvc.Testing;
-namespace Example.Api.IntegrationTests;
+namespace Example.OpenApi20.IntegrationTests;
[UsedImplicitly]
public class FooApplicationFactory : WebApplicationFactory;
\ No newline at end of file
diff --git a/tests/Example.Api.IntegrationTests/FooTestSpecification.cs b/tests/Example.OpenApi20.IntegrationTests/FooTestSpecification.cs
similarity index 87%
rename from tests/Example.Api.IntegrationTests/FooTestSpecification.cs
rename to tests/Example.OpenApi20.IntegrationTests/FooTestSpecification.cs
index ab8693d..15eca21 100644
--- a/tests/Example.Api.IntegrationTests/FooTestSpecification.cs
+++ b/tests/Example.OpenApi20.IntegrationTests/FooTestSpecification.cs
@@ -1,6 +1,6 @@
using System.Text;
-namespace Example.Api.IntegrationTests;
+namespace Example.OpenApi20.IntegrationTests;
public abstract class FooTestSpecification
{
diff --git a/tests/Example.Api.IntegrationTests/Http/HttpContentExtensions.cs b/tests/Example.OpenApi20.IntegrationTests/Http/HttpContentExtensions.cs
similarity index 89%
rename from tests/Example.Api.IntegrationTests/Http/HttpContentExtensions.cs
rename to tests/Example.OpenApi20.IntegrationTests/Http/HttpContentExtensions.cs
index 226419e..106567e 100644
--- a/tests/Example.Api.IntegrationTests/Http/HttpContentExtensions.cs
+++ b/tests/Example.OpenApi20.IntegrationTests/Http/HttpContentExtensions.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Nodes;
-namespace Example.Api.IntegrationTests.Http;
+namespace Example.OpenApi20.IntegrationTests.Http;
internal static class HttpContentExtensions
{
diff --git a/tests/Example.Api.IntegrationTests/Json/JsonNodeExtensions.cs b/tests/Example.OpenApi20.IntegrationTests/Json/JsonNodeExtensions.cs
similarity index 92%
rename from tests/Example.Api.IntegrationTests/Json/JsonNodeExtensions.cs
rename to tests/Example.OpenApi20.IntegrationTests/Json/JsonNodeExtensions.cs
index b36f928..d13a357 100644
--- a/tests/Example.Api.IntegrationTests/Json/JsonNodeExtensions.cs
+++ b/tests/Example.OpenApi20.IntegrationTests/Json/JsonNodeExtensions.cs
@@ -2,7 +2,7 @@
using AwesomeAssertions;
using Json.Pointer;
-namespace Example.Api.IntegrationTests.Json;
+namespace Example.OpenApi20.IntegrationTests.Json;
internal static class JsonNodeExtensions
{
diff --git a/tests/Example.Api.IntegrationTests/UpdateFooTests.cs b/tests/Example.OpenApi20.IntegrationTests/UpdateFooTests.cs
similarity index 94%
rename from tests/Example.Api.IntegrationTests/UpdateFooTests.cs
rename to tests/Example.OpenApi20.IntegrationTests/UpdateFooTests.cs
index 775405f..ca1d4d7 100644
--- a/tests/Example.Api.IntegrationTests/UpdateFooTests.cs
+++ b/tests/Example.OpenApi20.IntegrationTests/UpdateFooTests.cs
@@ -1,10 +1,10 @@
using System.Net;
using System.Net.Http.Headers;
using AwesomeAssertions;
-using Example.Api.IntegrationTests.Http;
-using Example.Api.IntegrationTests.Json;
+using Example.OpenApi20.IntegrationTests.Http;
+using Example.OpenApi20.IntegrationTests.Json;
-namespace Example.Api.IntegrationTests;
+namespace Example.OpenApi20.IntegrationTests;
public class UpdateFooTests(FooApplicationFactory app) : FooTestSpecification, IClassFixture
{
diff --git a/tests/Example.Api/Example.Api.csproj b/tests/Example.OpenApi20/Example.OpenApi20.csproj
similarity index 83%
rename from tests/Example.Api/Example.Api.csproj
rename to tests/Example.OpenApi20/Example.OpenApi20.csproj
index f07125d..0335268 100644
--- a/tests/Example.Api/Example.Api.csproj
+++ b/tests/Example.OpenApi20/Example.OpenApi20.csproj
@@ -4,18 +4,19 @@
net9.0
enable
enable
+ Example.OpenApi20
-
+
-
+
diff --git a/tests/Example.OpenApi20/Paths/FooFooId/Delete/Operation.Handler.cs b/tests/Example.OpenApi20/Paths/FooFooId/Delete/Operation.Handler.cs
new file mode 100644
index 0000000..b83f444
--- /dev/null
+++ b/tests/Example.OpenApi20/Paths/FooFooId/Delete/Operation.Handler.cs
@@ -0,0 +1,10 @@
+namespace Example.OpenApi20.Paths.FooFooId.Delete;
+
+internal partial class Operation
+{
+ internal partial Task HandleAsync(Request request, CancellationToken cancellationToken)
+ {
+ var response = new Response.OK200();
+ return Task.FromResult(response);
+ }
+}
\ No newline at end of file
diff --git a/tests/Example.Api/Paths/FooFooId/Put/Operation.Handler.cs b/tests/Example.OpenApi20/Paths/FooFooId/Put/Operation.Handler.cs
similarity index 62%
rename from tests/Example.Api/Paths/FooFooId/Put/Operation.Handler.cs
rename to tests/Example.OpenApi20/Paths/FooFooId/Put/Operation.Handler.cs
index 85b74e0..e21481c 100644
--- a/tests/Example.Api/Paths/FooFooId/Put/Operation.Handler.cs
+++ b/tests/Example.OpenApi20/Paths/FooFooId/Put/Operation.Handler.cs
@@ -1,16 +1,18 @@
using System.Collections.Immutable;
using Corvus.Json;
-namespace Example.Api.Paths.FooFooId.Put;
+namespace Example.OpenApi20.Paths.FooFooId.Put;
internal partial class Operation
{
public Operation()
{
- HandleValidationError = HandleValidationErrors;
+ HandleRequestValidationError = HandleValidationErrors;
+ ValidateResponse = false;
+ ValidationLevel = ValidationLevel.Detailed;
}
- private static Response HandleValidationErrors(ImmutableList validationResults)
+ private static Response.BadRequest400 HandleValidationErrors(ImmutableList validationResults)
{
var response = validationResults.Select(result =>
Responses.BadRequest.RequiredErrorAndName.Create(
@@ -34,6 +36,10 @@ internal partial Task HandleAsync(Request request, CancellationToken c
Status = 2
}
};
- return Task.FromResult(response);
+
+ var validationContext = response.Validate(ValidationLevel);
+ return !validationContext.IsValid
+ ? throw new JsonValidationException("Response is not valid", validationContext.Results)
+ : Task.FromResult(response);
}
}
\ No newline at end of file
diff --git a/tests/Example.Api/Program.cs b/tests/Example.OpenApi20/Program.cs
similarity index 89%
rename from tests/Example.Api/Program.cs
rename to tests/Example.OpenApi20/Program.cs
index b8784b3..a45305d 100644
--- a/tests/Example.Api/Program.cs
+++ b/tests/Example.OpenApi20/Program.cs
@@ -1,4 +1,4 @@
-using Example.Api;
+using Example.OpenApi20;
var builder = WebApplication.CreateBuilder(args);
builder.AddOperations(builder.Configuration.Get());
diff --git a/tests/Example.Api/Properties/launchSettings.json b/tests/Example.OpenApi20/Properties/launchSettings.json
similarity index 100%
rename from tests/Example.Api/Properties/launchSettings.json
rename to tests/Example.OpenApi20/Properties/launchSettings.json
diff --git a/tests/Example.Api/appsettings.Development.json b/tests/Example.OpenApi20/appsettings.Development.json
similarity index 100%
rename from tests/Example.Api/appsettings.Development.json
rename to tests/Example.OpenApi20/appsettings.Development.json
diff --git a/tests/Example.Api/appsettings.json b/tests/Example.OpenApi20/appsettings.json
similarity index 100%
rename from tests/Example.Api/appsettings.json
rename to tests/Example.OpenApi20/appsettings.json
diff --git a/tests/Example.Api/api.json b/tests/Example.OpenApi20/openapi.json
similarity index 100%
rename from tests/Example.Api/api.json
rename to tests/Example.OpenApi20/openapi.json
diff --git a/tests/Example.OpenApi30.IntegrationTests/DeleteFooTests.cs b/tests/Example.OpenApi30.IntegrationTests/DeleteFooTests.cs
new file mode 100644
index 0000000..76f16d3
--- /dev/null
+++ b/tests/Example.OpenApi30.IntegrationTests/DeleteFooTests.cs
@@ -0,0 +1,24 @@
+using System.Net;
+using AwesomeAssertions;
+
+namespace Example.OpenApi30.IntegrationTests;
+
+public class DeleteFooTests(FooApplicationFactory app) : FooTestSpecification, IClassFixture
+{
+ [Fact]
+ public async Task When_Deleting_Foo_It_Should_Return_Ok()
+ {
+ using var client = app.CreateClient();
+ var result = await client.SendAsync(new HttpRequestMessage()
+ {
+ RequestUri = new Uri(client.BaseAddress!, "/foo/1"),
+ Method = new HttpMethod("DELETE")
+ }, CancellationToken);
+ result.StatusCode.Should().Be(HttpStatusCode.OK);
+ var responseContent = await result.Content.ReadAsByteArrayAsync(CancellationToken);
+ responseContent.Should().BeEmpty();
+ result.Content.Headers.ContentType.Should().BeNull();
+
+ result.Headers.Should().BeEmpty();
+ }
+}
diff --git a/tests/Example.OpenApi30.IntegrationTests/Example.OpenApi30.IntegrationTests.csproj b/tests/Example.OpenApi30.IntegrationTests/Example.OpenApi30.IntegrationTests.csproj
new file mode 100644
index 0000000..ba609f5
--- /dev/null
+++ b/tests/Example.OpenApi30.IntegrationTests/Example.OpenApi30.IntegrationTests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net9.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Example.OpenApi30.IntegrationTests/FooApplicationFactory.cs b/tests/Example.OpenApi30.IntegrationTests/FooApplicationFactory.cs
new file mode 100644
index 0000000..7421815
--- /dev/null
+++ b/tests/Example.OpenApi30.IntegrationTests/FooApplicationFactory.cs
@@ -0,0 +1,7 @@
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Mvc.Testing;
+
+namespace Example.OpenApi30.IntegrationTests;
+
+[UsedImplicitly]
+public class FooApplicationFactory : WebApplicationFactory;
diff --git a/tests/Example.OpenApi30.IntegrationTests/FooTestSpecification.cs b/tests/Example.OpenApi30.IntegrationTests/FooTestSpecification.cs
new file mode 100644
index 0000000..d248b34
--- /dev/null
+++ b/tests/Example.OpenApi30.IntegrationTests/FooTestSpecification.cs
@@ -0,0 +1,13 @@
+using System.Text;
+
+namespace Example.OpenApi30.IntegrationTests;
+
+public abstract class FooTestSpecification
+{
+ protected CancellationToken CancellationToken { get; } = TestContext.Current.CancellationToken;
+
+ protected HttpContent CreateJsonContent(string json) => new StringContent(
+ json,
+ encoding: Encoding.UTF8,
+ mediaType: "application/json");
+}
diff --git a/tests/Example.OpenApi30.IntegrationTests/Http/HttpContentExtensions.cs b/tests/Example.OpenApi30.IntegrationTests/Http/HttpContentExtensions.cs
new file mode 100644
index 0000000..0cab390
--- /dev/null
+++ b/tests/Example.OpenApi30.IntegrationTests/Http/HttpContentExtensions.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Nodes;
+
+namespace Example.OpenApi30.IntegrationTests.Http;
+
+internal static class HttpContentExtensions
+{
+ internal static async Task ReadAsJsonNodeAsync(this HttpContent content,
+ CancellationToken cancellationToken) =>
+ await JsonNode.ParseAsync(
+ await content.ReadAsStreamAsync(cancellationToken)
+ .ConfigureAwait(false),
+ cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+}
diff --git a/tests/Example.OpenApi30.IntegrationTests/Json/JsonNodeExtensions.cs b/tests/Example.OpenApi30.IntegrationTests/Json/JsonNodeExtensions.cs
new file mode 100644
index 0000000..05b2100
--- /dev/null
+++ b/tests/Example.OpenApi30.IntegrationTests/Json/JsonNodeExtensions.cs
@@ -0,0 +1,23 @@
+using System.Text.Json.Nodes;
+using AwesomeAssertions;
+using Json.Pointer;
+
+namespace Example.OpenApi30.IntegrationTests.Json;
+
+internal static class JsonNodeExtensions
+{
+ internal static JsonNode Evaluate(this JsonNode? node, string path)
+ {
+ JsonPointer.Parse(path).TryEvaluate(node, out var value).Should()
+ .BeTrue($"because the json node should contain the property {path}");
+ value.Should().NotBeNull($"because the property {path} should not be null");
+ return value!;
+ }
+
+ internal static T GetValue(this JsonNode? node, string path)
+ {
+ var value = node.Evaluate(path);
+ return value.GetValue();
+ }
+
+}
diff --git a/tests/Example.OpenApi30.IntegrationTests/UpdateFooTests.cs b/tests/Example.OpenApi30.IntegrationTests/UpdateFooTests.cs
new file mode 100644
index 0000000..29fed4b
--- /dev/null
+++ b/tests/Example.OpenApi30.IntegrationTests/UpdateFooTests.cs
@@ -0,0 +1,67 @@
+using System.Net;
+using System.Net.Http.Headers;
+using AwesomeAssertions;
+using Example.OpenApi30.IntegrationTests.Http;
+using Example.OpenApi30.IntegrationTests.Json;
+
+namespace Example.OpenApi30.IntegrationTests;
+
+public class UpdateFooTests(FooApplicationFactory app) : FooTestSpecification, IClassFixture
+{
+ [Fact]
+ public async Task When_Updating_Foo_It_Should_Return_Updated_Foo()
+ {
+ using var client = app.CreateClient();
+ var result = await client.SendAsync(new HttpRequestMessage()
+ {
+ RequestUri = new Uri(client.BaseAddress!, "/foo/1"),
+ Method = new HttpMethod("PUT"),
+ Content = CreateJsonContent(
+ """
+ {
+ "Name": "test"
+ }
+ """),
+ Headers =
+ {
+ { "Bar", "test" }
+ }
+ }, CancellationToken);
+ result.StatusCode.Should().Be(HttpStatusCode.OK);
+ var responseContent = await result.Content.ReadAsJsonNodeAsync(CancellationToken);
+ responseContent.Should().NotBeNull();
+ responseContent.GetValue("#/Name").Should().Be("test");
+ result.Headers.Should().HaveCount(1);
+ result.Headers.Should().ContainKey("Status")
+ .WhoseValue.Should().HaveCount(1)
+ .And.Contain("2");
+ result.Content.Headers.ContentType.Should().Be(MediaTypeHeaderValue.Parse("application/json"));
+ }
+
+ [Fact]
+ public async Task Given_invalid_request_When_Updating_Foo_It_Should_Return_400()
+ {
+ using var client = app.CreateClient();
+ var result = await client.SendAsync(new HttpRequestMessage()
+ {
+ RequestUri = new Uri(client.BaseAddress!, "/foo/test"),
+ Method = new HttpMethod("PUT"),
+ Content = CreateJsonContent(
+ """
+ {
+ "Name": "test"
+ }
+ """),
+ Headers =
+ {
+ { "Bar", "test" }
+ }
+ }, CancellationToken);
+ result.StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ var responseContent = await result.Content.ReadAsJsonNodeAsync(CancellationToken);
+ responseContent.Should().NotBeNull();
+ responseContent.AsArray().Should().HaveCount(1);
+ responseContent.GetValue("#/0/error").Should().NotBeNullOrEmpty();
+ responseContent.GetValue("#/0/name").Should().Be("https://localhost/api.json#/components/parameters/FooId/schema/type");
+ }
+}
diff --git a/tests/Example.OpenApi30/Example.OpenApi30.csproj b/tests/Example.OpenApi30/Example.OpenApi30.csproj
new file mode 100644
index 0000000..e654656
--- /dev/null
+++ b/tests/Example.OpenApi30/Example.OpenApi30.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net9.0
+ enable
+ enable
+ Example.OpenApi30
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Example.OpenApi30/Paths/FooFooId/Delete/Operation.Handler.cs b/tests/Example.OpenApi30/Paths/FooFooId/Delete/Operation.Handler.cs
new file mode 100644
index 0000000..3028b19
--- /dev/null
+++ b/tests/Example.OpenApi30/Paths/FooFooId/Delete/Operation.Handler.cs
@@ -0,0 +1,10 @@
+namespace Example.OpenApi30.Paths.FooFooId.Delete;
+
+internal partial class Operation
+{
+ internal partial Task HandleAsync(Request request, CancellationToken cancellationToken)
+ {
+ var response = new Response.OK200();
+ return Task.FromResult(response);
+ }
+}
diff --git a/tests/Example.OpenApi30/Paths/FooFooId/Put/Operation.Handler.cs b/tests/Example.OpenApi30/Paths/FooFooId/Put/Operation.Handler.cs
new file mode 100644
index 0000000..a535008
--- /dev/null
+++ b/tests/Example.OpenApi30/Paths/FooFooId/Put/Operation.Handler.cs
@@ -0,0 +1,40 @@
+using System.Collections.Immutable;
+using Corvus.Json;
+
+namespace Example.OpenApi30.Paths.FooFooId.Put;
+
+internal partial class Operation
+{
+ public Operation()
+ {
+ HandleRequestValidationError = HandleValidationErrors;
+ ValidateResponse = true;
+ }
+
+ private static Response.BadRequest400 HandleValidationErrors(ImmutableList validationResults)
+ {
+ var response = validationResults.Select(result =>
+ Components.Responses.BadRequest.Content.ApplicationJson.RequiredErrorAndName.Create(
+ name: result.Location?.SchemaLocation.ToString() ?? string.Empty,
+ error: result.Message ?? string.Empty));
+ return new Response.BadRequest400(
+ Components.Responses.BadRequest.Content.ApplicationJson.Create(response.ToArray()));
+ }
+
+ internal partial Task HandleAsync(Request request, CancellationToken cancellationToken)
+ {
+ _ = request.Query.Fee;
+ _ = request.Path.FooId;
+ _ = request.Header.Bar;
+
+ var response = new Response.OK200(Components.Schemas.FooProperties.Create(
+ name: request.Body.ApplicationJson?.Name))
+ {
+ Headers = new Response.OK200.ResponseHeaders
+ {
+ Status = 2
+ }
+ };
+ return Task.FromResult(response);
+ }
+}
diff --git a/tests/Example.OpenApi30/Program.cs b/tests/Example.OpenApi30/Program.cs
new file mode 100644
index 0000000..bd9e298
--- /dev/null
+++ b/tests/Example.OpenApi30/Program.cs
@@ -0,0 +1,9 @@
+using Example.OpenApi30;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.AddOperations(builder.Configuration.Get());
+var app = builder.Build();
+app.MapOperations();
+app.Run();
+
+public abstract partial class Program;
\ No newline at end of file
diff --git a/tests/Example.OpenApi30/appsettings.Development.json b/tests/Example.OpenApi30/appsettings.Development.json
new file mode 100644
index 0000000..1b2d3ba
--- /dev/null
+++ b/tests/Example.OpenApi30/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi30/appsettings.json b/tests/Example.OpenApi30/appsettings.json
new file mode 100644
index 0000000..1fdceaf
--- /dev/null
+++ b/tests/Example.OpenApi30/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "OpenApiSpecificationUri": "https://localhost/api.json"
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi30/openapi.json b/tests/Example.OpenApi30/openapi.json
new file mode 100644
index 0000000..48611c6
--- /dev/null
+++ b/tests/Example.OpenApi30/openapi.json
@@ -0,0 +1,147 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Example API",
+ "version": "2025-11-05",
+ "description": "Foo API"
+ },
+ "paths": {
+ "/foo/{FooId}": {
+ "put": {
+ "operationId": "Update_Foo",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/Bar"
+ },
+ {
+ "name": "Fee",
+ "in": "query",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "required": false,
+ "style": "form",
+ "explode": true
+ }
+ ],
+ "requestBody": {
+ "$ref": "#/components/requestBodies/FooProperties"
+ },
+ "responses": {
+ "200": {
+ "description": "Successfully updated",
+ "headers": {
+ "Status": {
+ "description": "The Status of foo",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ "Tag": {
+ "description": "An optional tag",
+ "schema": {
+ "type": "string"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FooProperties"
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ }
+ }
+ },
+ "delete": {
+ "operationId": "Delete_Foo",
+ "responses": {
+ "200": {
+ "description": "Successfully deleted"
+ }
+ }
+ },
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/FooId"
+ }
+ ]
+ }
+ },
+ "components": {
+ "schemas": {
+ "FooProperties": {
+ "description": "Foo properties.",
+ "type": "object",
+ "properties": {
+ "Name": {
+ "description": "Name of foo",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "parameters": {
+ "Bar": {
+ "name": "Bar",
+ "in": "header",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ "FooId": {
+ "name": "FooId",
+ "in": "path",
+ "schema": {
+ "type": "integer"
+ },
+ "required": true
+ }
+ },
+ "requestBodies": {
+ "FooProperties": {
+ "description": "Foo properties.",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FooProperties"
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "BadRequest": {
+ "description": "Returned when the request has validation errors",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "error": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "error"]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi31.IntegrationTests/DeleteFooTests.cs b/tests/Example.OpenApi31.IntegrationTests/DeleteFooTests.cs
new file mode 100644
index 0000000..50a1773
--- /dev/null
+++ b/tests/Example.OpenApi31.IntegrationTests/DeleteFooTests.cs
@@ -0,0 +1,24 @@
+using System.Net;
+using AwesomeAssertions;
+
+namespace Example.OpenApi31.IntegrationTests;
+
+public class DeleteFooTests(FooApplicationFactory app) : FooTestSpecification, IClassFixture
+{
+ [Fact]
+ public async Task When_Deleting_Foo_It_Should_Return_Ok()
+ {
+ using var client = app.CreateClient();
+ var result = await client.SendAsync(new HttpRequestMessage()
+ {
+ RequestUri = new Uri(client.BaseAddress!, "/foo/1"),
+ Method = new HttpMethod("DELETE")
+ }, CancellationToken);
+ result.StatusCode.Should().Be(HttpStatusCode.OK);
+ var responseContent = await result.Content.ReadAsByteArrayAsync(CancellationToken);
+ responseContent.Should().BeEmpty();
+ result.Content.Headers.ContentType.Should().BeNull();
+
+ result.Headers.Should().BeEmpty();
+ }
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi31.IntegrationTests/Example.OpenApi31.IntegrationTests.csproj b/tests/Example.OpenApi31.IntegrationTests/Example.OpenApi31.IntegrationTests.csproj
new file mode 100644
index 0000000..78f593d
--- /dev/null
+++ b/tests/Example.OpenApi31.IntegrationTests/Example.OpenApi31.IntegrationTests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net9.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Example.OpenApi31.IntegrationTests/FooApplicationFactory.cs b/tests/Example.OpenApi31.IntegrationTests/FooApplicationFactory.cs
new file mode 100644
index 0000000..2ba4121
--- /dev/null
+++ b/tests/Example.OpenApi31.IntegrationTests/FooApplicationFactory.cs
@@ -0,0 +1,7 @@
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Mvc.Testing;
+
+namespace Example.OpenApi31.IntegrationTests;
+
+[UsedImplicitly]
+public class FooApplicationFactory : WebApplicationFactory;
\ No newline at end of file
diff --git a/tests/Example.OpenApi31.IntegrationTests/FooTestSpecification.cs b/tests/Example.OpenApi31.IntegrationTests/FooTestSpecification.cs
new file mode 100644
index 0000000..a2ac8ca
--- /dev/null
+++ b/tests/Example.OpenApi31.IntegrationTests/FooTestSpecification.cs
@@ -0,0 +1,13 @@
+using System.Text;
+
+namespace Example.OpenApi31.IntegrationTests;
+
+public abstract class FooTestSpecification
+{
+ protected CancellationToken CancellationToken { get; } = TestContext.Current.CancellationToken;
+
+ protected HttpContent CreateJsonContent(string json) => new StringContent(
+ json,
+ encoding: Encoding.UTF8,
+ mediaType: "application/json");
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi31.IntegrationTests/Http/HttpContentExtensions.cs b/tests/Example.OpenApi31.IntegrationTests/Http/HttpContentExtensions.cs
new file mode 100644
index 0000000..bcc0da8
--- /dev/null
+++ b/tests/Example.OpenApi31.IntegrationTests/Http/HttpContentExtensions.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Nodes;
+
+namespace Example.OpenApi31.IntegrationTests.Http;
+
+internal static class HttpContentExtensions
+{
+ internal static async Task ReadAsJsonNodeAsync(this HttpContent content,
+ CancellationToken cancellationToken) =>
+ await JsonNode.ParseAsync(
+ await content.ReadAsStreamAsync(cancellationToken)
+ .ConfigureAwait(false),
+ cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi31.IntegrationTests/Json/JsonNodeExtensions.cs b/tests/Example.OpenApi31.IntegrationTests/Json/JsonNodeExtensions.cs
new file mode 100644
index 0000000..5689ab6
--- /dev/null
+++ b/tests/Example.OpenApi31.IntegrationTests/Json/JsonNodeExtensions.cs
@@ -0,0 +1,23 @@
+using System.Text.Json.Nodes;
+using AwesomeAssertions;
+using Json.Pointer;
+
+namespace Example.OpenApi31.IntegrationTests.Json;
+
+internal static class JsonNodeExtensions
+{
+ internal static JsonNode Evaluate(this JsonNode? node, string path)
+ {
+ JsonPointer.Parse(path).TryEvaluate(node, out var value).Should()
+ .BeTrue($"because the json node should contain the property {path}");
+ value.Should().NotBeNull($"because the property {path} should not be null");
+ return value!;
+ }
+
+ internal static T GetValue(this JsonNode? node, string path)
+ {
+ var value = node.Evaluate(path);
+ return value.GetValue();
+ }
+
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi31.IntegrationTests/UpdateFooTests.cs b/tests/Example.OpenApi31.IntegrationTests/UpdateFooTests.cs
new file mode 100644
index 0000000..248a95e
--- /dev/null
+++ b/tests/Example.OpenApi31.IntegrationTests/UpdateFooTests.cs
@@ -0,0 +1,67 @@
+using System.Net;
+using System.Net.Http.Headers;
+using AwesomeAssertions;
+using Example.OpenApi31.IntegrationTests.Http;
+using Example.OpenApi31.IntegrationTests.Json;
+
+namespace Example.OpenApi31.IntegrationTests;
+
+public class UpdateFooTests(FooApplicationFactory app) : FooTestSpecification, IClassFixture
+{
+ [Fact]
+ public async Task When_Updating_Foo_It_Should_Return_Updated_Foo()
+ {
+ using var client = app.CreateClient();
+ var result = await client.SendAsync(new HttpRequestMessage()
+ {
+ RequestUri = new Uri(client.BaseAddress!, "/foo/1"),
+ Method = new HttpMethod("PUT"),
+ Content = CreateJsonContent(
+ """
+ {
+ "Name": "test"
+ }
+ """),
+ Headers =
+ {
+ { "Bar", "test" }
+ }
+ }, CancellationToken);
+ result.StatusCode.Should().Be(HttpStatusCode.OK);
+ var responseContent = await result.Content.ReadAsJsonNodeAsync(CancellationToken);
+ responseContent.Should().NotBeNull();
+ responseContent.GetValue("#/Name").Should().Be("test");
+ result.Headers.Should().HaveCount(1);
+ result.Headers.Should().ContainKey("Status")
+ .WhoseValue.Should().HaveCount(1)
+ .And.Contain("2");
+ result.Content.Headers.ContentType.Should().Be(MediaTypeHeaderValue.Parse("application/json"));
+ }
+
+ [Fact]
+ public async Task Given_invalid_request_When_Updating_Foo_It_Should_Return_400()
+ {
+ using var client = app.CreateClient();
+ var result = await client.SendAsync(new HttpRequestMessage()
+ {
+ RequestUri = new Uri(client.BaseAddress!, "/foo/test"),
+ Method = new HttpMethod("PUT"),
+ Content = CreateJsonContent(
+ """
+ {
+ "Name": "test"
+ }
+ """),
+ Headers =
+ {
+ { "Bar", "test" }
+ }
+ }, CancellationToken);
+ result.StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ var responseContent = await result.Content.ReadAsJsonNodeAsync(CancellationToken);
+ responseContent.Should().NotBeNull();
+ responseContent.AsArray().Should().HaveCount(1);
+ responseContent.GetValue("#/0/error").Should().NotBeNullOrEmpty();
+ responseContent.GetValue("#/0/name").Should().Be("https://localhost/api.json#/components/parameters/FooId/schema/type");
+ }
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi31/Example.OpenApi31.csproj b/tests/Example.OpenApi31/Example.OpenApi31.csproj
new file mode 100644
index 0000000..9c5c66c
--- /dev/null
+++ b/tests/Example.OpenApi31/Example.OpenApi31.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net9.0
+ enable
+ enable
+ Example.OpenApi31
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Example.Api/Paths/FooFooId/Delete/Operation.Handler.cs b/tests/Example.OpenApi31/Paths/FooFooId/Delete/Operation.Handler.cs
similarity index 82%
rename from tests/Example.Api/Paths/FooFooId/Delete/Operation.Handler.cs
rename to tests/Example.OpenApi31/Paths/FooFooId/Delete/Operation.Handler.cs
index 02db5d2..ec6b618 100644
--- a/tests/Example.Api/Paths/FooFooId/Delete/Operation.Handler.cs
+++ b/tests/Example.OpenApi31/Paths/FooFooId/Delete/Operation.Handler.cs
@@ -1,4 +1,4 @@
-namespace Example.Api.Paths.FooFooId.Delete;
+namespace Example.OpenApi31.Paths.FooFooId.Delete;
internal partial class Operation
{
diff --git a/tests/Example.OpenApi31/Paths/FooFooId/Put/Operation.Handler.cs b/tests/Example.OpenApi31/Paths/FooFooId/Put/Operation.Handler.cs
new file mode 100644
index 0000000..7fd2835
--- /dev/null
+++ b/tests/Example.OpenApi31/Paths/FooFooId/Put/Operation.Handler.cs
@@ -0,0 +1,39 @@
+using System.Collections.Immutable;
+using Corvus.Json;
+
+namespace Example.OpenApi31.Paths.FooFooId.Put;
+
+internal partial class Operation
+{
+ public Operation()
+ {
+ HandleRequestValidationError = HandleValidationErrors;
+ }
+
+ private static Response.BadRequest400 HandleValidationErrors(ImmutableList validationResults)
+ {
+ var response = validationResults.Select(result =>
+ Components.Responses.BadRequest.Content.ApplicationJson.RequiredErrorAndName.Create(
+ name: result.Location?.SchemaLocation.ToString() ?? string.Empty,
+ error: result.Message ?? string.Empty));
+ return new Response.BadRequest400(
+ Components.Responses.BadRequest.Content.ApplicationJson.Create(response.ToArray()));
+ }
+
+ internal partial Task HandleAsync(Request request, CancellationToken cancellationToken)
+ {
+ _ = request.Query.Fee;
+ _ = request.Path.FooId;
+ _ = request.Header.Bar;
+
+ var response = new Response.OK200(Components.Schemas.FooProperties.Create(
+ name: request.Body.ApplicationJson?.Name))
+ {
+ Headers = new Response.OK200.ResponseHeaders
+ {
+ Status = 2
+ }
+ };
+ return Task.FromResult(response);
+ }
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi31/Program.cs b/tests/Example.OpenApi31/Program.cs
new file mode 100644
index 0000000..334940a
--- /dev/null
+++ b/tests/Example.OpenApi31/Program.cs
@@ -0,0 +1,9 @@
+using Example.OpenApi31;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.AddOperations(builder.Configuration.Get());
+var app = builder.Build();
+app.MapOperations();
+app.Run();
+
+public abstract partial class Program;
\ No newline at end of file
diff --git a/tests/Example.OpenApi31/appsettings.Development.json b/tests/Example.OpenApi31/appsettings.Development.json
new file mode 100644
index 0000000..1b2d3ba
--- /dev/null
+++ b/tests/Example.OpenApi31/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi31/appsettings.json b/tests/Example.OpenApi31/appsettings.json
new file mode 100644
index 0000000..1fdceaf
--- /dev/null
+++ b/tests/Example.OpenApi31/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "OpenApiSpecificationUri": "https://localhost/api.json"
+}
\ No newline at end of file
diff --git a/tests/Example.OpenApi31/openapi.json b/tests/Example.OpenApi31/openapi.json
new file mode 100644
index 0000000..f8d7cba
--- /dev/null
+++ b/tests/Example.OpenApi31/openapi.json
@@ -0,0 +1,147 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Example API",
+ "version": "2025-11-05",
+ "description": "Foo API"
+ },
+ "paths": {
+ "/foo/{FooId}": {
+ "put": {
+ "operationId": "Update_Foo",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/Bar"
+ },
+ {
+ "name": "Fee",
+ "in": "query",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "required": false,
+ "style": "form",
+ "explode": true
+ }
+ ],
+ "requestBody": {
+ "$ref": "#/components/requestBodies/FooProperties"
+ },
+ "responses": {
+ "200": {
+ "description": "Successfully updated",
+ "headers": {
+ "Status": {
+ "description": "The Status of foo",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ "Tag": {
+ "description": "An optional tag",
+ "schema": {
+ "type": "string"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FooProperties"
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ }
+ }
+ },
+ "delete": {
+ "operationId": "Delete_Foo",
+ "responses": {
+ "200": {
+ "description": "Successfully deleted"
+ }
+ }
+ },
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/FooId"
+ }
+ ]
+ }
+ },
+ "components": {
+ "schemas": {
+ "FooProperties": {
+ "description": "Foo properties.",
+ "type": "object",
+ "properties": {
+ "Name": {
+ "description": "Name of foo",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "parameters": {
+ "Bar": {
+ "name": "Bar",
+ "in": "header",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ },
+ "FooId": {
+ "name": "FooId",
+ "in": "path",
+ "schema": {
+ "type": "integer"
+ },
+ "required": true
+ }
+ },
+ "requestBodies": {
+ "FooProperties": {
+ "description": "Foo properties.",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FooProperties"
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "BadRequest": {
+ "description": "Returned when the request has validation errors",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "error": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "error"]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/OpenAPI.WebApiGenerator.Tests/ApiGeneratorTests.cs b/tests/OpenAPI.WebApiGenerator.Tests/ApiGeneratorTests.cs
index b1010ff..7f3301f 100644
--- a/tests/OpenAPI.WebApiGenerator.Tests/ApiGeneratorTests.cs
+++ b/tests/OpenAPI.WebApiGenerator.Tests/ApiGeneratorTests.cs
@@ -16,8 +16,11 @@ public class ApiGeneratorTests
{
private CancellationToken Cancellation => TestContext.Current.CancellationToken;
- [Fact]
- public void GivenAnOpenAPISpec_WhenGeneratingAPI_ExpectedClassesShouldHaveBeenGenerated()
+ [Theory]
+ [InlineData("openapi-v2.json")]
+ [InlineData("openapi-v3.json")]
+ [InlineData("openapi-v3.1.json")]
+ public void GivenAnOpenAPISpec_WhenGeneratingAPI_ExpectedClassesShouldHaveBeenGenerated(string specFile)
{
var generator = new ApiGenerator();
@@ -25,24 +28,24 @@ public void GivenAnOpenAPISpec_WhenGeneratingAPI_ExpectedClassesShouldHaveBeenGe
driver = driver.AddAdditionalTexts(
[
- new TestAdditionalFile("OpenApiSpecs/file.json")
+ new TestAdditionalFile($"OpenApiSpecs/{specFile}")
]
);
var compilation = CSharpCompilation.Create(nameof(ApiGeneratorTests));
- driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics, TestContext.Current.CancellationToken);
+ driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics, Cancellation);
// Operation handler stubs should be generated with a warning
diagnostics.Should().AllSatisfy(diagnostic =>
{
diagnostic.Severity.Should().Be(DiagnosticSeverity.Warning);
diagnostic.Id.Should().Be("AF1001", diagnostic.GetMessage());
- });
+ });
var generatedFiles = newCompilation.SyntaxTrees
.Select(t => Path.GetFileName(t.FilePath))
.ToArray();
-
+
generatedFiles.Should().HaveCountGreaterThan(0);
generatedFiles.Should().ContainMatch("*.Request.g.cs");
generatedFiles.Should().ContainMatch("*.Response.g.cs");
@@ -62,6 +65,10 @@ public void GivenAImplementedOperation_WhenGeneratingAPI_NoOperationHandlerStubs
"""
{
"swagger": "2.0",
+ "info": {
+ "title": "foo",
+ "version": "1.0"
+ },
"paths": {
"/foo": {
"put": {
@@ -156,6 +163,10 @@ public void NoResponseContent_Generating_DefaultResponseConstructor()
"""
{
"swagger": "2.0",
+ "info": {
+ "title": "foo",
+ "version": "1.0"
+ },
"paths": {
"/foo": {
"delete": {
@@ -213,4 +224,5 @@ private Compilation SetupGenerator(string openApiSpec, out ImmutableArray
-
+
Always
diff --git a/tests/OpenAPI.WebApiGenerator.Tests/OpenApiSpecs/file.json b/tests/OpenAPI.WebApiGenerator.Tests/OpenApiSpecs/openapi-v2.json
similarity index 100%
rename from tests/OpenAPI.WebApiGenerator.Tests/OpenApiSpecs/file.json
rename to tests/OpenAPI.WebApiGenerator.Tests/OpenApiSpecs/openapi-v2.json
diff --git a/tests/OpenAPI.WebApiGenerator.Tests/OpenApiSpecs/openapi-v3.1.json b/tests/OpenAPI.WebApiGenerator.Tests/OpenApiSpecs/openapi-v3.1.json
new file mode 100644
index 0000000..2b00e74
--- /dev/null
+++ b/tests/OpenAPI.WebApiGenerator.Tests/OpenApiSpecs/openapi-v3.1.json
@@ -0,0 +1,865 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Comprehensive Test API",
+ "description": "An exhaustive OpenAPI 3.1 specification for testing code generation",
+ "version": "1.0.0",
+ "contact": {
+ "name": "Test",
+ "email": "test@example.com"
+ },
+ "license": {
+ "name": "MIT",
+ "identifier": "MIT"
+ }
+ },
+ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
+ "servers": [
+ {
+ "url": "https://api.example.com/v1"
+ }
+ ],
+ "tags": [
+ {"name": "pets", "description": "Pet operations"},
+ {"name": "store", "description": "Store operations"},
+ {"name": "users", "description": "User operations"}
+ ],
+ "paths": {
+ "/pets": {
+ "get": {
+ "operationId": "listPets",
+ "summary": "List all pets",
+ "tags": ["pets"],
+ "parameters": [
+ {"$ref": "#/components/parameters/LimitParam"},
+ {"$ref": "#/components/parameters/OffsetParam"},
+ {
+ "name": "status",
+ "in": "query",
+ "description": "Filter by status",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/PetStatus"
+ }
+ },
+ {
+ "name": "tags",
+ "in": "query",
+ "description": "Filter by tags",
+ "required": false,
+ "style": "form",
+ "explode": true,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {"$ref": "#/components/parameters/RequestIdHeader"}
+ ],
+ "responses": {
+ "200": {
+ "description": "A list of pets",
+ "headers": {
+ "X-Total-Count": {"$ref": "#/components/headers/TotalCount"},
+ "X-Page-Size": {
+ "description": "Number of items per page",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ }
+ },
+ "400": {"$ref": "#/components/responses/BadRequest"},
+ "default": {
+ "description": "Unexpected error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "operationId": "createPet",
+ "summary": "Create a pet",
+ "tags": ["pets"],
+ "requestBody": {"$ref": "#/components/requestBodies/NewPetBody"},
+ "responses": {
+ "201": {
+ "description": "Pet created",
+ "headers": {
+ "Location": {
+ "description": "URL of created pet",
+ "schema": {
+ "type": "string",
+ "format": "uri"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "400": {"$ref": "#/components/responses/BadRequest"}
+ }
+ }
+ },
+ "/pets/{petId}": {
+ "parameters": [
+ {"$ref": "#/components/parameters/PetIdPath"}
+ ],
+ "get": {
+ "operationId": "getPet",
+ "summary": "Get a pet by ID",
+ "tags": ["pets"],
+ "responses": {
+ "200": {"$ref": "#/components/responses/PetResponse"},
+ "404": {"$ref": "#/components/responses/NotFound"}
+ }
+ },
+ "put": {
+ "operationId": "updatePet",
+ "summary": "Update a pet",
+ "tags": ["pets"],
+ "requestBody": {
+ "description": "Pet data to update",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NewPet"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Pet updated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "404": {"$ref": "#/components/responses/NotFound"}
+ }
+ },
+ "delete": {
+ "operationId": "deletePet",
+ "summary": "Delete a pet",
+ "tags": ["pets"],
+ "parameters": [
+ {
+ "name": "X-Api-Key",
+ "in": "header",
+ "description": "API key for authorization",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "Pet deleted"
+ },
+ "404": {"$ref": "#/components/responses/NotFound"}
+ }
+ }
+ },
+ "/pets/{petId}/image": {
+ "post": {
+ "operationId": "uploadPetImage",
+ "summary": "Upload pet image",
+ "tags": ["pets"],
+ "parameters": [
+ {
+ "name": "petId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "requestBody": {
+ "description": "Image to upload",
+ "required": true,
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "file": {
+ "type": "string",
+ "contentMediaType": "image/*",
+ "contentEncoding": "binary"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "required": ["file"]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Image uploaded",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ImageUploadResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/store/orders": {
+ "post": {
+ "operationId": "placeOrder",
+ "summary": "Place an order",
+ "tags": ["store"],
+ "requestBody": {"$ref": "#/components/requestBodies/OrderBody"},
+ "responses": {
+ "201": {"$ref": "#/components/responses/OrderResponse"},
+ "400": {
+ "description": "Invalid order",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/store/orders/{orderId}": {
+ "get": {
+ "operationId": "getOrder",
+ "summary": "Get order by ID",
+ "tags": ["store"],
+ "parameters": [
+ {"$ref": "#/components/parameters/OrderIdPath"}
+ ],
+ "responses": {
+ "200": {"$ref": "#/components/responses/OrderResponse"},
+ "404": {
+ "description": "Order not found"
+ }
+ }
+ },
+ "delete": {
+ "operationId": "cancelOrder",
+ "summary": "Cancel an order",
+ "tags": ["store"],
+ "parameters": [
+ {
+ "name": "orderId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "Order cancelled"
+ },
+ "404": {
+ "description": "Order not found"
+ }
+ }
+ }
+ },
+ "/store/inventory": {
+ "get": {
+ "operationId": "getInventory",
+ "summary": "Get store inventory",
+ "tags": ["store"],
+ "responses": {
+ "200": {
+ "description": "Inventory counts by status",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "integer"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users": {
+ "post": {
+ "operationId": "createUser",
+ "summary": "Create user",
+ "tags": ["users"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {"$ref": "#/components/responses/UserResponse"}
+ }
+ }
+ },
+ "/users/{username}": {
+ "parameters": [
+ {"$ref": "#/components/parameters/UsernamePath"}
+ ],
+ "get": {
+ "operationId": "getUser",
+ "summary": "Get user by username",
+ "tags": ["users"],
+ "responses": {
+ "200": {"$ref": "#/components/responses/UserResponse"},
+ "404": {"$ref": "#/components/responses/NotFound"}
+ }
+ },
+ "put": {
+ "operationId": "updateUser",
+ "summary": "Update user",
+ "tags": ["users"],
+ "requestBody": {"$ref": "#/components/requestBodies/UserBody"},
+ "responses": {
+ "200": {
+ "description": "User updated"
+ },
+ "404": {"$ref": "#/components/responses/NotFound"}
+ }
+ },
+ "delete": {
+ "operationId": "deleteUser",
+ "summary": "Delete user",
+ "tags": ["users"],
+ "parameters": [
+ {
+ "name": "username",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "User deleted"
+ },
+ "404": {
+ "description": "User not found"
+ }
+ }
+ }
+ },
+ "/users/login": {
+ "post": {
+ "operationId": "loginUser",
+ "summary": "User login",
+ "tags": ["users"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["username", "password"],
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string",
+ "format": "password"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Login successful",
+ "headers": {
+ "X-Rate-Limit": {"$ref": "#/components/headers/RateLimit"},
+ "X-Expires-After": {
+ "description": "Token expiration time",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/LoginResponse"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Invalid credentials"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Pet": {
+ "type": "object",
+ "required": ["id", "name"],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "readOnly": true
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "category": {
+ "$ref": "#/components/schemas/Category"
+ },
+ "photoUrls": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ }
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Tag"
+ }
+ },
+ "status": {
+ "$ref": "#/components/schemas/PetStatus"
+ },
+ "metadata": {
+ "type": ["object", "null"],
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "readOnly": true
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "readOnly": true
+ }
+ }
+ },
+ "NewPet": {
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "category": {
+ "$ref": "#/components/schemas/Category"
+ },
+ "photoUrls": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ }
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Tag"
+ }
+ },
+ "status": {
+ "type": "string",
+ "enum": ["available", "pending", "sold"],
+ "default": "available"
+ }
+ }
+ },
+ "PetStatus": {
+ "type": "string",
+ "enum": ["available", "pending", "sold"],
+ "default": "available"
+ },
+ "Category": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "Tag": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "Order": {
+ "type": "object",
+ "required": ["petId", "quantity"],
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uuid",
+ "readOnly": true
+ },
+ "petId": {
+ "type": "integer"
+ },
+ "quantity": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "shipDate": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["placed", "approved", "delivered"],
+ "default": "placed"
+ },
+ "complete": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ },
+ "User": {
+ "type": "object",
+ "required": ["username", "email"],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "readOnly": true
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50,
+ "pattern": "^[a-zA-Z0-9_]+$"
+ },
+ "firstName": {
+ "type": ["string", "null"]
+ },
+ "lastName": {
+ "type": ["string", "null"]
+ },
+ "email": {
+ "type": "string",
+ "format": "email"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "writeOnly": true
+ },
+ "phone": {
+ "type": ["string", "null"]
+ },
+ "userStatus": {
+ "type": "integer",
+ "description": "User status"
+ }
+ }
+ },
+ "LoginResponse": {
+ "type": "object",
+ "properties": {
+ "token": {
+ "type": "string"
+ },
+ "expiresIn": {
+ "type": "integer"
+ }
+ }
+ },
+ "ImageUploadResponse": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "type": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ },
+ "Error": {
+ "type": "object",
+ "required": ["code", "message"],
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "details": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": {
+ "PetIdPath": {
+ "name": "petId",
+ "in": "path",
+ "description": "Pet ID",
+ "required": true,
+ "schema": {
+ "type": "integer"
+ }
+ },
+ "OrderIdPath": {
+ "name": "orderId",
+ "in": "path",
+ "description": "Order ID",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "UsernamePath": {
+ "name": "username",
+ "in": "path",
+ "description": "Username",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50,
+ "pattern": "^[a-zA-Z0-9_]+$"
+ }
+ },
+ "LimitParam": {
+ "name": "limit",
+ "in": "query",
+ "description": "Maximum number of items to return",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 20
+ }
+ },
+ "OffsetParam": {
+ "name": "offset",
+ "in": "query",
+ "description": "Number of items to skip",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 0
+ }
+ },
+ "RequestIdHeader": {
+ "name": "X-Request-Id",
+ "in": "header",
+ "description": "Request correlation ID",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ },
+ "headers": {
+ "TotalCount": {
+ "description": "Total number of items",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ "RateLimit": {
+ "description": "Calls per hour allowed",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ },
+ "requestBodies": {
+ "NewPetBody": {
+ "description": "Pet to create",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NewPet"
+ }
+ }
+ }
+ },
+ "OrderBody": {
+ "description": "Order to place",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ }
+ }
+ },
+ "UserBody": {
+ "description": "User data",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "PetResponse": {
+ "description": "Pet found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "OrderResponse": {
+ "description": "Order found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ }
+ }
+ },
+ "UserResponse": {
+ "description": "User found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "BadRequest": {
+ "description": "Invalid input",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "NotFound": {
+ "description": "Resource not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ },
+ "securitySchemes": {
+ "bearerAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT"
+ },
+ "apiKey": {
+ "type": "apiKey",
+ "in": "header",
+ "name": "X-Api-Key"
+ }
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/OpenAPI.WebApiGenerator.Tests/OpenApiSpecs/openapi-v3.json b/tests/OpenAPI.WebApiGenerator.Tests/OpenApiSpecs/openapi-v3.json
new file mode 100644
index 0000000..caece69
--- /dev/null
+++ b/tests/OpenAPI.WebApiGenerator.Tests/OpenApiSpecs/openapi-v3.json
@@ -0,0 +1,908 @@
+{
+ "openapi": "3.0.3",
+ "info": {
+ "title": "Comprehensive Test API",
+ "description": "An exhaustive OpenAPI 3.0 specification for testing code generation",
+ "version": "1.0.0",
+ "contact": {
+ "name": "Test",
+ "email": "test@example.com"
+ },
+ "license": {
+ "name": "MIT"
+ }
+ },
+ "servers": [
+ {
+ "url": "https://api.example.com/v1"
+ }
+ ],
+ "tags": [
+ {"name": "pets", "description": "Pet operations"},
+ {"name": "store", "description": "Store operations"},
+ {"name": "users", "description": "User operations"}
+ ],
+ "paths": {
+ "/pets": {
+ "get": {
+ "operationId": "listPets",
+ "summary": "List all pets",
+ "tags": ["pets"],
+ "parameters": [
+ {
+ "name": "limit",
+ "in": "query",
+ "description": "Maximum number of items to return",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "format": "int32",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 20
+ }
+ },
+ {
+ "name": "offset",
+ "in": "query",
+ "description": "Number of items to skip",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "format": "int32",
+ "minimum": 0,
+ "default": 0
+ }
+ },
+ {
+ "name": "status",
+ "in": "query",
+ "description": "Filter by status",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "enum": ["available", "pending", "sold"]
+ }
+ },
+ {
+ "name": "tags",
+ "in": "query",
+ "description": "Filter by tags",
+ "required": false,
+ "style": "form",
+ "explode": true,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "X-Request-Id",
+ "in": "header",
+ "description": "Request correlation ID",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A list of pets",
+ "headers": {
+ "X-Total-Count": {
+ "description": "Total number of pets",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ "X-Page-Size": {
+ "description": "Number of items per page",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid parameters",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "Unexpected error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "operationId": "createPet",
+ "summary": "Create a pet",
+ "tags": ["pets"],
+ "requestBody": {
+ "description": "Pet to create",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NewPet"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Pet created",
+ "headers": {
+ "Location": {
+ "description": "URL of created pet",
+ "schema": {
+ "type": "string",
+ "format": "uri"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid input",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/pets/{petId}": {
+ "parameters": [
+ {
+ "name": "petId",
+ "in": "path",
+ "description": "Pet ID",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ ],
+ "get": {
+ "operationId": "getPet",
+ "summary": "Get a pet by ID",
+ "tags": ["pets"],
+ "responses": {
+ "200": {
+ "description": "Pet found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Pet not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "put": {
+ "operationId": "updatePet",
+ "summary": "Update a pet",
+ "tags": ["pets"],
+ "requestBody": {
+ "description": "Pet data to update",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NewPet"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Pet updated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Pet not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "operationId": "deletePet",
+ "summary": "Delete a pet",
+ "tags": ["pets"],
+ "parameters": [
+ {
+ "name": "X-Api-Key",
+ "in": "header",
+ "description": "API key for authorization",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "Pet deleted"
+ },
+ "404": {
+ "description": "Pet not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/pets/{petId}/image": {
+ "post": {
+ "operationId": "uploadPetImage",
+ "summary": "Upload pet image",
+ "tags": ["pets"],
+ "parameters": [
+ {
+ "name": "petId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ ],
+ "requestBody": {
+ "description": "Image to upload",
+ "required": true,
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "file": {
+ "type": "string",
+ "format": "binary"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "required": ["file"]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Image uploaded",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ImageUploadResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/store/orders": {
+ "post": {
+ "operationId": "placeOrder",
+ "summary": "Place an order",
+ "tags": ["store"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Order placed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid order",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/store/orders/{orderId}": {
+ "get": {
+ "operationId": "getOrder",
+ "summary": "Get order by ID",
+ "tags": ["store"],
+ "parameters": [
+ {
+ "name": "orderId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Order found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Order not found"
+ }
+ }
+ },
+ "delete": {
+ "operationId": "cancelOrder",
+ "summary": "Cancel an order",
+ "tags": ["store"],
+ "parameters": [
+ {
+ "name": "orderId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "Order cancelled"
+ },
+ "404": {
+ "description": "Order not found"
+ }
+ }
+ }
+ },
+ "/store/inventory": {
+ "get": {
+ "operationId": "getInventory",
+ "summary": "Get store inventory",
+ "tags": ["store"],
+ "responses": {
+ "200": {
+ "description": "Inventory counts by status",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users": {
+ "post": {
+ "operationId": "createUser",
+ "summary": "Create user",
+ "tags": ["users"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "User created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users/{username}": {
+ "get": {
+ "operationId": "getUser",
+ "summary": "Get user by username",
+ "tags": ["users"],
+ "parameters": [
+ {
+ "name": "username",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50,
+ "pattern": "^[a-zA-Z0-9_]+$"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "User found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "User not found"
+ }
+ }
+ },
+ "put": {
+ "operationId": "updateUser",
+ "summary": "Update user",
+ "tags": ["users"],
+ "parameters": [
+ {
+ "name": "username",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "User updated"
+ },
+ "404": {
+ "description": "User not found"
+ }
+ }
+ },
+ "delete": {
+ "operationId": "deleteUser",
+ "summary": "Delete user",
+ "tags": ["users"],
+ "parameters": [
+ {
+ "name": "username",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "User deleted"
+ },
+ "404": {
+ "description": "User not found"
+ }
+ }
+ }
+ },
+ "/users/login": {
+ "post": {
+ "operationId": "loginUser",
+ "summary": "User login",
+ "tags": ["users"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["username", "password"],
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string",
+ "format": "password"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Login successful",
+ "headers": {
+ "X-Rate-Limit": {
+ "description": "Calls per hour allowed",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ },
+ "X-Expires-After": {
+ "description": "Token expiration time",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/LoginResponse"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Invalid credentials"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Pet": {
+ "type": "object",
+ "required": ["id", "name"],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64",
+ "readOnly": true
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "category": {
+ "$ref": "#/components/schemas/Category"
+ },
+ "photoUrls": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ }
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Tag"
+ }
+ },
+ "status": {
+ "type": "string",
+ "enum": ["available", "pending", "sold"],
+ "default": "available"
+ },
+ "metadata": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "nullable": true
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "readOnly": true
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "readOnly": true
+ }
+ }
+ },
+ "NewPet": {
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "category": {
+ "$ref": "#/components/schemas/Category"
+ },
+ "photoUrls": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ }
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Tag"
+ }
+ },
+ "status": {
+ "type": "string",
+ "enum": ["available", "pending", "sold"],
+ "default": "available"
+ }
+ }
+ },
+ "Category": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "Tag": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "Order": {
+ "type": "object",
+ "required": ["petId", "quantity"],
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uuid",
+ "readOnly": true
+ },
+ "petId": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "quantity": {
+ "type": "integer",
+ "format": "int32",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "shipDate": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["placed", "approved", "delivered"],
+ "default": "placed"
+ },
+ "complete": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ },
+ "User": {
+ "type": "object",
+ "required": ["username", "email"],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64",
+ "readOnly": true
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50,
+ "pattern": "^[a-zA-Z0-9_]+$"
+ },
+ "firstName": {
+ "type": "string"
+ },
+ "lastName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "format": "email"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "writeOnly": true
+ },
+ "phone": {
+ "type": "string"
+ },
+ "userStatus": {
+ "type": "integer",
+ "format": "int32",
+ "description": "User status"
+ }
+ }
+ },
+ "LoginResponse": {
+ "type": "object",
+ "properties": {
+ "token": {
+ "type": "string"
+ },
+ "expiresIn": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ },
+ "ImageUploadResponse": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "type": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ },
+ "Error": {
+ "type": "object",
+ "required": ["code", "message"],
+ "properties": {
+ "code": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "message": {
+ "type": "string"
+ },
+ "details": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "securitySchemes": {
+ "bearerAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT"
+ },
+ "apiKey": {
+ "type": "apiKey",
+ "in": "header",
+ "name": "X-Api-Key"
+ }
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+}
\ No newline at end of file