diff --git a/Directory.Build.props b/Directory.Build.props index 43158313fe..692c40088d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,16 +12,16 @@ - + - - $(NoWarn);1591 - true - true - + + $(NoWarn);1591 + true + true + @@ -31,5 +31,6 @@ 4.16.1 2.4.* 17.0.0 + 1.0.* diff --git a/src/JsonApiDotNetCore.OpenApi.Client/ApiException.cs b/src/JsonApiDotNetCore.OpenApi.Client/ApiException.cs new file mode 100644 index 0000000000..2a8e310ab3 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client/ApiException.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +// ReSharper disable once CheckNamespace +namespace JsonApiDotNetCore.OpenApi.Client.Exceptions +{ + // We cannot rely on a generated ApiException as soon as we are generating multiple clients, see https://github.com/RicoSuter/NSwag/issues/2839#issuecomment-776647377. + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal class ApiException : Exception + { + public int StatusCode { get; } + + public string Response { get; } + + public IReadOnlyDictionary> Headers { get; } + + public ApiException(string message, int statusCode, string response, IReadOnlyDictionary> headers, Exception innerException) + : base( + message + "\n\nStatus: " + statusCode + "\nResponse: \n" + + (response == null ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException) + { + StatusCode = statusCode; + Response = response; + Headers = headers; + } + + public override string ToString() + { + return $"HTTP Response: \n\n{Response}\n\n{base.ToString()}"; + } + } + + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class ApiException : ApiException + { + public TResult Result { get; } + + public ApiException(string message, int statusCode, string response, IReadOnlyDictionary> headers, TResult result, + Exception innerException) + : base(message, statusCode, response, headers, innerException) + { + Result = result; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs index a4c9f5575d..d15fa61fd4 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using Humanizer; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; @@ -13,42 +14,41 @@ internal sealed class JsonApiSchemaIdSelector { private static readonly IDictionary OpenTypeToSchemaTemplateMap = new Dictionary { - [typeof(ResourcePostRequestDocument<>)] = "###-post-request-document", - [typeof(ResourcePatchRequestDocument<>)] = "###-patch-request-document", - [typeof(ResourceObjectInPostRequest<>)] = "###-data-in-post-request", - [typeof(AttributesInPostRequest<>)] = "###-attributes-in-post-request", - [typeof(RelationshipsInPostRequest<>)] = "###-relationships-in-post-request", - [typeof(ResourceObjectInPatchRequest<>)] = "###-data-in-patch-request", - [typeof(AttributesInPatchRequest<>)] = "###-attributes-in-patch-request", - [typeof(RelationshipsInPatchRequest<>)] = "###-relationships-in-patch-request", - [typeof(ToOneRelationshipInRequest<>)] = "to-one-###-in-request", - [typeof(NullableToOneRelationshipInRequest<>)] = "nullable-to-one-###-in-request", - [typeof(ToManyRelationshipInRequest<>)] = "to-many-###-in-request", - [typeof(PrimaryResourceResponseDocument<>)] = "###-primary-response-document", - [typeof(SecondaryResourceResponseDocument<>)] = "###-secondary-response-document", - [typeof(NullableSecondaryResourceResponseDocument<>)] = "nullable-###-secondary-response-document", - [typeof(ResourceCollectionResponseDocument<>)] = "###-collection-response-document", - [typeof(ResourceIdentifierResponseDocument<>)] = "###-identifier-response-document", - [typeof(NullableResourceIdentifierResponseDocument<>)] = "nullable-###-identifier-response-document", - [typeof(ResourceIdentifierCollectionResponseDocument<>)] = "###-identifier-collection-response-document", - [typeof(ToOneRelationshipInResponse<>)] = "to-one-###-in-response", - [typeof(NullableToOneRelationshipInResponse<>)] = "nullable-to-one-###-in-response", - [typeof(ToManyRelationshipInResponse<>)] = "to-many-###-in-response", - [typeof(ResourceObjectInResponse<>)] = "###-data-in-response", - [typeof(AttributesInResponse<>)] = "###-attributes-in-response", - [typeof(RelationshipsInResponse<>)] = "###-relationships-in-response", - [typeof(ResourceIdentifierObject<>)] = "###-identifier" + [typeof(ResourcePostRequestDocument<>)] = "[ResourceName] Post Request Document", + [typeof(ResourcePatchRequestDocument<>)] = "[ResourceName] Patch Request Document", + [typeof(ResourceObjectInPostRequest<>)] = "[ResourceName] Data In Post Request", + [typeof(AttributesInPostRequest<>)] = "[ResourceName] Attributes In Post Request", + [typeof(RelationshipsInPostRequest<>)] = "[ResourceName] Relationships In Post Request", + [typeof(ResourceObjectInPatchRequest<>)] = "[ResourceName] Data In Patch Request", + [typeof(AttributesInPatchRequest<>)] = "[ResourceName] Attributes In Patch Request", + [typeof(RelationshipsInPatchRequest<>)] = "[ResourceName] Relationships In Patch Request", + [typeof(ToOneRelationshipInRequest<>)] = "To One [ResourceName] In Request", + [typeof(NullableToOneRelationshipInRequest<>)] = "Nullable To One [ResourceName] In Request", + [typeof(ToManyRelationshipInRequest<>)] = "To Many [ResourceName] In Request", + [typeof(PrimaryResourceResponseDocument<>)] = "[ResourceName] Primary Response Document", + [typeof(SecondaryResourceResponseDocument<>)] = "[ResourceName] Secondary Response Document", + [typeof(NullableSecondaryResourceResponseDocument<>)] = "Nullable [ResourceName] Secondary Response Document", + [typeof(ResourceCollectionResponseDocument<>)] = "[ResourceName] Collection Response Document", + [typeof(ResourceIdentifierResponseDocument<>)] = "[ResourceName] Identifier Response Document", + [typeof(NullableResourceIdentifierResponseDocument<>)] = "Nullable [ResourceName] Identifier Response Document", + [typeof(ResourceIdentifierCollectionResponseDocument<>)] = "[ResourceName] Identifier Collection Response Document", + [typeof(ToOneRelationshipInResponse<>)] = "To One [ResourceName] In Response", + [typeof(NullableToOneRelationshipInResponse<>)] = "Nullable To One [ResourceName] In Response", + [typeof(ToManyRelationshipInResponse<>)] = "To Many [ResourceName] In Response", + [typeof(ResourceObjectInResponse<>)] = "[ResourceName] Data In Response", + [typeof(AttributesInResponse<>)] = "[ResourceName] Attributes In Response", + [typeof(RelationshipsInResponse<>)] = "[ResourceName] Relationships In Response", + [typeof(ResourceIdentifierObject<>)] = "[ResourceName] Identifier" }; - private readonly ResourceNameFormatter _formatter; + private readonly JsonNamingPolicy? _namingPolicy; private readonly IResourceGraph _resourceGraph; - public JsonApiSchemaIdSelector(ResourceNameFormatter formatter, IResourceGraph resourceGraph) + public JsonApiSchemaIdSelector(JsonNamingPolicy? namingPolicy, IResourceGraph resourceGraph) { - ArgumentGuard.NotNull(formatter, nameof(formatter)); ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - _formatter = formatter; + _namingPolicy = namingPolicy; _resourceGraph = resourceGraph; } @@ -65,15 +65,24 @@ public string GetSchemaId(Type type) if (type.IsConstructedGenericType && OpenTypeToSchemaTemplateMap.ContainsKey(type.GetGenericTypeDefinition())) { + string pascalCaseSchemaIdTemplate = OpenTypeToSchemaTemplateMap[type.GetGenericTypeDefinition()]; Type resourceClrType = type.GetGenericArguments().First(); - string resourceName = _formatter.FormatResourceName(resourceClrType).Singularize(); - string template = OpenTypeToSchemaTemplateMap[type.GetGenericTypeDefinition()]; - return template.Replace("###", resourceName); + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + + string pascalCaseSchemaId = pascalCaseSchemaIdTemplate + .Replace("[ResourceName]", resourceClrType.Name) + .Replace(" ", ""); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + + return _namingPolicy != null ? _namingPolicy.ConvertName(pascalCaseSchemaId) : pascalCaseSchemaId; } // Used for a fixed set of types, such as jsonapi-object, links-in-many-resource-document etc. - return _formatter.FormatResourceName(type).Singularize(); + return _namingPolicy != null ? _namingPolicy.ConvertName(type.Name) : type.Name; } } } diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index 8f2a781fbd..287b656142 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -59,7 +59,6 @@ private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection var resourceGraph = scope.ServiceProvider.GetRequiredService(); var jsonApiOptions = scope.ServiceProvider.GetRequiredService(); JsonNamingPolicy? namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy; - ResourceNameFormatter resourceNameFormatter = new(namingPolicy); AddSchemaGenerator(services); @@ -67,7 +66,7 @@ private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection { swaggerGenOptions.SupportNonNullableReferenceTypes(); SetOperationInfo(swaggerGenOptions, controllerResourceMapping, namingPolicy); - SetSchemaIdSelector(swaggerGenOptions, resourceGraph, resourceNameFormatter); + SetSchemaIdSelector(swaggerGenOptions, resourceGraph, namingPolicy); swaggerGenOptions.DocumentFilter(); setupSwaggerGenAction?.Invoke(swaggerGenOptions); @@ -105,9 +104,9 @@ private static IList GetOperationTags(ApiDescription description, IContr }; } - private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceGraph resourceGraph, ResourceNameFormatter resourceNameFormatter) + private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceGraph resourceGraph, JsonNamingPolicy? namingPolicy) { - JsonApiSchemaIdSelector jsonApiObjectSchemaSelector = new(resourceNameFormatter, resourceGraph); + JsonApiSchemaIdSelector jsonApiObjectSchemaSelector = new(namingPolicy, resourceGraph); swaggerGenOptions.CustomSchemaIds(type => jsonApiObjectSchemaSelector.GetSchemaId(type)); } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index 7d47d180d5..d3ff41d441 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -40,7 +40,7 @@ public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceG ArgumentGuard.NotNull(options, nameof(options)); _defaultSchemaGenerator = defaultSchemaGenerator; - _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(_schemaRepositoryAccessor); + _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(_schemaRepositoryAccessor, options.SerializerOptions.PropertyNamingPolicy); _resourceObjectSchemaGenerator = new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, options, _schemaRepositoryAccessor); } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs index f14e585bb8..ab1c3f651c 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs @@ -1,22 +1,27 @@ using System; using System.Collections.Generic; +using System.Text.Json; using Microsoft.OpenApi.Models; namespace JsonApiDotNetCore.OpenApi.SwaggerComponents { internal sealed class NullableReferenceSchemaGenerator { + private const string PascalCaseNullableSchemaReferenceId = "NullValue"; + private readonly string _nullableSchemaReferenceId; + private static readonly NullableReferenceSchemaStrategy NullableReferenceStrategy = Enum.Parse(NullableReferenceSchemaStrategy.Implicit.ToString()); - private static OpenApiSchema? _referenceSchemaForNullValue; + private static OpenApiSchema? _referenceSchemaForExplicitNullValue; private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; - public NullableReferenceSchemaGenerator(ISchemaRepositoryAccessor schemaRepositoryAccessor) + public NullableReferenceSchemaGenerator(ISchemaRepositoryAccessor schemaRepositoryAccessor, JsonNamingPolicy? namingPolicy) { ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); _schemaRepositoryAccessor = schemaRepositoryAccessor; + _nullableSchemaReferenceId = namingPolicy != null ? namingPolicy.ConvertName(PascalCaseNullableSchemaReferenceId) : PascalCaseNullableSchemaReferenceId; } public OpenApiSchema GenerateSchema(OpenApiSchema referenceSchema) @@ -28,20 +33,13 @@ public OpenApiSchema GenerateSchema(OpenApiSchema referenceSchema) OneOf = new List { referenceSchema, - GetNullableReferenceSchema() + NullableReferenceStrategy == NullableReferenceSchemaStrategy.Explicit ? GetExplicitNullSchema() : GetImplicitNullSchema() } }; } - private OpenApiSchema GetNullableReferenceSchema() - { - return NullableReferenceStrategy == NullableReferenceSchemaStrategy.Explicit - ? GetNullableReferenceSchemaUsingExplicitNullType() - : GetNullableReferenceSchemaUsingImplicitNullType(); - } - // This approach is supported in OAS starting from v3.1. See https://github.com/OAI/OpenAPI-Specification/issues/1368#issuecomment-580103688 - private static OpenApiSchema GetNullableReferenceSchemaUsingExplicitNullType() + private static OpenApiSchema GetExplicitNullSchema() { return new OpenApiSchema { @@ -50,48 +48,58 @@ private static OpenApiSchema GetNullableReferenceSchemaUsingExplicitNullType() } // This approach is supported starting from OAS v3.0. See https://github.com/OAI/OpenAPI-Specification/issues/1368#issuecomment-487314681 - private OpenApiSchema GetNullableReferenceSchemaUsingImplicitNullType() + private OpenApiSchema GetImplicitNullSchema() { - if (_referenceSchemaForNullValue != null) + EnsureFullSchemaForNullValueExists(); + + return _referenceSchemaForExplicitNullValue ??= new OpenApiSchema { - return _referenceSchemaForNullValue; - } + Reference = new OpenApiReference + { + Id = _nullableSchemaReferenceId, + Type = ReferenceType.Schema + } + }; + } - var fullSchemaForNullValue = new OpenApiSchema + private void EnsureFullSchemaForNullValueExists() + { + if (!_schemaRepositoryAccessor.Current.Schemas.ContainsKey(_nullableSchemaReferenceId)) { - Nullable = true, - Not = new OpenApiSchema + var fullSchemaForNullValue = new OpenApiSchema { - AnyOf = new List + Nullable = true, + Not = new OpenApiSchema { - new() + AnyOf = new List { - Type = "string" + new() + { + Type = "string" + }, + new() + { + Type = "number" + }, + new() + { + Type = "boolean" + }, + new() + { + Type = "object" + }, + new() + { + Type = "array" + } }, - new() - { - Type = "number" - }, - new() - { - Type = "boolean" - }, - new() - { - Type = "object" - }, - new() - { - Type = "array" - } - }, - Items = new OpenApiSchema() - } - }; + Items = new OpenApiSchema() + } + }; - _referenceSchemaForNullValue = _schemaRepositoryAccessor.Current.AddDefinition("null-value", fullSchemaForNullValue); - - return _referenceSchemaForNullValue; + _schemaRepositoryAccessor.Current.AddDefinition(_nullableSchemaReferenceId, fullSchemaForNullValue); + } } } } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs index 620409d5ab..550aca58a7 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; +using System.Text.Json; using JsonApiDotNetCore.OpenApi.JsonApiObjects; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; @@ -14,8 +15,6 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents { internal sealed class ResourceFieldObjectSchemaBuilder { - private static readonly SchemaRepository ResourceSchemaRepository = new(); - private static readonly Type[] RelationshipInResponseOpenTypes = { typeof(ToOneRelationshipInResponse<>), @@ -27,11 +26,12 @@ internal sealed class ResourceFieldObjectSchemaBuilder private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; private readonly SchemaGenerator _defaultSchemaGenerator; private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator; + private readonly SchemaRepository _resourceSchemaRepository = new(); private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator; private readonly IDictionary _schemasForResourceFields; public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISchemaRepositoryAccessor schemaRepositoryAccessor, - SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator) + SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, JsonNamingPolicy? namingPolicy) { ArgumentGuard.NotNull(resourceTypeInfo, nameof(resourceTypeInfo)); ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); @@ -43,18 +43,18 @@ public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISche _defaultSchemaGenerator = defaultSchemaGenerator; _resourceTypeSchemaGenerator = resourceTypeSchemaGenerator; - _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor); + _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor, namingPolicy); _schemasForResourceFields = GetFieldSchemas(); } private IDictionary GetFieldSchemas() { - if (!ResourceSchemaRepository.TryLookupByType(_resourceTypeInfo.ResourceType.ClrType, out OpenApiSchema referenceSchemaForResource)) + if (!_resourceSchemaRepository.TryLookupByType(_resourceTypeInfo.ResourceType.ClrType, out OpenApiSchema referenceSchemaForResource)) { - referenceSchemaForResource = _defaultSchemaGenerator.GenerateSchema(_resourceTypeInfo.ResourceType.ClrType, ResourceSchemaRepository); + referenceSchemaForResource = _defaultSchemaGenerator.GenerateSchema(_resourceTypeInfo.ResourceType.ClrType, _resourceSchemaRepository); } - OpenApiSchema fullSchemaForResource = ResourceSchemaRepository.Schemas[referenceSchemaForResource.Reference.Id]; + OpenApiSchema fullSchemaForResource = _resourceSchemaRepository.Schemas[referenceSchemaForResource.Reference.Id]; return fullSchemaForResource.Properties; } @@ -99,7 +99,7 @@ private void AddAttributeSchemaToResourceObject(AttrAttribute attribute, OpenApi private void ExposeSchema(OpenApiReference openApiReference, Type typeRepresentedBySchema) { - OpenApiSchema fullSchema = ResourceSchemaRepository.Schemas[openApiReference.Id]; + OpenApiSchema fullSchema = _resourceSchemaRepository.Schemas[openApiReference.Id]; _schemaRepositoryAccessor.Current.AddDefinition(openApiReference.Id, fullSchema); _schemaRepositoryAccessor.Current.RegisterType(typeRepresentedBySchema, openApiReference.Id); } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs index 1db417f7e5..28195c699b 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs @@ -33,7 +33,7 @@ public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IRe _allowClientGeneratedIds = options.AllowClientGeneratedIds; _resourceFieldObjectSchemaBuilderFactory = resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor, - defaultSchemaGenerator, _resourceTypeSchemaGenerator); + defaultSchemaGenerator, _resourceTypeSchemaGenerator, options.SerializerOptions.PropertyNamingPolicy); } public OpenApiSchema GenerateSchema(Type resourceObjectType) diff --git a/test/OpenApiClientTests/LegacyClient/ApiResponse.cs b/test/OpenApiClientTests/LegacyClient/ApiResponse.cs index 585212f12f..2682f1be9d 100644 --- a/test/OpenApiClientTests/LegacyClient/ApiResponse.cs +++ b/test/OpenApiClientTests/LegacyClient/ApiResponse.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; using JsonApiDotNetCore.OpenApi.Client; -using OpenApiClientTests.LegacyClient.GeneratedCode; +using JsonApiDotNetCore.OpenApi.Client.Exceptions; #pragma warning disable AV1008 // Class should not be static diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index 1b6b00faf6..26b2078436 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Specialized; +using JsonApiDotNetCore.OpenApi.Client.Exceptions; using OpenApiClientTests.LegacyClient.GeneratedCode; using Xunit; diff --git a/test/OpenApiClientTests/NamingConvention/KebabCase/GeneratedTypesTests.cs b/test/OpenApiClientTests/NamingConvention/KebabCase/GeneratedTypesTests.cs new file mode 100644 index 0000000000..12f9a62adc --- /dev/null +++ b/test/OpenApiClientTests/NamingConvention/KebabCase/GeneratedTypesTests.cs @@ -0,0 +1,80 @@ +using FluentAssertions; +using OpenApiClientTests.NamingConvention.KebabCase.GeneratedCode; +using Xunit; + +namespace OpenApiClientTests.NamingConvention.KebabCase +{ + public sealed class GeneratedTypesTests + { + [Fact] + public void Generated_code_is_named_as_expected() + { + nameof(KebabCaseClient.GetSupermarketCollectionAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketCollectionAsync).Should().NotBeNull(); + nameof(KebabCaseClient.PostSupermarketAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketAsync).Should().NotBeNull(); + nameof(KebabCaseClient.PatchSupermarketAsync).Should().NotBeNull(); + nameof(KebabCaseClient.DeleteSupermarketAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketBackupStoreManagerAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketBackupStoreManagerAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketBackupStoreManagerRelationshipAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketBackupStoreManagerRelationshipAsync).Should().NotBeNull(); + nameof(KebabCaseClient.PatchSupermarketBackupStoreManagerRelationshipAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketCashiersAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketCashiersAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketCashiersRelationshipAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketCashiersRelationshipAsync).Should().NotBeNull(); + nameof(KebabCaseClient.PostSupermarketCashiersRelationshipAsync).Should().NotBeNull(); + nameof(KebabCaseClient.PatchSupermarketCashiersRelationshipAsync).Should().NotBeNull(); + nameof(KebabCaseClient.DeleteSupermarketCashiersRelationshipAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketStoreManagerAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketStoreManagerAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketStoreManagerRelationshipAsync).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketStoreManagerRelationshipAsync).Should().NotBeNull(); + nameof(KebabCaseClient.PatchSupermarketStoreManagerRelationshipAsync).Should().NotBeNull(); + + nameof(SupermarketCollectionResponseDocument).Should().NotBeNull(); + nameof(LinksInResourceCollectionDocument).Should().NotBeNull(); + nameof(JsonapiObject).Should().NotBeNull(); + nameof(SupermarketDataInResponse).Should().NotBeNull(); + nameof(SupermarketsResourceType).Should().NotBeNull(); + nameof(SupermarketAttributesInResponse.NameOfCity).Should().NotBeNull(); + nameof(SupermarketRelationshipsInResponse.StoreManager).Should().NotBeNull(); + nameof(SupermarketRelationshipsInResponse.BackupStoreManager).Should().NotBeNull(); + nameof(LinksInResourceObject).Should().NotBeNull(); + nameof(SupermarketType).Should().NotBeNull(); + nameof(KebabCaseClient.GetSupermarketAsync).Should().NotBeNull(); + nameof(ToOneStaffMemberInResponse).Should().NotBeNull(); + nameof(NullableToOneStaffMemberInResponse).Should().NotBeNull(); + nameof(ToManyStaffMemberInResponse).Should().NotBeNull(); + nameof(LinksInRelationshipObject).Should().NotBeNull(); + nameof(StaffMemberIdentifier).Should().NotBeNull(); + nameof(StaffMembersResourceType).Should().NotBeNull(); + nameof(StaffMembersResourceType.StaffMembers).Should().NotBeNull(); + nameof(SupermarketPrimaryResponseDocument).Should().NotBeNull(); + nameof(LinksInResourceDocument).Should().NotBeNull(); + nameof(StaffMemberSecondaryResponseDocument).Should().NotBeNull(); + nameof(StaffMemberDataInResponse).Should().NotBeNull(); + nameof(StaffMemberAttributesInResponse).Should().NotBeNull(); + nameof(NullableStaffMemberSecondaryResponseDocument).Should().NotBeNull(); + nameof(StaffMemberCollectionResponseDocument).Should().NotBeNull(); + nameof(StaffMemberIdentifierResponseDocument).Should().NotBeNull(); + nameof(LinksInResourceIdentifierDocument).Should().NotBeNull(); + nameof(NullableStaffMemberIdentifierResponseDocument).Should().NotBeNull(); + nameof(StaffMemberIdentifierCollectionResponseDocument).Should().NotBeNull(); + nameof(LinksInResourceIdentifierCollectionDocument).Should().NotBeNull(); + nameof(SupermarketPostRequestDocument).Should().NotBeNull(); + nameof(SupermarketDataInPostRequest).Should().NotBeNull(); + nameof(SupermarketAttributesInPostRequest).Should().NotBeNull(); + nameof(SupermarketRelationshipsInPostRequest).Should().NotBeNull(); + nameof(ToOneStaffMemberInRequest).Should().NotBeNull(); + nameof(NullableToOneStaffMemberInRequest).Should().NotBeNull(); + nameof(ToManyStaffMemberInRequest).Should().NotBeNull(); + nameof(SupermarketPatchRequestDocument).Should().NotBeNull(); + nameof(SupermarketDataInPatchRequest).Should().NotBeNull(); + nameof(SupermarketAttributesInPatchRequest).Should().NotBeNull(); + nameof(SupermarketRelationshipsInPatchRequest).Should().NotBeNull(); + } + } +} diff --git a/test/OpenApiClientTests/OpenApiClientTests.csproj b/test/OpenApiClientTests/OpenApiClientTests.csproj index 3bd3b5da2d..d1edd7a677 100644 --- a/test/OpenApiClientTests/OpenApiClientTests.csproj +++ b/test/OpenApiClientTests/OpenApiClientTests.csproj @@ -1,49 +1,57 @@ - - $(NetCoreAppVersion) - + + $(NetCoreAppVersion) + + - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + OpenApiClientTests.LegacyClient.GeneratedCode OpenApiClient + OpenApiClient.cs NSwagCSharp - /UseBaseUrl:false /GenerateClientInterfaces:true /ClientClassAccessModifier:internal + /UseBaseUrl:false /GenerateClientInterfaces:true /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions - - - + + + OpenApiClientTests.NamingConvention.KebabCase.GeneratedCode + KebabCaseClient + KebabCaseClient.cs + NSwagCSharp + /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions + + - - - - - - + + + + + + diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationTests.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationTests.cs index d461552bcc..1ce07bfc87 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationTests.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationTests.cs @@ -19,6 +19,7 @@ public LegacyOpenApiIntegrationTests() UseController(); } + // TODO: This test fails when all tests are openapi tests run in parallel; something isn't going right with the fixtures. [Fact] public async Task Retrieved_document_matches_expected_document() { diff --git a/test/OpenApiTests/NamingConvention/KebabCase/KebabCaseNamingConventionStartup.cs b/test/OpenApiTests/NamingConvention/KebabCase/KebabCaseNamingConventionStartup.cs new file mode 100644 index 0000000000..4b26bbad9a --- /dev/null +++ b/test/OpenApiTests/NamingConvention/KebabCase/KebabCaseNamingConventionStartup.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using Microsoft.EntityFrameworkCore; +using OpenApiTests.LegacyOpenApiIntegration; + +namespace OpenApiTests.NamingConventions.KebabCase +{ + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class KebabCaseNamingConventionStartup : OpenApiStartup + where TDbContext : DbContext + { + protected override void SetJsonApiOptions(JsonApiOptions options) + { + base.SetJsonApiOptions(options); + + options.SerializerOptions.PropertyNamingPolicy = JsonKebabCaseNamingPolicy.Instance; + options.SerializerOptions.DictionaryKeyPolicy = JsonKebabCaseNamingPolicy.Instance; + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + } + } +} diff --git a/test/OpenApiTests/NamingConvention/KebabCase/KebabCaseTests.cs b/test/OpenApiTests/NamingConvention/KebabCase/KebabCaseTests.cs new file mode 100644 index 0000000000..353870ef2d --- /dev/null +++ b/test/OpenApiTests/NamingConvention/KebabCase/KebabCaseTests.cs @@ -0,0 +1,529 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.NamingConventions.KebabCase +{ + public sealed class KebabCaseTests + : IClassFixture, NamingConventionDbContext>> + { + private static Lazy>? _lazyOpenApiDocument; + private readonly IntegrationTestContext, NamingConventionDbContext> _testContext; + + public KebabCaseTests(IntegrationTestContext, NamingConventionDbContext> testContext) + { + _testContext = testContext; + + _lazyOpenApiDocument ??= new Lazy>(async () => + { + testContext.UseController(); + + string content = await GetAsync("swagger/v1/swagger.json"); + + await WriteSwaggerDocumentToFileAsync(content); + + JsonDocument document = JsonDocument.Parse(content); + + return document.ToJsonElement(); + }, LazyThreadSafetyMode.ExecutionAndPublication); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_GetCollection_endpoint() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + string? documentSchemaRefId = null; + + document.ShouldContainPath("paths./supermarkets.get").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("get-supermarket-collection"); + }); + + documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + .ShouldBeReferenceSchemaId("supermarket-collection-response-document").SchemaReferenceId; + }); + + document.ShouldContainPath("components.schemas").With(schemasElement => + { + string? resourceDataSchemaRefId = null; + + schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.ShouldContainPath("links.$ref").ShouldBeReferenceSchemaId("links-in-resource-collection-document"); + propertiesElement.ShouldContainPath("jsonapi.$ref").ShouldBeReferenceSchemaId("jsonapi-object"); + + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.items.$ref").ShouldBeReferenceSchemaId("supermarket-data-in-response") + .SchemaReferenceId; + }); + + string? resourceAttributesInResponseSchemaRefId = null; + string? resourceRelationshipInResponseSchemaRefId = null; + string? primaryResourceTypeSchemaRefId = null; + + schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + { + primaryResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeReferenceSchemaId("supermarkets-resource-type") + .SchemaReferenceId; + + resourceAttributesInResponseSchemaRefId = propertiesElement.ShouldContainPath("attributes.$ref") + .ShouldBeReferenceSchemaId("supermarket-attributes-in-response").SchemaReferenceId; + + resourceRelationshipInResponseSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + .ShouldBeReferenceSchemaId("supermarket-relationships-in-response").SchemaReferenceId; + + propertiesElement.ShouldContainPath("links.$ref").ShouldBeReferenceSchemaId("links-in-resource-object"); + }); + + schemasElement.ShouldContainPath($"{primaryResourceTypeSchemaRefId}.enum[0]").With(enumValueElement => + { + enumValueElement.ShouldBeString("supermarkets"); + }); + + schemasElement.ShouldContainPath($"{resourceAttributesInResponseSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.Should().ContainProperty("name-of-city"); + propertiesElement.Should().ContainProperty("kind"); + propertiesElement.ShouldContainPath("kind.$ref").ShouldBeReferenceSchemaId("supermarket-type"); + }); + + string? nullableToOneResourceResponseDataSchemaRefId = null; + + schemasElement.ShouldContainPath($"{resourceRelationshipInResponseSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.Should().ContainProperty("store-manager"); + + propertiesElement.ShouldContainPath("store-manager.$ref").ShouldBeReferenceSchemaId("to-one-staff-member-in-response"); + + nullableToOneResourceResponseDataSchemaRefId = propertiesElement.ShouldContainPath("backup-store-manager.$ref") + .ShouldBeReferenceSchemaId("nullable-to-one-staff-member-in-response").SchemaReferenceId; + + propertiesElement.Should().ContainProperty("cashiers"); + propertiesElement.ShouldContainPath("cashiers.$ref").ShouldBeReferenceSchemaId("to-many-staff-member-in-response"); + }); + + + string? relatedResourceIdentifierSchemaRefId = null; + + schemasElement.ShouldContainPath($"{nullableToOneResourceResponseDataSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.ShouldContainPath("links.$ref").ShouldBeReferenceSchemaId("links-in-relationship-object"); + relatedResourceIdentifierSchemaRefId = propertiesElement.ShouldContainPath("data.oneOf[0].$ref") + .ShouldBeReferenceSchemaId("staff-member-identifier").SchemaReferenceId; + propertiesElement.ShouldContainPath("data.oneOf[1].$ref").ShouldBeReferenceSchemaId("null-value"); + }); + + string? relatedResourceTypeSchemaRefId = null; + + schemasElement.ShouldContainPath($"{relatedResourceIdentifierSchemaRefId}.properties").With(propertiesElement => + { + relatedResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeReferenceSchemaId("staff-members-resource-type") + .SchemaReferenceId; + }); + + schemasElement.ShouldContainPath($"{relatedResourceTypeSchemaRefId}.enum[0]").ShouldBeReferenceSchemaId("staff-members"); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_GetSingle_endpoint() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + string? documentSchemaRefId = null; + + document.ShouldContainPath("paths./supermarkets/{id}.get").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("get-supermarket"); + }); + + documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + .ShouldBeReferenceSchemaId("supermarket-primary-response-document").SchemaReferenceId; + }); + + document.ShouldContainPath("components.schemas").With(schemasElement => + { + schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.ShouldContainPath("links.$ref").ShouldBeReferenceSchemaId("links-in-resource-document"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_GetSecondary_endpoint_with_single_resource() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + string? documentSchemaRefId = null; + + document.ShouldContainPath("paths./supermarkets/{id}/store-manager.get").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("get-supermarket-store-manager"); + }); + + documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + .ShouldBeReferenceSchemaId("staff-member-secondary-response-document").SchemaReferenceId; + }); + + document.ShouldContainPath("components.schemas").With(schemasElement => + { + string? resourceDataSchemaRefId = null; + + schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + { + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref") + .ShouldBeReferenceSchemaId("staff-member-data-in-response").SchemaReferenceId; + }); + + schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeReferenceSchemaId("staff-member-attributes-in-response"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_GetSecondary_endpoint_with_nullable_resource() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + document.ShouldContainPath("paths./supermarkets/{id}/backup-store-manager.get").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("get-supermarket-backup-store-manager"); + }); + + getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref").ShouldBeReferenceSchemaId("nullable-staff-member-secondary-response-document"); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_GetSecondary_endpoint_with_resources() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + document.ShouldContainPath("paths./supermarkets/{id}/cashiers.get").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("get-supermarket-cashiers"); + }); + + getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + .ShouldBeReferenceSchemaId("staff-member-collection-response-document"); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_GetRelationship_endpoint_with_ToOne_relationship() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + string? documentSchemaRefId = null; + + document.ShouldContainPath("paths./supermarkets/{id}/relationships/store-manager.get").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("get-supermarket-store-manager-relationship"); + }); + + documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + .ShouldBeReferenceSchemaId("staff-member-identifier-response-document").SchemaReferenceId; + }); + + document.ShouldContainPath("components.schemas").With(schemasElement => + { + schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.ShouldContainPath("links.$ref").ShouldBeReferenceSchemaId("links-in-resource-identifier-document"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_GetRelationship_endpoint_with_nullable_ToOne_relationship() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + document.ShouldContainPath("paths./supermarkets/{id}/relationships/backup-store-manager.get").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("get-supermarket-backup-store-manager-relationship"); + }); + + getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref").ShouldBeReferenceSchemaId("nullable-staff-member-identifier-response-document"); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_GetRelationship_endpoint_with_ToMany_relationship() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + string? documentSchemaRefId = null; + + document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.get").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("get-supermarket-cashiers-relationship"); + }); + + documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + .ShouldBeReferenceSchemaId("staff-member-identifier-collection-response-document").SchemaReferenceId; + }); + + document.ShouldContainPath("components.schemas").With(schemasElement => + { + schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.ShouldContainPath("links.$ref").ShouldBeReferenceSchemaId("links-in-resource-identifier-collection-document"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_Post_endpoint() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + string? documentSchemaRefId = null; + + document.ShouldContainPath("paths./supermarkets.post").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("post-supermarket"); + }); + + documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + .ShouldBeReferenceSchemaId("supermarket-post-request-document").SchemaReferenceId; + }); + + document.ShouldContainPath("components.schemas").With(schemasElement => + { + string? resourceDataSchemaRefId = null; + + schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + { + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeReferenceSchemaId("supermarket-data-in-post-request") + .SchemaReferenceId; + }); + + string? resourceRelationshipInPostRequestSchemaRefId = null; + + schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeReferenceSchemaId("supermarket-attributes-in-post-request"); + + resourceRelationshipInPostRequestSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + .ShouldBeReferenceSchemaId("supermarket-relationships-in-post-request").SchemaReferenceId; + }); + + schemasElement.ShouldContainPath($"{resourceRelationshipInPostRequestSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.Should().ContainProperty("store-manager"); + propertiesElement.ShouldContainPath("store-manager.$ref").ShouldBeReferenceSchemaId("to-one-staff-member-in-request"); + + propertiesElement.Should().ContainProperty("backup-store-manager"); + propertiesElement.ShouldContainPath("backup-store-manager.$ref").ShouldBeReferenceSchemaId("nullable-to-one-staff-member-in-request"); + + propertiesElement.Should().ContainProperty("cashiers"); + propertiesElement.ShouldContainPath("cashiers.$ref").ShouldBeReferenceSchemaId("to-many-staff-member-in-request"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_PostRelationship_endpoint() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.post").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("post-supermarket-cashiers-relationship"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_Patch_endpoint() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + string? documentSchemaRefId = null; + + document.ShouldContainPath("paths./supermarkets/{id}.patch").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("patch-supermarket"); + }); + + documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + .ShouldBeReferenceSchemaId("supermarket-patch-request-document").SchemaReferenceId; + }); + + document.ShouldContainPath("components.schemas").With(schemasElement => + { + string? resourceDataSchemaRefId = null; + + schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + { + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeReferenceSchemaId("supermarket-data-in-patch-request") + .SchemaReferenceId; + }); + + schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + { + propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeReferenceSchemaId("supermarket-attributes-in-patch-request"); + propertiesElement.ShouldContainPath("relationships.$ref").ShouldBeReferenceSchemaId("supermarket-relationships-in-patch-request"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_PatchRelationship_endpoint_with_ToOne_relationship() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + document.ShouldContainPath("paths./supermarkets/{id}/relationships/store-manager.patch").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("patch-supermarket-store-manager-relationship"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_PatchRelationship_endpoint_with_nullable_ToOne_relationship() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + document.ShouldContainPath("paths./supermarkets/{id}/relationships/backup-store-manager.patch").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("patch-supermarket-backup-store-manager-relationship"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_PatchRelationship_endpoint_with_ToMany_relationship() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.patch").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("patch-supermarket-cashiers-relationship"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_Delete_endpoint() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + document.ShouldContainPath("paths./supermarkets/{id}.delete").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("delete-supermarket"); + }); + }); + } + + [Fact] + public async Task Kebab_casing_convention_is_applied_to_DeleteRelationship_endpoint() + { + // Act + JsonElement document = await _lazyOpenApiDocument!.Value; + + // Assert + document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.delete").With(getElement => + { + getElement.ShouldContainPath("operationId").With(operationElement => + { + operationElement.ShouldBeString("delete-supermarket-cashiers-relationship"); + }); + }); + } + + private async Task GetAsync(string requestUrl) + { + var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + + using HttpClient client = _testContext.Factory.CreateClient(); + using HttpResponseMessage responseMessage = await client.SendAsync(request); + + return await responseMessage.Content.ReadAsStringAsync(); + } + + private async Task WriteSwaggerDocumentToFileAsync(string document) + { + string testSuitePath = GetTestSuitePath(); + string documentPath = Path.Join(testSuitePath, "swagger.json"); + await File.WriteAllTextAsync(documentPath, document); + } + + private string GetTestSuitePath() + { + string solutionTestDirectoryPath = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.Parent!.FullName; + string currentNamespacePathRelativeToTestDirectory = Path.Join(GetType().Namespace!.Split('.')); + + return Path.Join(solutionTestDirectoryPath, currentNamespacePathRelativeToTestDirectory); + } + } +} diff --git a/test/OpenApiTests/NamingConvention/KebabCase/swagger.json b/test/OpenApiTests/NamingConvention/KebabCase/swagger.json new file mode 100644 index 0000000000..267b0edf32 --- /dev/null +++ b/test/OpenApiTests/NamingConvention/KebabCase/swagger.json @@ -0,0 +1,1466 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "paths": { + "/supermarkets": { + "get": { + "tags": [ + "supermarkets" + ], + "operationId": "get-supermarket-collection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/supermarket-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "supermarkets" + ], + "operationId": "head-supermarket-collection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/supermarket-collection-response-document" + } + } + } + } + } + }, + "post": { + "tags": [ + "supermarkets" + ], + "operationId": "post-supermarket", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/supermarket-post-request-document" + } + } + } + }, + "responses": { + "201": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/supermarket-primary-response-document" + } + } + } + }, + "204": { + "description": "Success" + } + } + } + }, + "/supermarkets/{id}": { + "get": { + "tags": [ + "supermarkets" + ], + "operationId": "get-supermarket", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/supermarket-primary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "supermarkets" + ], + "operationId": "head-supermarket", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/supermarket-primary-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "supermarkets" + ], + "operationId": "patch-supermarket", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/supermarket-patch-request-document" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/supermarket-primary-response-document" + } + } + } + }, + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "supermarkets" + ], + "operationId": "delete-supermarket", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/supermarkets/{id}/backup-store-manager": { + "get": { + "tags": [ + "supermarkets" + ], + "operationId": "get-supermarket-backup-store-manager", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullable-staff-member-secondary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "supermarkets" + ], + "operationId": "head-supermarket-backup-store-manager", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullable-staff-member-secondary-response-document" + } + } + } + } + } + } + }, + "/supermarkets/{id}/relationships/backup-store-manager": { + "get": { + "tags": [ + "supermarkets" + ], + "operationId": "get-supermarket-backup-store-manager-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullable-staff-member-identifier-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "supermarkets" + ], + "operationId": "head-supermarket-backup-store-manager-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullable-staff-member-identifier-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "supermarkets" + ], + "operationId": "patch-supermarket-backup-store-manager-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullable-to-one-staff-member-in-request" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/supermarkets/{id}/cashiers": { + "get": { + "tags": [ + "supermarkets" + ], + "operationId": "get-supermarket-cashiers", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/staff-member-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "supermarkets" + ], + "operationId": "head-supermarket-cashiers", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/staff-member-collection-response-document" + } + } + } + } + } + } + }, + "/supermarkets/{id}/relationships/cashiers": { + "get": { + "tags": [ + "supermarkets" + ], + "operationId": "get-supermarket-cashiers-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/staff-member-identifier-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "supermarkets" + ], + "operationId": "head-supermarket-cashiers-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/staff-member-identifier-collection-response-document" + } + } + } + } + } + }, + "post": { + "tags": [ + "supermarkets" + ], + "operationId": "post-supermarket-cashiers-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-staff-member-in-request" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "patch": { + "tags": [ + "supermarkets" + ], + "operationId": "patch-supermarket-cashiers-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-staff-member-in-request" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "supermarkets" + ], + "operationId": "delete-supermarket-cashiers-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-staff-member-in-request" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/supermarkets/{id}/store-manager": { + "get": { + "tags": [ + "supermarkets" + ], + "operationId": "get-supermarket-store-manager", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/staff-member-secondary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "supermarkets" + ], + "operationId": "head-supermarket-store-manager", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/staff-member-secondary-response-document" + } + } + } + } + } + } + }, + "/supermarkets/{id}/relationships/store-manager": { + "get": { + "tags": [ + "supermarkets" + ], + "operationId": "get-supermarket-store-manager-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/staff-member-identifier-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "supermarkets" + ], + "operationId": "head-supermarket-store-manager-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/staff-member-identifier-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "supermarkets" + ], + "operationId": "patch-supermarket-store-manager-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-one-staff-member-in-request" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "jsonapi-object": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ext": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile": { + "type": "array", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "links-in-relationship-object": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "related": { + "type": "string" + } + }, + "additionalProperties": false + }, + "links-in-resource-collection-document": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "links-in-resource-document": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, + "links-in-resource-identifier-collection-document": { + "required": [ + "first", + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "type": "string" + }, + "first": { + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "links-in-resource-identifier-document": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "type": "string" + } + }, + "additionalProperties": false + }, + "links-in-resource-object": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + } + }, + "additionalProperties": false + }, + "null-value": { + "not": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ], + "items": { } + }, + "nullable": true + }, + "nullable-staff-member-identifier-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/staff-member-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + } + }, + "additionalProperties": false + }, + "nullable-staff-member-secondary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/staff-member-data-in-response" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + } + }, + "additionalProperties": false + }, + "nullable-to-one-staff-member-in-request": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/staff-member-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "nullable-to-one-staff-member-in-response": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/staff-member-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + }, + "links": { + "$ref": "#/components/schemas/links-in-relationship-object" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "staff-member-attributes-in-response": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "staff-member-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/staff-member-data-in-response" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + }, + "additionalProperties": false + }, + "staff-member-data-in-response": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/staff-members-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/staff-member-attributes-in-response" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-object" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "staff-member-identifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/staff-members-resource-type" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "staff-member-identifier-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/staff-member-identifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + } + }, + "additionalProperties": false + }, + "staff-member-identifier-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/staff-member-identifier" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + } + }, + "additionalProperties": false + }, + "staff-member-secondary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/staff-member-data-in-response" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + } + }, + "additionalProperties": false + }, + "staff-members-resource-type": { + "enum": [ + "staff-members" + ], + "type": "string" + }, + "supermarket-attributes-in-patch-request": { + "type": "object", + "properties": { + "name-of-city": { + "type": "string" + }, + "kind": { + "$ref": "#/components/schemas/supermarket-type" + } + }, + "additionalProperties": false + }, + "supermarket-attributes-in-post-request": { + "required": [ + "name-of-city" + ], + "type": "object", + "properties": { + "name-of-city": { + "type": "string" + }, + "kind": { + "$ref": "#/components/schemas/supermarket-type" + } + }, + "additionalProperties": false + }, + "supermarket-attributes-in-response": { + "type": "object", + "properties": { + "name-of-city": { + "type": "string" + }, + "kind": { + "$ref": "#/components/schemas/supermarket-type" + } + }, + "additionalProperties": false + }, + "supermarket-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/supermarket-data-in-response" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + }, + "additionalProperties": false + }, + "supermarket-data-in-patch-request": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/supermarkets-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/supermarket-attributes-in-patch-request" + }, + "relationships": { + "$ref": "#/components/schemas/supermarket-relationships-in-patch-request" + } + }, + "additionalProperties": false + }, + "supermarket-data-in-post-request": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/supermarkets-resource-type" + }, + "attributes": { + "$ref": "#/components/schemas/supermarket-attributes-in-post-request" + }, + "relationships": { + "$ref": "#/components/schemas/supermarket-relationships-in-post-request" + } + }, + "additionalProperties": false + }, + "supermarket-data-in-response": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/supermarkets-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/supermarket-attributes-in-response" + }, + "relationships": { + "$ref": "#/components/schemas/supermarket-relationships-in-response" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-object" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "supermarket-patch-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/supermarket-data-in-patch-request" + } + }, + "additionalProperties": false + }, + "supermarket-post-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/supermarket-data-in-post-request" + } + }, + "additionalProperties": false + }, + "supermarket-primary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/supermarket-data-in-response" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + } + }, + "additionalProperties": false + }, + "supermarket-relationships-in-patch-request": { + "type": "object", + "properties": { + "store-manager": { + "$ref": "#/components/schemas/to-one-staff-member-in-request" + }, + "backup-store-manager": { + "$ref": "#/components/schemas/nullable-to-one-staff-member-in-request" + }, + "cashiers": { + "$ref": "#/components/schemas/to-many-staff-member-in-request" + } + }, + "additionalProperties": false + }, + "supermarket-relationships-in-post-request": { + "required": [ + "store-manager" + ], + "type": "object", + "properties": { + "store-manager": { + "$ref": "#/components/schemas/to-one-staff-member-in-request" + }, + "backup-store-manager": { + "$ref": "#/components/schemas/nullable-to-one-staff-member-in-request" + }, + "cashiers": { + "$ref": "#/components/schemas/to-many-staff-member-in-request" + } + }, + "additionalProperties": false + }, + "supermarket-relationships-in-response": { + "type": "object", + "properties": { + "store-manager": { + "$ref": "#/components/schemas/to-one-staff-member-in-response" + }, + "backup-store-manager": { + "$ref": "#/components/schemas/nullable-to-one-staff-member-in-response" + }, + "cashiers": { + "$ref": "#/components/schemas/to-many-staff-member-in-response" + } + }, + "additionalProperties": false + }, + "supermarket-type": { + "enum": [ + "Traditional", + "Budget", + "Warehouse" + ], + "type": "string" + }, + "supermarkets-resource-type": { + "enum": [ + "supermarkets" + ], + "type": "string" + }, + "to-many-staff-member-in-request": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/staff-member-identifier" + } + } + }, + "additionalProperties": false + }, + "to-many-staff-member-in-response": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/staff-member-identifier" + } + }, + "links": { + "$ref": "#/components/schemas/links-in-relationship-object" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "to-one-staff-member-in-request": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/staff-member-identifier" + } + }, + "additionalProperties": false + }, + "to-one-staff-member-in-response": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/staff-member-identifier" + }, + "links": { + "$ref": "#/components/schemas/links-in-relationship-object" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/OpenApiTests/NamingConvention/NamingConventionDbContext.cs b/test/OpenApiTests/NamingConvention/NamingConventionDbContext.cs new file mode 100644 index 0000000000..eaf9e2eb03 --- /dev/null +++ b/test/OpenApiTests/NamingConvention/NamingConventionDbContext.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace OpenApiTests.NamingConventions +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class NamingConventionDbContext : DbContext + { + public DbSet Supermarkets { get; set; } + public DbSet StaffMembers { get; set; } + + public NamingConventionDbContext(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/test/OpenApiTests/NamingConvention/StaffMember.cs b/test/OpenApiTests/NamingConvention/StaffMember.cs new file mode 100644 index 0000000000..07cef3ca09 --- /dev/null +++ b/test/OpenApiTests/NamingConvention/StaffMember.cs @@ -0,0 +1,16 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.NamingConventions +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class StaffMember : Identifiable + { + [Attr] + public string Name { get; set; } = null!; + + [Attr] + public int Age { get; set; } + } +} diff --git a/test/OpenApiTests/NamingConvention/Supermarket.cs b/test/OpenApiTests/NamingConvention/Supermarket.cs new file mode 100644 index 0000000000..3c36055e91 --- /dev/null +++ b/test/OpenApiTests/NamingConvention/Supermarket.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.NamingConventions +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Supermarket : Identifiable + { + [Attr] + public string NameOfCity { get; set; } = null!; + + [Attr] + public SupermarketType Kind { get; set; } + + [HasOne] + public StaffMember StoreManager { get; set; } = null!; + + [HasOne] + public StaffMember? BackupStoreManager { get; set; } + + [HasMany] + public ICollection Cashiers { get; set; } = new HashSet(); + } +} diff --git a/test/OpenApiTests/NamingConvention/SupermarketType.cs b/test/OpenApiTests/NamingConvention/SupermarketType.cs new file mode 100644 index 0000000000..2a34330b83 --- /dev/null +++ b/test/OpenApiTests/NamingConvention/SupermarketType.cs @@ -0,0 +1,9 @@ +namespace OpenApiTests.NamingConventions +{ + public enum SupermarketType + { + Traditional, + Budget, + Warehouse + } +} diff --git a/test/OpenApiTests/NamingConvention/SupermarketsController.cs b/test/OpenApiTests/NamingConvention/SupermarketsController.cs new file mode 100644 index 0000000000..6e423f8bfd --- /dev/null +++ b/test/OpenApiTests/NamingConvention/SupermarketsController.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace OpenApiTests.NamingConventions +{ + public sealed class SupermarketsController : JsonApiController + { + public SupermarketsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService resourceService) + : base(options, resourceGraph, loggerFactory, resourceService) + { + } + } +} diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index 712134cb82..a7fd138407 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -1,27 +1,28 @@ - - $(NetCoreAppVersion) - + + $(NetCoreAppVersion) + - - - - + + + + - - - - - + + + + + + - - - + + + - - - - - - + + + + + + diff --git a/test/TestBuildingBlocks/JsonDocumentExtensions.cs b/test/TestBuildingBlocks/JsonDocumentExtensions.cs new file mode 100644 index 0000000000..dffd69cd2f --- /dev/null +++ b/test/TestBuildingBlocks/JsonDocumentExtensions.cs @@ -0,0 +1,16 @@ +using System.Text.Json; + +namespace TestBuildingBlocks +{ + public static class JsonDocumentExtensions + { + public static JsonElement ToJsonElement(this JsonDocument source) + { + using (source) + { + JsonElement clonedRoot = source.RootElement.Clone(); + return clonedRoot; + } + } + } +} diff --git a/test/TestBuildingBlocks/JsonElementAssertions.cs b/test/TestBuildingBlocks/JsonElementAssertions.cs new file mode 100644 index 0000000000..b7825358e5 --- /dev/null +++ b/test/TestBuildingBlocks/JsonElementAssertions.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using FluentAssertions.Execution; + +namespace TestBuildingBlocks +{ + public sealed class JsonElementAssertions : JsonElementAssertions + { + internal JsonElementAssertions(JsonElement subject) + : base(subject) + { + } + } + + public class JsonElementAssertions + where TAssertions : JsonElementAssertions + { + /// + /// - Gets the object which value is being asserted. + /// + private JsonElement Subject { get; } + + protected JsonElementAssertions(JsonElement subject) + { + Subject = subject; + } + + public void HaveProperty(string propertyName, string because = "", params object[] becauseArgs) + { + Execute.Assertion.ForCondition(Subject.TryGetProperty(propertyName, out _)).BecauseOf(because, becauseArgs) + .FailWith($"Expected element to have property with name '{propertyName}, but did not find it."); + } + } +} diff --git a/test/TestBuildingBlocks/JsonElementExtensions.cs b/test/TestBuildingBlocks/JsonElementExtensions.cs new file mode 100644 index 0000000000..da064d5b33 --- /dev/null +++ b/test/TestBuildingBlocks/JsonElementExtensions.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using System.Text.Json; +using BlushingPenguin.JsonPath; +using FluentAssertions; +using FluentAssertions.Execution; + +namespace TestBuildingBlocks +{ + public static class JsonElementExtensions + { + public static JsonElementAssertions Should(this JsonElement source) + { + return new JsonElementAssertions(source); + } + + public static JsonElement ShouldContainPath(this JsonElement source, string path) + { + JsonElement value = default; + + Action action = () => value = source.SelectToken(path, true)!.Value; + action.Should().NotThrow(); + + return value; + } + + public static void ShouldBeString(this JsonElement source, string value) + { + source.ValueKind.Should().Be(JsonValueKind.String); + source.GetString().Should().Be(value); + } + + public static ReferenceSchemaIdAssertion ShouldBeReferenceSchemaId(this JsonElement source, string value) + { + source.ValueKind.Should().Be(JsonValueKind.String); + + string jsonElementValue = source.GetString(); + jsonElementValue.Should().NotBeNull(); + + string referenceSchemaId = jsonElementValue!.Split('/').Last(); + referenceSchemaId.Should().Be(value); + + return new ReferenceSchemaIdAssertion + { + SchemaReferenceId = value + }; + } + + public sealed class ReferenceSchemaIdAssertion + { + public string SchemaReferenceId { get; internal init; } + } + + public sealed class JsonElementAssertions : JsonElementAssertions + { + internal JsonElementAssertions(JsonElement subject) + : base(subject) + { + } + } + + public class JsonElementAssertions + where TAssertions : JsonElementAssertions + { + private readonly JsonElement _subject; + + protected JsonElementAssertions(JsonElement subject) + { + _subject = subject; + } + + public void ContainProperty(string propertyName) + { + string escapedJson = _subject.ToString()?.Replace("{", "{{").Replace("}", "}}"); + + Execute.Assertion.ForCondition(_subject.TryGetProperty(propertyName, out _)) + .FailWith($"Expected JSON element '{escapedJson}' to contain a property named '{propertyName}'."); + } + } + } +} diff --git a/test/TestBuildingBlocks/TestBuildingBlocks.csproj b/test/TestBuildingBlocks/TestBuildingBlocks.csproj index a4eae45836..e80b0a6e67 100644 --- a/test/TestBuildingBlocks/TestBuildingBlocks.csproj +++ b/test/TestBuildingBlocks/TestBuildingBlocks.csproj @@ -1,21 +1,22 @@ - - $(NetCoreAppVersion) - + + $(NetCoreAppVersion) + - - - + + + - - - - - - - - - - - + + + + + + + + + + + +