Skip to content

OpenAPI: Generalization of naming conventions #1098

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 7 additions & 6 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
<PackageReference Include="JetBrains.Annotations" Version="2021.1.0" PrivateAssets="All" />
<PackageReference Include="CSharpGuidelinesAnalyzer" Version="3.7.0" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CSharpGuidelinesAnalyzer.config" Visible="False" />
</ItemGroup>

<PropertyGroup Condition="'$(Configuration)'=='Release'">
<NoWarn>$(NoWarn);1591</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<NoWarn>$(NoWarn);1591</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<!-- Test Project Dependencies -->
<PropertyGroup>
Expand All @@ -31,5 +31,6 @@
<MoqVersion>4.16.1</MoqVersion>
<XUnitVersion>2.4.*</XUnitVersion>
<TestSdkVersion>17.0.0</TestSdkVersion>
<BlushingPenguinVersion>1.0.*</BlushingPenguinVersion>
</PropertyGroup>
</Project>
46 changes: 46 additions & 0 deletions src/JsonApiDotNetCore.OpenApi.Client/ApiException.cs
Original file line number Diff line number Diff line change
@@ -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<string, IEnumerable<string>> Headers { get; }

public ApiException(string message, int statusCode, string response, IReadOnlyDictionary<string, IEnumerable<string>> 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<TResult> : ApiException
{
public TResult Result { get; }

public ApiException(string message, int statusCode, string response, IReadOnlyDictionary<string, IEnumerable<string>> headers, TResult result,
Exception innerException)
: base(message, statusCode, response, headers, innerException)
{
Result = result;
}
}
}
75 changes: 42 additions & 33 deletions src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,42 +14,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 @@ -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
Comment on lines +71 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move these down to where these are intended for


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;
}
}
}
7 changes: 3 additions & 4 deletions src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,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 @@ -105,9 +104,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 @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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>(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)
Expand All @@ -28,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 @@ -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<OpenApiSchema>
Nullable = true,
Not = new OpenApiSchema
{
new()
AnyOf = new List<OpenApiSchema>
{
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);
}
}
}
}
Loading