Skip to content

Commit 8e1d16d

Browse files
committed
Unify method names in schema generators, move into subdirectory, split document generators
1 parent 6178c58 commit 8e1d16d

16 files changed

+371
-294
lines changed

src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using JsonApiDotNetCore.Middleware;
2-
using JsonApiDotNetCore.OpenApi.JsonApiObjects;
32
using Microsoft.AspNetCore.Mvc.ApiExplorer;
43
using Microsoft.AspNetCore.Mvc.Formatters;
54
using Microsoft.Net.Http.Headers;

src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonApiSchemaFacts.cs renamed to src/JsonApiDotNetCore.OpenApi/JsonApiSchemaFacts.cs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
#pragma warning disable AV1008 // Class should not be static
55

6-
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects;
6+
namespace JsonApiDotNetCore.OpenApi;
77

88
internal static class JsonApiSchemaFacts
99
{
@@ -16,18 +16,6 @@ internal static class JsonApiSchemaFacts
1616
typeof(ToManyRelationshipInRequest<>)
1717
];
1818

19-
private static readonly Type[] ResponseSchemaTypes =
20-
[
21-
typeof(ResourceCollectionResponseDocument<>),
22-
typeof(PrimaryResourceResponseDocument<>),
23-
typeof(SecondaryResourceResponseDocument<>),
24-
typeof(NullableSecondaryResourceResponseDocument<>),
25-
typeof(ResourceIdentifierCollectionResponseDocument<>),
26-
typeof(ResourceIdentifierResponseDocument<>),
27-
typeof(NullableResourceIdentifierResponseDocument<>),
28-
typeof(ErrorResponseDocument)
29-
];
30-
3119
private static readonly Type[] SchemaTypesHavingNullableDataProperty =
3220
[
3321
typeof(NullableSecondaryResourceResponseDocument<>),
@@ -43,14 +31,6 @@ internal static class JsonApiSchemaFacts
4331
typeof(NullableToOneRelationshipInResponse<>)
4432
];
4533

