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)
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+