Skip to content

OpenAPI: Generalization of naming conventions #1123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 41 additions & 34 deletions src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json;
using Humanizer;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
Expand All @@ -10,42 +11,41 @@ internal sealed class JsonApiSchemaIdSelector
{
private static readonly IDictionary<Type, string> OpenTypeToSchemaTemplateMap = new Dictionary<Type, string>
{
[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;
}

Expand All @@ -62,14 +62,21 @@ public string GetSchemaId(Type type)

if (type.IsConstructedGenericType && OpenTypeToSchemaTemplateMap.ContainsKey(type.GetGenericTypeDefinition()))
{
Type openType = type.GetGenericTypeDefinition();
Type resourceClrType = type.GetGenericArguments().First();
string resourceName = _formatter.FormatResourceName(resourceClrType).Singularize();
resourceType = _resourceGraph.FindResourceType(resourceClrType);

string template = OpenTypeToSchemaTemplateMap[type.GetGenericTypeDefinition()];
return template.Replace("###", resourceName);
if (resourceType == null)
{
throw new UnreachableCodeException();
}

string pascalCaseSchemaId = OpenTypeToSchemaTemplateMap[openType].Replace("[ResourceName]", resourceType.PublicName.Singularize()).Pascalize();

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();
// Used for a fixed set of types, such as JsonApiObject, LinksInResourceCollectionDocument etc.
return _namingPolicy != null ? _namingPolicy.ConvertName(type.Name) : type.Name;
}
}
8 changes: 4 additions & 4 deletions src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBu
var apiDescriptionProviders = provider.GetRequiredService<IEnumerable<IApiDescriptionProvider>>();

JsonApiActionDescriptorCollectionProvider descriptorCollectionProviderWrapper = new(controllerResourceMapping, actionDescriptorCollectionProvider);

return new ApiDescriptionGroupCollectionProvider(descriptorCollectionProviderWrapper, apiDescriptionProviders);
});

Expand All @@ -55,15 +56,14 @@ private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection
var resourceGraph = scope.ServiceProvider.GetRequiredService<IResourceGraph>();
var jsonApiOptions = scope.ServiceProvider.GetRequiredService<IJsonApiOptions>();
JsonNamingPolicy? namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy;
ResourceNameFormatter resourceNameFormatter = new(namingPolicy);

AddSchemaGenerator(services);

services.AddSwaggerGen(swaggerGenOptions =>
{
swaggerGenOptions.SupportNonNullableReferenceTypes();
SetOperationInfo(swaggerGenOptions, controllerResourceMapping, namingPolicy);
SetSchemaIdSelector(swaggerGenOptions, resourceGraph, resourceNameFormatter);
SetSchemaIdSelector(swaggerGenOptions, resourceGraph, namingPolicy);
swaggerGenOptions.DocumentFilter<EndpointOrderingFilter>();

setupSwaggerGenAction?.Invoke(swaggerGenOptions);
Expand Down Expand Up @@ -101,9 +101,9 @@ private static IList<string> 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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
using System.Text.Json;
using Microsoft.OpenApi.Models;

namespace JsonApiDotNetCore.OpenApi.SwaggerComponents;

internal sealed class NullableReferenceSchemaGenerator
{
private static readonly NullableReferenceSchemaStrategy NullableReferenceStrategy =
private const string PascalCaseNullableSchemaReferenceId = "NullValue";

private readonly NullableReferenceSchemaStrategy _nullableReferenceStrategy =
Enum.Parse<NullableReferenceSchemaStrategy>(NullableReferenceSchemaStrategy.Implicit.ToString());

private static OpenApiSchema? _referenceSchemaForNullValue;
private readonly string _nullableSchemaReferenceId;
private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor;

public NullableReferenceSchemaGenerator(ISchemaRepositoryAccessor schemaRepositoryAccessor)
private OpenApiSchema? _referenceSchemaForExplicitNullValue;

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)
Expand All @@ -26,20 +33,13 @@ public OpenApiSchema GenerateSchema(OpenApiSchema referenceSchema)
OneOf = new List<OpenApiSchema>
{
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
{
Expand All @@ -48,47 +48,57 @@ 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<OpenApiSchema>
Nullable = true,
Not = new OpenApiSchema
{
new()
{
Type = "string"
},
new()
{
Type = "number"
},
new()
{
Type = "boolean"
},
new()
AnyOf = new List<OpenApiSchema>
{
Type = "object"
new()
{
Type = "string"
},
new()
{
Type = "number"
},
new()
{
Type = "boolean"
},
new()
{
Type = "object"
},
new()
{
Type = "array"
}
},
new()
{
Type = "array"
}
},
Items = new OpenApiSchema()
}
};

_referenceSchemaForNullValue = _schemaRepositoryAccessor.Current.AddDefinition("null-value", fullSchemaForNullValue);
Items = new OpenApiSchema()
}
};

return _referenceSchemaForNullValue;
_schemaRepositoryAccessor.Current.AddDefinition(_nullableSchemaReferenceId, fullSchemaForNullValue);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Text.Json;
using JsonApiDotNetCore.OpenApi.JsonApiObjects;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
Expand All @@ -11,8 +12,6 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents;

internal sealed class ResourceFieldObjectSchemaBuilder
{
private static readonly SchemaRepository ResourceSchemaRepository = new();

private static readonly Type[] RelationshipInResponseOpenTypes =
{
typeof(ToOneRelationshipInResponse<>),
Expand All @@ -24,11 +23,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<string, OpenApiSchema> _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));
Expand All @@ -40,18 +40,18 @@ public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISche
_defaultSchemaGenerator = defaultSchemaGenerator;
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;

_nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor);
_nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor, namingPolicy);
_schemasForResourceFields = GetFieldSchemas();
}

private IDictionary<string, OpenApiSchema> 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;
}

Expand Down Expand Up @@ -96,7 +96,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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IRe
_resourceGraph = resourceGraph;
_schemaRepositoryAccessor = schemaRepositoryAccessor;

_resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceGraph);
_resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceGraph, options.SerializerOptions.PropertyNamingPolicy);

_allowClientGeneratedIds = options.AllowClientGeneratedIds;

_resourceFieldObjectSchemaBuilderFactory = resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor,
defaultSchemaGenerator, _resourceTypeSchemaGenerator);
defaultSchemaGenerator, _resourceTypeSchemaGenerator, options.SerializerOptions.PropertyNamingPolicy);
}

public OpenApiSchema GenerateSchema(Type resourceObjectType)
Expand Down
Loading