46-
public static bool RequiresCustomSchemaGenerator(Type schemaType)
47-
{
48-
// Subset of the entry types Swashbuckle calls us for, which must be handled by our custom schema generators.
49-
50-
Type lookupType = schemaType.ConstructedToOpenType();
51-
return RequestSchemaTypes.Contains(lookupType) || ResponseSchemaTypes.Contains(lookupType);
52-
}
53-
5434
public static bool IsRequestSchemaType(Type schemaType)
5535
{
5636
Type lookupType = schemaType.ConstructedToOpenType();
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using JsonApiDotNetCore.OpenApi.SchemaGenerators.Components;
3+
using Microsoft.OpenApi.Models;
4+
using Swashbuckle.AspNetCore.SwaggerGen;
5+
6+
namespace JsonApiDotNetCore.OpenApi.SchemaGenerators.Bodies;
7+
8+
/// <summary>
9+
/// Generates the OpenAPI component schema for a request and/or response body.
10+
/// </summary>
11+
internal abstract class BodySchemaGenerator
12+
{
13+
private readonly MetaSchemaGenerator _metaSchemaGenerator;
14+
private readonly LinksVisibilitySchemaGenerator _linksVisibilitySchemaGenerator;
15+
private readonly IJsonApiOptions _options;
16+
17+
protected BodySchemaGenerator(MetaSchemaGenerator metaSchemaGenerator, LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator,
18+
IJsonApiOptions options)
19+
{
20+
ArgumentGuard.NotNull(metaSchemaGenerator);
21+
ArgumentGuard.NotNull(linksVisibilitySchemaGenerator);
22+
ArgumentGuard.NotNull(options);
23+
24+
_metaSchemaGenerator = metaSchemaGenerator;
25+
_linksVisibilitySchemaGenerator = linksVisibilitySchemaGenerator;
26+
_options = options;
27+
}
28+
29+
public abstract bool CanGenerate(Type modelType);
30+
31+
public OpenApiSchema GenerateSchema(Type bodyType, SchemaRepository schemaRepository)
32+
{
33+
ArgumentGuard.NotNull(bodyType);
34+
ArgumentGuard.NotNull(schemaRepository);
35+
36+
if (schemaRepository.TryLookupByType(bodyType, out OpenApiSchema? referenceSchema))
37+
{
38+
return referenceSchema;
39+
}
40+
41+
_metaSchemaGenerator.GenerateSchema(schemaRepository);
42+
43+
referenceSchema = GenerateBodySchema(bodyType, schemaRepository);
44+
OpenApiSchema fullSchema = schemaRepository.Schemas[referenceSchema.Reference.Id];
45+
46+
_linksVisibilitySchemaGenerator.UpdateSchemaForTopLevel(bodyType, fullSchema, schemaRepository);
47+
48+
SetJsonApiVersion(fullSchema);
49+
50+
return referenceSchema;
51+
}
52+
53+
protected abstract OpenApiSchema GenerateBodySchema(Type bodyType, SchemaRepository schemaRepository);
54+
55+
private void SetJsonApiVersion(OpenApiSchema fullSchema)
56+
{
57+
if (fullSchema.Properties.ContainsKey(JsonApiPropertyName.Jsonapi) && !_options.IncludeJsonApiVersion)
58+
{
59+
fullSchema.Properties.Remove(JsonApiPropertyName.Jsonapi);
60+
}
61+
}
62+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
3+
using JsonApiDotNetCore.OpenApi.SchemaGenerators.Components;
4+
using JsonApiDotNetCore.Serialization.Objects;
5+
using Microsoft.OpenApi.Models;
6+
using Swashbuckle.AspNetCore.SwaggerGen;
7+
8+
namespace JsonApiDotNetCore.OpenApi.SchemaGenerators.Bodies;
9+
10+
/// <summary>
11+
/// Generates the OpenAPI component schema for an error document.
12+
/// </summary>
13+
internal sealed class ErrorResponseBodySchemaGenerator : BodySchemaGenerator
14+
{
15+
private readonly SchemaGenerator _defaultSchemaGenerator;
16+
private readonly MetaSchemaGenerator _metaSchemaGenerator;
17+
18+
public ErrorResponseBodySchemaGenerator(SchemaGenerator defaultSchemaGenerator, MetaSchemaGenerator metaSchemaGenerator,
19+
LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator, IJsonApiOptions options)
20+
: base(metaSchemaGenerator, linksVisibilitySchemaGenerator, options)
21+
{
22+
ArgumentGuard.NotNull(defaultSchemaGenerator);
23+
24+
_defaultSchemaGenerator = defaultSchemaGenerator;
25+
_metaSchemaGenerator = metaSchemaGenerator;
26+
}
27+
28+
public override bool CanGenerate(Type modelType)
29+
{
30+
return modelType == typeof(ErrorResponseDocument);
31+
}
32+
33+
protected override OpenApiSchema GenerateBodySchema(Type bodyType, SchemaRepository schemaRepository)
34+
{
35+
OpenApiSchema referenceSchemaForErrorObject = _defaultSchemaGenerator.GenerateSchema(typeof(ErrorObject), schemaRepository);
36+
OpenApiSchema fullSchemaForErrorObject = schemaRepository.Schemas[referenceSchemaForErrorObject.Reference.Id];
37+
38+
OpenApiSchema referenceSchemaForMeta = _metaSchemaGenerator.GenerateSchema(schemaRepository);
39+
fullSchemaForErrorObject.Properties[JsonApiPropertyName.Meta] = referenceSchemaForMeta.WrapInExtendedSchema();
40+
41+
return _defaultSchemaGenerator.GenerateSchema(bodyType, schemaRepository);
42+
}
43+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using System.Reflection;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
4+
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
5+
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
6+
using JsonApiDotNetCore.OpenApi.SchemaGenerators.Components;
7+
using JsonApiDotNetCore.OpenApi.SwaggerComponents;
8+
using Microsoft.OpenApi.Models;
9+
using Swashbuckle.AspNetCore.SwaggerGen;
10+
11+
namespace JsonApiDotNetCore.OpenApi.SchemaGenerators.Bodies;
12+
13+
/// <summary>
14+
/// Generates the OpenAPI component schema for a resource/relationship request/response body.
15+
/// </summary>
16+
internal sealed class ResourceOrRelationshipBodySchemaGenerator : BodySchemaGenerator
17+
{
18+
private static readonly Type[] RequestSchemaTypes =
19+
[
20+
typeof(CreateResourceRequestDocument<>),
21+
typeof(UpdateResourceRequestDocument<>),
22+
typeof(ToOneRelationshipInRequest<>),
23+
typeof(NullableToOneRelationshipInRequest<>),
24+
typeof(ToManyRelationshipInRequest<>)
25+
];
26+
27+
private static readonly Type[] ResponseSchemaTypes =
28+
[
29+
typeof(ResourceCollectionResponseDocument<>),
30+
typeof(PrimaryResourceResponseDocument<>),
31+
typeof(SecondaryResourceResponseDocument<>),
32+
typeof(NullableSecondaryResourceResponseDocument<>),
33+
typeof(ResourceIdentifierResponseDocument<>),
34+
typeof(NullableResourceIdentifierResponseDocument<>),
35+
typeof(ResourceIdentifierCollectionResponseDocument<>)
36+
];
37+
38+
private readonly SchemaGenerator _defaultSchemaGenerator;
39+
private readonly AbstractResourceDataSchemaGenerator _abstractResourceDataSchemaGenerator;
40+
private readonly DataSchemaGenerator _dataSchemaGenerator;
41+
private readonly IncludeDependencyScanner _includeDependencyScanner;
42+
private readonly IResourceGraph _resourceGraph;
43+
44+
public ResourceOrRelationshipBodySchemaGenerator(SchemaGenerator defaultSchemaGenerator,
45+
AbstractResourceDataSchemaGenerator abstractResourceDataSchemaGenerator, DataSchemaGenerator dataSchemaGenerator,
46+
MetaSchemaGenerator metaSchemaGenerator, LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator,
47+
IncludeDependencyScanner includeDependencyScanner, IResourceGraph resourceGraph, IJsonApiOptions options)
48+
: base(metaSchemaGenerator, linksVisibilitySchemaGenerator, options)
49+
{
50+
ArgumentGuard.NotNull(defaultSchemaGenerator);
51+
ArgumentGuard.NotNull(abstractResourceDataSchemaGenerator);
52+
ArgumentGuard.NotNull(dataSchemaGenerator);
53+
ArgumentGuard.NotNull(includeDependencyScanner);
54+
ArgumentGuard.NotNull(resourceGraph);
55+
56+
_defaultSchemaGenerator = defaultSchemaGenerator;
57+
_abstractResourceDataSchemaGenerator = abstractResourceDataSchemaGenerator;
58+
_dataSchemaGenerator = dataSchemaGenerator;
59+
_includeDependencyScanner = includeDependencyScanner;
60+
_resourceGraph = resourceGraph;
61+
}
62+
63+
public override bool CanGenerate(Type modelType)
64+
{
65+
Type modelOpenType = modelType.ConstructedToOpenType();
66+
return RequestSchemaTypes.Contains(modelOpenType) || ResponseSchemaTypes.Contains(modelOpenType);
67+
}
68+
69+
protected override OpenApiSchema GenerateBodySchema(Type bodyType, SchemaRepository schemaRepository)
70+
{
71+
ArgumentGuard.NotNull(bodyType);
72+
ArgumentGuard.NotNull(schemaRepository);
73+
74+
if (schemaRepository.TryLookupByType(bodyType, out OpenApiSchema? referenceSchemaForBody))
75+
{
76+
return referenceSchemaForBody;
77+
}
78+
79+
bool isRequestSchema = RequestSchemaTypes.Contains(bodyType.ConstructedToOpenType());
80+
81+
if (!isRequestSchema)
82+
{
83+
// There's no way to intercept in the Swashbuckle recursive component schema generation when using inheritance, which we need
84+
// to perform generic type expansions. As a workaround, we generate an empty base schema upfront. Each time the schema
85+
// for a derived type is generated, we'll add it to the discriminator mapping.
86+
_ = _abstractResourceDataSchemaGenerator.GenerateSchema(schemaRepository);
87+
}
88+
89+
Type dataConstructedType = GetInnerTypeOfDataProperty(bodyType);
90+
91+
if (!isRequestSchema)
92+
{
93+
// Ensure all reachable related resource types are available in the discriminator mapping so includes work.
94+
// Doing this matters when not all endpoints are exposed.
95+
EnsureResourceTypesAreMappedInDiscriminator(dataConstructedType, schemaRepository);
96+
}
97+
98+
OpenApiSchema referenceSchemaForData = _dataSchemaGenerator.GenerateSchema(dataConstructedType, schemaRepository);
99+
100+
if (!isRequestSchema)
101+
{
102+
_abstractResourceDataSchemaGenerator.MapDiscriminator(dataConstructedType, referenceSchemaForData, schemaRepository);
103+
}
104+
105+
referenceSchemaForBody = _defaultSchemaGenerator.GenerateSchema(bodyType, schemaRepository);
106+
OpenApiSchema fullSchemaForBody = schemaRepository.Schemas[referenceSchemaForBody.Reference.Id].UnwrapLastExtendedSchema();
107+
108+
if (JsonApiSchemaFacts.HasNullableDataProperty(bodyType))
109+
{
110+
fullSchemaForBody.Properties[JsonApiPropertyName.Data].Nullable = true;
111+
}
112+
113+
return referenceSchemaForBody;
114+
}
115+
116+
private static Type GetInnerTypeOfDataProperty(Type bodyType)
117+
{
118+
PropertyInfo? dataProperty = bodyType.GetProperty("Data");
119+
120+
if (dataProperty == null)
121+
{
122+
throw new UnreachableCodeException();
123+
}
124+
125+
return dataProperty.PropertyType.ConstructedToOpenType().IsAssignableTo(typeof(ICollection<>))
126+
? dataProperty.PropertyType.GenericTypeArguments[0]
127+
: dataProperty.PropertyType;
128+
}
129+
130+
private void EnsureResourceTypesAreMappedInDiscriminator(Type dataConstructedType, SchemaRepository schemaRepository)
131+
{
132+
Type dataOpenType = dataConstructedType.GetGenericTypeDefinition();
133+
134+
if (dataOpenType == typeof(ResourceDataInResponse<>))
135+
{
136+
var resourceTypeInfo = ResourceTypeInfo.Create(dataConstructedType, _resourceGraph);
137+
138+
foreach (ResourceType resourceType in _includeDependencyScanner.GetReachableRelatedTypes(resourceTypeInfo.ResourceType))
139+
{
140+
EnsureResourceTypeIsMappedInDiscriminator(schemaRepository, resourceType);
141+
}
142+
}
143+
}
144+
145+
private void EnsureResourceTypeIsMappedInDiscriminator(SchemaRepository schemaRepository, ResourceType resourceType)
146+
{
147+
Type resourceDataConstructedType = typeof(ResourceDataInResponse<>).MakeGenericType(resourceType.ClrType);
148+
OpenApiSchema referenceSchemaForResourceData = _dataSchemaGenerator.GenerateSchema(resourceDataConstructedType, schemaRepository);
149+
150+
_abstractResourceDataSchemaGenerator.MapDiscriminator(resourceDataConstructedType, referenceSchemaForResourceData, schemaRepository);
151+
}
152+
}

src/JsonApiDotNetCore.OpenApi/SwaggerComponents/AbstractResourceDataSchemaGenerator.cs renamed to src/JsonApiDotNetCore.OpenApi/SchemaGenerators/Components/AbstractResourceDataSchemaGenerator.cs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
using JsonApiDotNetCore.Configuration;
22
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
3+
using JsonApiDotNetCore.OpenApi.SwaggerComponents;
34
using Microsoft.OpenApi.Any;
45
using Microsoft.OpenApi.Interfaces;
56
using Microsoft.OpenApi.Models;
67
using Swashbuckle.AspNetCore.SwaggerGen;
78

8-
namespace JsonApiDotNetCore.OpenApi.SwaggerComponents;
9+
namespace JsonApiDotNetCore.OpenApi.SchemaGenerators.Components;
910

1011
internal sealed class AbstractResourceDataSchemaGenerator
1112
{
@@ -23,7 +24,7 @@ public AbstractResourceDataSchemaGenerator(JsonApiSchemaIdSelector schemaIdSelec
2324
_resourceGraph = resourceGraph;
2425
}
2526

26-
public OpenApiSchema Get(SchemaRepository schemaRepository)
27+
public OpenApiSchema GenerateSchema(SchemaRepository schemaRepository)
2728
{
2829
ArgumentGuard.NotNull(schemaRepository);
2930

@@ -67,16 +68,7 @@ public OpenApiSchema Get(SchemaRepository schemaRepository)
6768

6869
string schemaId = _schemaIdSelector.GetSchemaId(ResourceDataAbstractType);
6970

70-
referenceSchema = new OpenApiSchema
71-
{
72-
Reference = new OpenApiReference
73-
{
74-
Id = schemaId,
75-
Type = ReferenceType.Schema
76-
}
77-
};
78-
79-
schemaRepository.AddDefinition(schemaId, fullSchema);
71+
referenceSchema = schemaRepository.AddDefinition(schemaId, fullSchema);
8072
schemaRepository.RegisterType(ResourceDataAbstractType, schemaId);
8173

8274
return referenceSchema;

0 commit comments

Comments
 (0)