diff --git a/test/OpenApiNSwagEndToEndTests/ClientIdGenerationModes/ClientIdGenerationModesTests.cs b/test/OpenApiNSwagEndToEndTests/ClientIdGenerationModes/ClientIdGenerationModesTests.cs new file mode 100644 index 0000000000..83a20f1d0f --- /dev/null +++ b/test/OpenApiNSwagEndToEndTests/ClientIdGenerationModes/ClientIdGenerationModesTests.cs @@ -0,0 +1,194 @@ +using FluentAssertions; +using FluentAssertions.Specialized; +using JsonApiDotNetCore.OpenApi.Client.NSwag; +using Newtonsoft.Json; +using OpenApiNSwagEndToEndTests.ClientIdGenerationModes.GeneratedCode; +using OpenApiTests; +using OpenApiTests.ClientIdGenerationModes; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiNSwagEndToEndTests.ClientIdGenerationModes; + +public sealed class ClientIdGenerationModesTests + : IClassFixture, ClientIdGenerationModesDbContext>> +{ + private readonly IntegrationTestContext, ClientIdGenerationModesDbContext> _testContext; + private readonly ClientIdGenerationModesFakers _fakers = new(); + + public ClientIdGenerationModesTests(IntegrationTestContext, ClientIdGenerationModesDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.UseController(); + testContext.UseController(); + } + + [Fact] + public async Task Cannot_create_resource_without_ID_when_supplying_ID_is_required() + { + // Arrange + Player player = _fakers.Player.Generate(); + + using HttpClient httpClient = _testContext.Factory.CreateClient(); + ClientIdGenerationModesClient apiClient = new(httpClient); + + // Act + Func> action = () => ApiResponse.TranslateAsync(() => apiClient.PostPlayerAsync(null, new PlayerPostRequestDocument + { + Data = new PlayerDataInPostRequest + { + Id = null!, + Attributes = new PlayerAttributesInPostRequest + { + UserName = player.UserName + } + } + })); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + assertion.Which.Message.Should().Be("Cannot write a null value for property 'id'. Property requires a value. Path 'data'."); + } + + [Fact] + public async Task Can_create_resource_with_ID_when_supplying_ID_is_required() + { + // Arrange + Player player = _fakers.Player.Generate(); + player.Id = Guid.NewGuid(); + + using HttpClient httpClient = _testContext.Factory.CreateClient(); + ClientIdGenerationModesClient apiClient = new(httpClient); + + // Act + PlayerPrimaryResponseDocument? document = await ApiResponse.TranslateAsync(() => apiClient.PostPlayerAsync(null, new PlayerPostRequestDocument + { + Data = new PlayerDataInPostRequest + { + Id = player.StringId!, + Attributes = new PlayerAttributesInPostRequest + { + UserName = player.UserName + } + } + })); + + // Assert + document.Should().BeNull(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Player playerInDatabase = await dbContext.Players.FirstWithIdAsync(player.Id); + + playerInDatabase.UserName.Should().Be(player.UserName); + }); + } + + [Fact] + public async Task Can_create_resource_without_ID_when_supplying_ID_is_allowed() + { + // Arrange + Game game = _fakers.Game.Generate(); + + using HttpClient httpClient = _testContext.Factory.CreateClient(); + ClientIdGenerationModesClient apiClient = new(httpClient); + + // Act + GamePrimaryResponseDocument? document = await ApiResponse.TranslateAsync(() => apiClient.PostGameAsync(null, new GamePostRequestDocument + { + Data = new GameDataInPostRequest + { + Id = null!, + Attributes = new GameAttributesInPostRequest + { + Title = game.Title, + PurchasePrice = (double)game.PurchasePrice + } + } + })); + + // Assert + document.ShouldNotBeNull(); + document.Data.Id.Should().NotBeNullOrEmpty(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Game gameInDatabase = await dbContext.Games.FirstWithIdAsync(Guid.Parse(document.Data.Id)); + + gameInDatabase.Title.Should().Be(game.Title); + gameInDatabase.PurchasePrice.Should().Be(game.PurchasePrice); + }); + } + + [Fact] + public async Task Can_create_resource_with_ID_when_supplying_ID_is_allowed() + { + // Arrange + Game game = _fakers.Game.Generate(); + game.Id = Guid.NewGuid(); + + using HttpClient httpClient = _testContext.Factory.CreateClient(); + ClientIdGenerationModesClient apiClient = new(httpClient); + + // Act + GamePrimaryResponseDocument? document = await ApiResponse.TranslateAsync(() => apiClient.PostGameAsync(null, new GamePostRequestDocument + { + Data = new GameDataInPostRequest + { + Id = game.StringId!, + Attributes = new GameAttributesInPostRequest + { + Title = game.Title, + PurchasePrice = (double)game.PurchasePrice + } + } + })); + + // Assert + document.Should().BeNull(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Game gameInDatabase = await dbContext.Games.FirstWithIdAsync(game.Id); + + gameInDatabase.Title.Should().Be(game.Title); + gameInDatabase.PurchasePrice.Should().Be(game.PurchasePrice); + }); + } + + [Fact] + public async Task Can_create_resource_without_ID_when_supplying_ID_is_forbidden() + { + // Arrange + PlayerGroup playerGroup = _fakers.Group.Generate(); + + using HttpClient httpClient = _testContext.Factory.CreateClient(); + ClientIdGenerationModesClient apiClient = new(httpClient); + + // Act + PlayerGroupPrimaryResponseDocument? document = await ApiResponse.TranslateAsync(() => apiClient.PostPlayerGroupAsync(null, + new PlayerGroupPostRequestDocument + { + Data = new PlayerGroupDataInPostRequest + { + Attributes = new PlayerGroupAttributesInPostRequest + { + Name = playerGroup.Name + } + } + })); + + // Assert + document.ShouldNotBeNull(); + document.Data.Id.Should().NotBeNullOrEmpty(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + PlayerGroup playerGroupInDatabase = await dbContext.PlayerGroups.FirstWithIdAsync(long.Parse(document.Data.Id)); + + playerGroupInDatabase.Name.Should().Be(playerGroup.Name); + }); + } +} diff --git a/test/OpenApiNSwagEndToEndTests/OpenApiNSwagEndToEndTests.csproj b/test/OpenApiNSwagEndToEndTests/OpenApiNSwagEndToEndTests.csproj index a069a30eb2..c70d144e98 100644 --- a/test/OpenApiNSwagEndToEndTests/OpenApiNSwagEndToEndTests.csproj +++ b/test/OpenApiNSwagEndToEndTests/OpenApiNSwagEndToEndTests.csproj @@ -23,6 +23,13 @@ + + OpenApiNSwagEndToEndTests.ClientIdGenerationModes.GeneratedCode + ClientIdGenerationModesClient + ClientIdGenerationModesClient.cs + NSwagCSharp + /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.NSwag /GenerateNullableReferenceTypes:true + OpenApiNSwagEndToEndTests.Headers.GeneratedCode HeadersClient diff --git a/test/OpenApiTests/ClientIdGenerationModes/ClientIdGenerationModesDbContext.cs b/test/OpenApiTests/ClientIdGenerationModes/ClientIdGenerationModesDbContext.cs new file mode 100644 index 0000000000..f346f95bcf --- /dev/null +++ b/test/OpenApiTests/ClientIdGenerationModes/ClientIdGenerationModesDbContext.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; + +namespace OpenApiTests.ClientIdGenerationModes; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed class ClientIdGenerationModesDbContext(DbContextOptions options) : TestableDbContext(options) +{ + public DbSet Players => Set(); + public DbSet Games => Set(); + public DbSet PlayerGroups => Set(); +} diff --git a/test/OpenApiTests/ClientIdGenerationModes/ClientIdGenerationModesFakers.cs b/test/OpenApiTests/ClientIdGenerationModes/ClientIdGenerationModesFakers.cs new file mode 100644 index 0000000000..83c08c8a59 --- /dev/null +++ b/test/OpenApiTests/ClientIdGenerationModes/ClientIdGenerationModesFakers.cs @@ -0,0 +1,29 @@ +using Bogus; +using JetBrains.Annotations; +using TestBuildingBlocks; + +// @formatter:wrap_chained_method_calls chop_if_long +// @formatter:wrap_before_first_method_call true + +namespace OpenApiTests.ClientIdGenerationModes; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed class ClientIdGenerationModesFakers : FakerContainer +{ + private readonly Lazy> _lazyPlayerFaker = new(() => new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(player => player.UserName, faker => faker.Person.UserName)); + + private readonly Lazy> _lazyGameFaker = new(() => new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(game => game.Title, faker => faker.Commerce.ProductName()) + .RuleFor(game => game.PurchasePrice, faker => faker.Finance.Amount(1, 80))); + + private readonly Lazy> _lazyGroupFaker = new(() => new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(playerGroup => playerGroup.Name, faker => faker.Person.Company.Name)); + + public Faker Player => _lazyPlayerFaker.Value; + public Faker Game => _lazyGameFaker.Value; + public Faker Group => _lazyGroupFaker.Value; +} diff --git a/test/OpenApiTests/ClientIdGenerationModes/ClientIdGenerationModesTests.cs b/test/OpenApiTests/ClientIdGenerationModes/ClientIdGenerationModesTests.cs new file mode 100644 index 0000000000..193455be3c --- /dev/null +++ b/test/OpenApiTests/ClientIdGenerationModes/ClientIdGenerationModesTests.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ClientIdGenerationModes; + +public sealed class ClientIdGenerationModesTests + : IClassFixture, ClientIdGenerationModesDbContext>> +{ + private readonly OpenApiTestContext, ClientIdGenerationModesDbContext> _testContext; + + public ClientIdGenerationModesTests(OpenApiTestContext, ClientIdGenerationModesDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.UseController(); + testContext.UseController(); + + testContext.SwaggerDocumentOutputDirectory = $"{GetType().Namespace!.Replace('.', '/')}/GeneratedSwagger"; + } + + [Fact] + public async Task Schema_property_for_ID_is_required_in_post_request() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.playerDataInPostRequest").With(dataElement => + { + dataElement.Should().ContainPath("required").With(requiredElement => + { + requiredElement.Should().ContainArrayElement("id"); + }); + + dataElement.Should().ContainPath("properties.id"); + }); + } + + [Fact] + public async Task Schema_property_for_ID_is_optional_in_post_request() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.gameDataInPostRequest").With(dataElement => + { + dataElement.Should().ContainPath("required").With(requiredElement => + { + requiredElement.Should().NotContainArrayElement("id"); + }); + + dataElement.Should().ContainPath("properties.id"); + }); + } + + [Fact] + public async Task Schema_property_for_ID_is_omitted_in_post_request() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.playerGroupDataInPostRequest").With(dataElement => + { + dataElement.Should().ContainPath("required").With(requiredElement => + { + requiredElement.Should().NotContainArrayElement("id"); + }); + + dataElement.Should().NotContainPath("properties.id"); + }); + } +} diff --git a/test/OpenApiTests/ClientIdGenerationModes/Game.cs b/test/OpenApiTests/ClientIdGenerationModes/Game.cs new file mode 100644 index 0000000000..c4880054c1 --- /dev/null +++ b/test/OpenApiTests/ClientIdGenerationModes/Game.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.ClientIdGenerationModes; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.ClientIdGenerationModes", ClientIdGeneration = ClientIdGenerationMode.Allowed, + GenerateControllerEndpoints = JsonApiEndpoints.Post)] +public sealed class Game : Identifiable +{ + [Attr] + public string Title { get; set; } = null!; + + [Attr] + public decimal PurchasePrice { get; set; } +} diff --git a/test/OpenApiTests/ClientIdGenerationModes/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/ClientIdGenerationModes/GeneratedSwagger/swagger.g.json new file mode 100644 index 0000000000..9583a743b6 --- /dev/null +++ b/test/OpenApiTests/ClientIdGenerationModes/GeneratedSwagger/swagger.g.json @@ -0,0 +1,1182 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost" + } + ], + "paths": { + "/games": { + "post": { + "tags": [ + "games" + ], + "summary": "Creates a new game.", + "operationId": "postGame", + "parameters": [ + { + "name": "query", + "in": "query", + "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "example": "" + } + } + ], + "requestBody": { + "description": "The attributes and relationships of the game to create.", + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/gamePostRequestDocument" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "The game was successfully created, which resulted in additional changes. The newly created game is returned.", + "headers": { + "Location": { + "description": "The URL at which the newly created game can be retrieved.", + "required": true, + "schema": { + "type": "string", + "format": "uri" + } + } + }, + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/gamePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "The game was successfully created, which did not result in additional changes." + }, + "400": { + "description": "The query string is invalid or the request body is missing or malformed.", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + }, + "409": { + "description": "A resource type in the request body is incompatible.", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + }, + "422": { + "description": "Validation of the request body failed.", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + } + } + } + }, + "/playerGroups": { + "post": { + "tags": [ + "playerGroups" + ], + "summary": "Creates a new playerGroup.", + "operationId": "postPlayerGroup", + "parameters": [ + { + "name": "query", + "in": "query", + "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "example": "" + } + } + ], + "requestBody": { + "description": "The attributes and relationships of the playerGroup to create.", + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/playerGroupPostRequestDocument" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "The playerGroup was successfully created, which resulted in additional changes. The newly created playerGroup is returned.", + "headers": { + "Location": { + "description": "The URL at which the newly created playerGroup can be retrieved.", + "required": true, + "schema": { + "type": "string", + "format": "uri" + } + } + }, + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/playerGroupPrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "The playerGroup was successfully created, which did not result in additional changes." + }, + "400": { + "description": "The query string is invalid or the request body is missing or malformed.", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + }, + "403": { + "description": "Client-generated IDs cannot be used at this endpoint.", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + }, + "409": { + "description": "A resource type in the request body is incompatible.", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + }, + "422": { + "description": "Validation of the request body failed.", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + } + } + } + }, + "/players": { + "post": { + "tags": [ + "players" + ], + "summary": "Creates a new player.", + "operationId": "postPlayer", + "parameters": [ + { + "name": "query", + "in": "query", + "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "example": "" + } + } + ], + "requestBody": { + "description": "The attributes and relationships of the player to create.", + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/playerPostRequestDocument" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "The player was successfully created, which resulted in additional changes. The newly created player is returned.", + "headers": { + "Location": { + "description": "The URL at which the newly created player can be retrieved.", + "required": true, + "schema": { + "type": "string", + "format": "uri" + } + } + }, + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/playerPrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "The player was successfully created, which did not result in additional changes." + }, + "400": { + "description": "The query string is invalid or the request body is missing or malformed.", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + }, + "409": { + "description": "A resource type in the request body is incompatible.", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + }, + "422": { + "description": "Validation of the request body failed.", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "dataInResponse": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "minLength": 1, + "type": "string" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "type", + "mapping": { + "games": "#/components/schemas/gameDataInResponse", + "playerGroups": "#/components/schemas/playerGroupDataInResponse", + "players": "#/components/schemas/playerDataInResponse" + } + }, + "x-abstract": true + }, + "errorLinks": { + "type": "object", + "properties": { + "about": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "errorObject": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/errorLinks" + } + ], + "nullable": true + }, + "status": { + "type": "string" + }, + "code": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "source": { + "allOf": [ + { + "$ref": "#/components/schemas/errorSource" + } + ], + "nullable": true + }, + "meta": { + "type": "object", + "additionalProperties": { }, + "nullable": true + } + }, + "additionalProperties": false + }, + "errorResponseDocument": { + "required": [ + "errors" + ], + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/errorObject" + } + } + }, + "additionalProperties": false + }, + "errorSource": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "nullable": true + }, + "parameter": { + "type": "string", + "nullable": true + }, + "header": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "gameAttributesInPostRequest": { + "required": [ + "title" + ], + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "purchasePrice": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "gameAttributesInResponse": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "purchasePrice": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "gameDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/gameResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/gameAttributesInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "gameDataInResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/dataInResponse" + }, + { + "required": [ + "links" + ], + "type": "object", + "properties": { + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/gameAttributesInResponse" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceData" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } + } + }, + "additionalProperties": false + } + ], + "additionalProperties": false + }, + "gameIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/gameResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "gamePostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/gameDataInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "gamePrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + }, + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/gameDataInResponse" + } + ] + }, + "included": { + "type": "array", + "items": { + "$ref": "#/components/schemas/dataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } + } + }, + "additionalProperties": false + }, + "gameResourceType": { + "enum": [ + "games" + ], + "type": "string", + "additionalProperties": false + }, + "linksInRelationship": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceData": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, + "playerAttributesInPostRequest": { + "required": [ + "userName" + ], + "type": "object", + "properties": { + "userName": { + "type": "string" + } + }, + "additionalProperties": false + }, + "playerAttributesInResponse": { + "type": "object", + "properties": { + "userName": { + "type": "string" + } + }, + "additionalProperties": false + }, + "playerDataInPostRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/playerResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/playerAttributesInPostRequest" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/playerRelationshipsInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "playerDataInResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/dataInResponse" + }, + { + "required": [ + "links" + ], + "type": "object", + "properties": { + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/playerAttributesInResponse" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/playerRelationshipsInResponse" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceData" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } + } + }, + "additionalProperties": false + } + ], + "additionalProperties": false + }, + "playerGroupAttributesInPostRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "playerGroupAttributesInResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "playerGroupDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/playerGroupResourceType" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/playerGroupAttributesInPostRequest" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/playerGroupRelationshipsInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "playerGroupDataInResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/dataInResponse" + }, + { + "required": [ + "links" + ], + "type": "object", + "properties": { + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/playerGroupAttributesInResponse" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/playerGroupRelationshipsInResponse" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceData" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } + } + }, + "additionalProperties": false + } + ], + "additionalProperties": false + }, + "playerGroupIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/playerGroupResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "playerGroupPostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/playerGroupDataInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "playerGroupPrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + }, + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/playerGroupDataInResponse" + } + ] + }, + "included": { + "type": "array", + "items": { + "$ref": "#/components/schemas/dataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } + } + }, + "additionalProperties": false + }, + "playerGroupRelationshipsInPostRequest": { + "type": "object", + "properties": { + "players": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyPlayerInRequest" + } + ] + } + }, + "additionalProperties": false + }, + "playerGroupRelationshipsInResponse": { + "type": "object", + "properties": { + "players": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyPlayerInResponse" + } + ] + } + }, + "additionalProperties": false + }, + "playerGroupResourceType": { + "enum": [ + "playerGroups" + ], + "type": "string", + "additionalProperties": false + }, + "playerIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/playerResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "playerPostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/playerDataInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "playerPrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + }, + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/playerDataInResponse" + } + ] + }, + "included": { + "type": "array", + "items": { + "$ref": "#/components/schemas/dataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } + } + }, + "additionalProperties": false + }, + "playerRelationshipsInPostRequest": { + "type": "object", + "properties": { + "ownedGames": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyGameInRequest" + } + ] + }, + "memberOf": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyPlayerGroupInRequest" + } + ] + } + }, + "additionalProperties": false + }, + "playerRelationshipsInResponse": { + "type": "object", + "properties": { + "ownedGames": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyGameInResponse" + } + ] + }, + "memberOf": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyPlayerGroupInResponse" + } + ] + } + }, + "additionalProperties": false + }, + "playerResourceType": { + "enum": [ + "players" + ], + "type": "string", + "additionalProperties": false + }, + "toManyGameInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/gameIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyGameInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationship" + } + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/gameIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } + } + }, + "additionalProperties": false + }, + "toManyPlayerGroupInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/playerGroupIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyPlayerGroupInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationship" + } + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/playerGroupIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } + } + }, + "additionalProperties": false + }, + "toManyPlayerInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/playerIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyPlayerInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationship" + } + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/playerIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/OpenApiTests/ClientIdGenerationModes/Player.cs b/test/OpenApiTests/ClientIdGenerationModes/Player.cs new file mode 100644 index 0000000000..e150af5756 --- /dev/null +++ b/test/OpenApiTests/ClientIdGenerationModes/Player.cs @@ -0,0 +1,22 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.ClientIdGenerationModes; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.ClientIdGenerationModes", ClientIdGeneration = ClientIdGenerationMode.Required, + GenerateControllerEndpoints = JsonApiEndpoints.Post)] +public sealed class Player : Identifiable +{ + [Attr] + public string UserName { get; set; } = null!; + + [HasMany] + public List OwnedGames { get; set; } = []; + + [HasMany] + public List MemberOf { get; set; } = []; +} diff --git a/test/OpenApiTests/ClientIdGenerationModes/PlayerGroup.cs b/test/OpenApiTests/ClientIdGenerationModes/PlayerGroup.cs new file mode 100644 index 0000000000..9f2547a61f --- /dev/null +++ b/test/OpenApiTests/ClientIdGenerationModes/PlayerGroup.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.ClientIdGenerationModes; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.ClientIdGenerationModes", ClientIdGeneration = ClientIdGenerationMode.Forbidden, + GenerateControllerEndpoints = JsonApiEndpoints.Post)] +public sealed class PlayerGroup : Identifiable +{ + [Attr] + public string Name { get; set; } = null!; + + [HasMany] + public List Players { get; set; } = []; +}