diff --git a/Build.ps1 b/Build.ps1 index ee1dd68cfb..e3c05d0f89 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -85,14 +85,25 @@ function CreateNuGetPackage { if ([string]::IsNullOrWhitespace($versionSuffix)) { dotnet pack .\src\JsonApiDotNetCore -c Release -o .\artifacts + dotnet pack .\src\JsonApiDotNetCore.OpenApi -c Release -o .\artifacts } else { dotnet pack .\src\JsonApiDotNetCore -c Release -o .\artifacts --version-suffix=$versionSuffix + dotnet pack .\src\JsonApiDotNetCore.OpenApi -c Release -o .\artifacts --version-suffix=$versionSuffix } CheckLastExitCode } +# In a PR the base branch needs to be fetched in order for regitlint to work. +function FetchBaseBranchIfNotMaster(){ + if ($env:APPVEYOR_PULL_REQUEST_NUMBER -And $env:APPVEYOR_REPO_BRANCH -ne "master"){ + git fetch -q origin ${env:APPVEYOR_REPO_BRANCH}:${env:APPVEYOR_REPO_BRANCH} + } +} + +FetchBaseBranchIfNotMaster + dotnet tool restore CheckLastExitCode diff --git a/Directory.Build.props b/Directory.Build.props index dbd5c5ab26..baffceb9d2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,6 +4,8 @@ 5.0.* 5.0.* 5.0.* + 6.2.* + 4.2.0 $(MSBuildThisFileDirectory)CodingGuidelines.ruleset diff --git a/JsonApiDotNetCore.sln b/JsonApiDotNetCore.sln index e5e97193c2..fcb0603b0e 100644 --- a/JsonApiDotNetCore.sln +++ b/JsonApiDotNetCore.sln @@ -44,6 +44,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiDbContextTests", "test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestBuildingBlocks", "test\TestBuildingBlocks\TestBuildingBlocks.csproj", "{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApi", "src\JsonApiDotNetCore.OpenApi\JsonApiDotNetCore.OpenApi.csproj", "{71287D6F-6C3B-44B4-9FCA-E78FE3F02289}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiTests", "test\OpenApiTests\OpenApiTests.csproj", "{B693DE14-BB28-496F-AB39-B4E674ABCA80}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -210,6 +214,30 @@ Global {210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}.Release|x64.Build.0 = Release|Any CPU {210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}.Release|x86.ActiveCfg = Release|Any CPU {210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}.Release|x86.Build.0 = Release|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|x64.ActiveCfg = Debug|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|x64.Build.0 = Debug|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|x86.ActiveCfg = Debug|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|x86.Build.0 = Debug|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Release|Any CPU.Build.0 = Release|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Release|x64.ActiveCfg = Release|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Release|x64.Build.0 = Release|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Release|x86.ActiveCfg = Release|Any CPU + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Release|x86.Build.0 = Release|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Debug|x64.ActiveCfg = Debug|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Debug|x64.Build.0 = Debug|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Debug|x86.ActiveCfg = Debug|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Debug|x86.Build.0 = Debug|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|Any CPU.Build.0 = Release|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x64.ActiveCfg = Release|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x64.Build.0 = Release|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x86.ActiveCfg = Release|Any CPU + {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -228,6 +256,8 @@ Global {6CAFDDBE-00AB-4784-801B-AB419C3C3A26} = {026FBC6C-AF76-4568-9B87-EC73457899FD} {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} {210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} + {71287D6F-6C3B-44B4-9FCA-E78FE3F02289} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} + {B693DE14-BB28-496F-AB39-B4E674ABCA80} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4} diff --git a/appveyor.yml b/appveyor.yml index e5b0f781ad..f7f78a94a5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,6 +15,7 @@ environment: branches: only: - master + - openapi - develop - unstable - /release\/.+/ diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index 9be350250a..c05efa2e41 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -27,7 +27,7 @@ public class Startup { services.AddSingleton(); - IMvcCoreBuilder builder = services.AddMvcCore(); + IMvcCoreBuilder mvcBuilder = services.AddMvcCore(); services.AddJsonApi(mvcBuilder: builder); // Ensure this call is placed after the AddJsonApi call. diff --git a/docs/usage/openapi.md b/docs/usage/openapi.md new file mode 100644 index 0000000000..79156c5de7 --- /dev/null +++ b/docs/usage/openapi.md @@ -0,0 +1,72 @@ +# OpenAPI + +You can describe your API with an OpenAPI specification using the [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) integration for JsonApiDotNetCore. + +## Installation + +Install the `JsonApiDotNetCore.OpenApi` NuGet package. + +### CLI + +``` +dotnet add package JsonApiDotNetCore.OpenApi +``` + +### Visual Studio + +```powershell +Install-Package JsonApiDotNetCore.OpenApi +``` + +### *.csproj + +```xml + + + + +``` + +## Usage + +Add the integration in your `Startup` class. + +```c# +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + IMvcCoreBuilder mvcBuilder = services.AddMvcCore(); + services.AddJsonApi(mvcBuilder: mvcBuilder); + + // Adds the Swashbuckle integration. + services.AddOpenApi(mvcBuilder); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + app.UseJsonApi(); + + // Adds the Swashbuckle middleware. + app.UseSwagger(); + + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } +} +``` + +By default, the OpenAPI specification will be available at `http://localhost:/swagger/v1/swagger.json`. + +Swashbuckle also ships with [SwaggerUI](https://swagger.io/tools/swagger-ui/), tooling for a generated documentation page. This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Startup` class. + +```c# +// Startup.cs +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ + app.UseSwaggerUI(); +} +``` + +By default, SwaggerUI will be available at `http://localhost:/swagger`. + diff --git a/docs/usage/toc.md b/docs/usage/toc.md index a8b5473007..76ea777aac 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -21,6 +21,7 @@ # [Errors](errors.md) # [Metadata](meta.md) # [Caching](caching.md) +# [OpenAPI](openapi.md) # Extensibility ## [Layer Overview](extensibility/layer-overview.md) @@ -30,3 +31,4 @@ ## [Resource Repositories](extensibility/repositories.md) ## [Middleware](extensibility/middleware.md) ## [Query Strings](extensibility/query-strings.md) + diff --git a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj index 95c1faf884..8dba30b456 100644 --- a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj +++ b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj @@ -5,10 +5,12 @@ + + diff --git a/src/Examples/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs index 85740b83e1..570def00e4 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startup.cs @@ -1,6 +1,7 @@ using System; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; +using JsonApiDotNetCore.OpenApi; using JsonApiDotNetCoreExample.Data; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; @@ -44,6 +45,8 @@ public void ConfigureServices(IServiceCollection services) #endif }); + IMvcCoreBuilder mvcBuilder = services.AddMvcCore(); + using (CodeTimingSessionManager.Current.Measure("Configure JSON:API (startup)")) { services.AddJsonApi(options => @@ -57,8 +60,10 @@ public void ConfigureServices(IServiceCollection services) #if DEBUG options.IncludeExceptionStackTraceInErrors = true; #endif - }, discovery => discovery.AddCurrentAssembly()); + }, discovery => discovery.AddCurrentAssembly(), mvcBuilder: mvcBuilder); } + + services.AddOpenApi(mvcBuilder); } } @@ -82,6 +87,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment environment, app.UseJsonApi(); } + app.UseSwagger(); + app.UseSwaggerUI(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj b/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj new file mode 100644 index 0000000000..251e13bd19 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj @@ -0,0 +1,33 @@ + + + $(JsonApiDotNetCoreVersionPrefix) + $(NetCoreAppVersion) + true + + + + jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net;openapi;swagger;swaggerui;swashbuckle + A Swashbuckle integration that enables you to describe a JsonApiDotNetCore API with an OpenAPI specification. + json-api-dotnet + https://www.jsonapi.net/ + MIT + false + true + true + embedded + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs new file mode 100644 index 0000000000..a9b58bdd16 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs @@ -0,0 +1,19 @@ +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace JsonApiDotNetCore.OpenApi +{ + internal sealed class OpenApiEndpointConvention : IActionModelConvention + { + public void Apply(ActionModel action) + { + ArgumentGuard.NotNull(action, nameof(action)); + + if (!action.ActionMethod.GetCustomAttributes(true).OfType().Any()) + { + action.ApiExplorer.IsVisible = false; + } + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..e94d7458cf --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi +{ + public static class ServiceCollectionExtensions + { + /// + /// Adds the OpenAPI integration to JsonApiDotNetCore by configuring Swashbuckle. + /// + public static void AddOpenApi(this IServiceCollection services, IMvcCoreBuilder mvcBuilder, Action setupSwaggerGenAction = null) + { + ArgumentGuard.NotNull(services, nameof(services)); + ArgumentGuard.NotNull(mvcBuilder, nameof(mvcBuilder)); + + mvcBuilder.AddApiExplorer(); + + mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention())); + + services.AddSwaggerGen(setupSwaggerGenAction); + } + } +} diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 6192963587..5e1ae8a503 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@ - 4.2.0 + $(JsonApiDotNetCoreVersionPrefix) $(NetCoreAppVersion) true @@ -8,6 +8,7 @@ jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net A framework for building JSON:API compliant REST APIs using .NET Core and Entity Framework Core. Includes support for Atomic Operations. The ultimate goal of this library is to eliminate as much boilerplate as possible by offering out-of-the-box features such as sorting, filtering and pagination. You just need to focus on defining the resources and implementing your custom business logic. This library has been designed around dependency injection making extensibility incredibly easy. + json-api-dotnet https://www.jsonapi.net/ MIT false diff --git a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs index e4f127b725..2cc2ee88bc 100644 --- a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs +++ b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("JsonApiDotNetCore.OpenApi")] [assembly: InternalsVisibleTo("Benchmarks")] [assembly: InternalsVisibleTo("JsonApiDotNetCoreTests")] [assembly: InternalsVisibleTo("UnitTests")] diff --git a/test/OpenApiTests/Airplane.cs b/test/OpenApiTests/Airplane.cs new file mode 100644 index 0000000000..33a94b5013 --- /dev/null +++ b/test/OpenApiTests/Airplane.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Airplane : Identifiable + { + [Attr] + public int SeatingCapacity { get; set; } + + [Attr] + public DateTimeOffset ManufacturedAt { get; set; } + + [HasMany] + public ISet Flights { get; set; } + } +} diff --git a/test/OpenApiTests/AirplanesController.cs b/test/OpenApiTests/AirplanesController.cs new file mode 100644 index 0000000000..002f4b7b1e --- /dev/null +++ b/test/OpenApiTests/AirplanesController.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace OpenApiTests +{ + public sealed class AirplanesController : JsonApiController + { + public AirplanesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/OpenApiTests/Flight.cs b/test/OpenApiTests/Flight.cs new file mode 100644 index 0000000000..9ec0443b8e --- /dev/null +++ b/test/OpenApiTests/Flight.cs @@ -0,0 +1,17 @@ +using System; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Flight : Identifiable + { + [Attr] + public string Destination { get; set; } + + [Attr] + public DateTimeOffset DepartsAt { get; set; } + } +} diff --git a/test/OpenApiTests/FlightsController.cs b/test/OpenApiTests/FlightsController.cs new file mode 100644 index 0000000000..3b226673a2 --- /dev/null +++ b/test/OpenApiTests/FlightsController.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace OpenApiTests +{ + public sealed class FlightsController : JsonApiController + { + public FlightsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/OpenApiTests/OpenApiDbContext.cs b/test/OpenApiTests/OpenApiDbContext.cs new file mode 100644 index 0000000000..77e99aaddc --- /dev/null +++ b/test/OpenApiTests/OpenApiDbContext.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace OpenApiTests +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class OpenApiDbContext : DbContext + { + public DbSet Airplanes { get; set; } + public DbSet Flights { get; set; } + + public OpenApiDbContext(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/test/OpenApiTests/OpenApiDocumentTests.cs b/test/OpenApiTests/OpenApiDocumentTests.cs new file mode 100644 index 0000000000..898987712b --- /dev/null +++ b/test/OpenApiTests/OpenApiDocumentTests.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Reflection; +using System.Threading.Tasks; +using FluentAssertions; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests +{ + public sealed class OpenApiDocumentTests : IntegrationTestContext, OpenApiDbContext> + { + public OpenApiDocumentTests() + { + UseController(); + UseController(); + } + + [Fact] + public async Task Retrieved_document_matches_expected_document() + { + // Arrange + string embeddedResourceName = $"{nameof(OpenApiTests)}.swagger.json"; + string expectedDocument = await LoadEmbeddedResourceAsync(embeddedResourceName); + const string requestUrl = "swagger/v1/swagger.json"; + + // Act + string actualDocument = await GetAsync(requestUrl); + + // Assert + actualDocument.Should().BeJson(expectedDocument); + } + + private async Task GetAsync(string requestUrl) + { + var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + + using HttpClient client = Factory.CreateClient(); + HttpResponseMessage responseMessage = await client.SendAsync(request); + + return await responseMessage.Content.ReadAsStringAsync(); + } + + private static async Task LoadEmbeddedResourceAsync(string name) + { + var assembly = Assembly.GetExecutingAssembly(); + await using Stream stream = assembly.GetManifestResourceStream(name); + + if (stream == null) + { + throw new Exception($"Failed to load embedded resource '{name}'. Set Build Action to Embedded Resource in properties."); + } + + using var reader = new StreamReader(stream); + return await reader.ReadToEndAsync(); + } + } +} diff --git a/test/OpenApiTests/OpenApiStartup.cs b/test/OpenApiTests/OpenApiStartup.cs new file mode 100644 index 0000000000..88f956a0f6 --- /dev/null +++ b/test/OpenApiTests/OpenApiStartup.cs @@ -0,0 +1,32 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.OpenApi; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TestBuildingBlocks; + +namespace OpenApiTests +{ + public sealed class OpenApiStartup : TestableStartup + where TDbContext : DbContext + { + public override void ConfigureServices(IServiceCollection services) + { + IMvcCoreBuilder mvcBuilder = services.AddMvcCore(); + + services.AddJsonApi(SetJsonApiOptions, mvcBuilder: mvcBuilder); + + services.AddOpenApi(mvcBuilder); + } + + public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) + { + app.UseRouting(); + app.UseJsonApi(); + app.UseSwagger(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + } +} diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj new file mode 100644 index 0000000000..c4d01aadcf --- /dev/null +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -0,0 +1,27 @@ + + + $(NetCoreAppVersion) + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenApiTests/swagger.json b/test/OpenApiTests/swagger.json new file mode 100644 index 0000000000..e90093c24d --- /dev/null +++ b/test/OpenApiTests/swagger.json @@ -0,0 +1,963 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "paths": { + "/airplanes": { + "get": { + "tags": [ + "Airplanes" + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "head": { + "tags": [ + "Airplanes" + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "post": { + "tags": [ + "Airplanes" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Airplane" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Airplane" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Airplane" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/airplanes/{id}": { + "get": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "head": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "patch": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Airplane" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Airplane" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Airplane" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/airplanes/{id}/{relationshipName}": { + "get": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "head": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/airplanes/{id}/relationships/{relationshipName}": { + "get": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "head": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "post": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + }, + "text/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + }, + "application/*+json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "patch": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {} + }, + "text/json": { + "schema": {} + }, + "application/*+json": { + "schema": {} + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "Airplanes" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + }, + "text/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + }, + "application/*+json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/flights": { + "get": { + "tags": [ + "Flights" + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "head": { + "tags": [ + "Flights" + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "post": { + "tags": [ + "Flights" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Flight" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Flight" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Flight" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/flights/{id}": { + "get": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "head": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "patch": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Flight" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Flight" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Flight" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/flights/{id}/{relationshipName}": { + "get": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "head": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/flights/{id}/relationships/{relationshipName}": { + "get": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "head": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "post": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + }, + "text/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + }, + "application/*+json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "patch": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {} + }, + "text/json": { + "schema": {} + }, + "application/*+json": { + "schema": {} + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "Flights" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "relationshipName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + }, + "text/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + }, + "application/*+json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIdentifiable" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "Airplane": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "stringId": { + "type": "string", + "nullable": true + }, + "localId": { + "type": "string", + "nullable": true + }, + "seatingCapacity": { + "type": "integer", + "format": "int32" + }, + "manufacturedAt": { + "type": "string", + "format": "date-time" + }, + "flights": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/Flight" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "Flight": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "stringId": { + "type": "string", + "nullable": true + }, + "localId": { + "type": "string", + "nullable": true + }, + "destination": { + "type": "string", + "nullable": true + }, + "departsAt": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, + "IIdentifiable": { + "type": "object", + "properties": { + "stringId": { + "type": "string", + "nullable": true + }, + "localId": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +}