Skip to content

Commit 5ab8b22

Browse files
committed
Hide links that are inaccessible
1 parent 6b6cf38 commit 5ab8b22

19 files changed

+764
-14
lines changed

Diff for: src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInResponse.cs

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.ComponentModel.DataAnnotations;
21
using System.Text.Json.Serialization;
32
using JetBrains.Annotations;
43
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links;

Diff for: src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInResponse.cs

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.ComponentModel.DataAnnotations;
21
using System.Text.Json.Serialization;
32
using JetBrains.Annotations;
43
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links;

Diff for: src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,6 @@ private static void AddSchemaGenerators(IServiceCollection services)
9292
services.TryAddSingleton<ResourceIdentifierSchemaGenerator>();
9393
services.TryAddSingleton<AbstractResourceDataSchemaGenerator>();
9494
services.TryAddSingleton<ResourceDataSchemaGenerator>();
95+
services.TryAddSingleton<LinksVisibilitySchemaGenerator>();
9596
}
9697
}

Diff for: src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs

+9-4
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,27 @@ internal sealed class DocumentSchemaGenerator
3131
private readonly SchemaGenerator _defaultSchemaGenerator;
3232
private readonly AbstractResourceDataSchemaGenerator _abstractResourceDataSchemaGenerator;
3333
private readonly ResourceDataSchemaGenerator _resourceDataSchemaGenerator;
34+
private readonly LinksVisibilitySchemaGenerator _linksVisibilitySchemaGenerator;
3435
private readonly IncludeDependencyScanner _includeDependencyScanner;
3536
private readonly IResourceGraph _resourceGraph;
3637
private readonly IJsonApiOptions _options;
3738

3839
public DocumentSchemaGenerator(SchemaGenerator defaultSchemaGenerator, AbstractResourceDataSchemaGenerator abstractResourceDataSchemaGenerator,
39-
ResourceDataSchemaGenerator resourceDataSchemaGenerator, IncludeDependencyScanner includeDependencyScanner, IResourceGraph resourceGraph,
40-
IJsonApiOptions options)
40+
ResourceDataSchemaGenerator resourceDataSchemaGenerator, LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator,
41+
IncludeDependencyScanner includeDependencyScanner, IResourceGraph resourceGraph, IJsonApiOptions options)
4142
{
4243
ArgumentGuard.NotNull(defaultSchemaGenerator);
4344
ArgumentGuard.NotNull(abstractResourceDataSchemaGenerator);
4445
ArgumentGuard.NotNull(resourceDataSchemaGenerator);
46+
ArgumentGuard.NotNull(linksVisibilitySchemaGenerator);
4547
ArgumentGuard.NotNull(includeDependencyScanner);
4648
ArgumentGuard.NotNull(resourceGraph);
4749
ArgumentGuard.NotNull(options);
4850

4951
_defaultSchemaGenerator = defaultSchemaGenerator;
5052
_abstractResourceDataSchemaGenerator = abstractResourceDataSchemaGenerator;
5153
_resourceDataSchemaGenerator = resourceDataSchemaGenerator;
54+
_linksVisibilitySchemaGenerator = linksVisibilitySchemaGenerator;
5255
_includeDependencyScanner = includeDependencyScanner;
5356
_resourceGraph = resourceGraph;
5457
_options = options;
@@ -65,10 +68,12 @@ public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepos
6568

6669
OpenApiSchema fullSchemaForDocument = schemaRepository.Schemas[referenceSchemaForDocument.Reference.Id];
6770

68-
fullSchemaForDocument.SetValuesInMetaToNullable();
69-
7071
SetJsonApiVersion(fullSchemaForDocument, schemaRepository);
7172

73+
_linksVisibilitySchemaGenerator.UpdateSchemaForTopLevel(modelType, fullSchemaForDocument, schemaRepository);
74+
75+
fullSchemaForDocument.SetValuesInMetaToNullable();
76+
7277
return referenceSchemaForDocument;
7378
}
7479

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
3+
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
4+
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
5+
using JsonApiDotNetCore.Resources.Annotations;
6+
using Microsoft.OpenApi.Models;
7+
using Swashbuckle.AspNetCore.SwaggerGen;
8+
9+
namespace JsonApiDotNetCore.OpenApi.SwaggerComponents;
10+
11+
/// <summary>
12+
/// Hides links that are never returned.
13+
/// </summary>
14+
/// <remarks>
15+
/// Tradeoff: Special-casing links per resource type and per relationship means an explosion of expanded types, only because the links visibility may
16+
/// vary. Furthermore, relationship links fallback to their left-type resource, whereas we generate right-type component schemas for relationships. To
17+
/// keep it simple, we take the union of exposed links on resource types and relationships. Only what's not in this unification gets hidden. For example,
18+
/// when options == None, typeof(Blogs) == Self, and typeof(Posts) == Related, we'll keep Self | Related for both Blogs and Posts, and remove any other
19+
/// links.
20+
/// </remarks>
21+
internal sealed class LinksVisibilitySchemaGenerator
22+
{
23+
private const LinkTypes ResourceTopLinkTypes = LinkTypes.Self | LinkTypes.DescribedBy;
24+
private const LinkTypes ResourceCollectionTopLinkTypes = LinkTypes.Self | LinkTypes.DescribedBy | LinkTypes.Pagination;
25+
private const LinkTypes ResourceIdentifierTopLinkTypes = LinkTypes.Self | LinkTypes.Related | LinkTypes.DescribedBy;
26+
private const LinkTypes ResourceIdentifierCollectionTopLinkTypes = LinkTypes.Self | LinkTypes.Related | LinkTypes.DescribedBy | LinkTypes.Pagination;
27+
private const LinkTypes ErrorTopLinkTypes = LinkTypes.Self | LinkTypes.DescribedBy;
28+
private const LinkTypes RelationshipLinkTypes = LinkTypes.Self | LinkTypes.Related;
29+
private const LinkTypes ResourceLinkTypes = LinkTypes.Self;
30+
31+
private static readonly Dictionary<Type, LinkTypes> LinksInJsonApiComponentTypes = new()
32+
{
33+
[typeof(NullableSecondaryResourceResponseDocument<>)] = ResourceTopLinkTypes,
34+
[typeof(PrimaryResourceResponseDocument<>)] = ResourceTopLinkTypes,
35+
[typeof(SecondaryResourceResponseDocument<>)] = ResourceTopLinkTypes,
36+
[typeof(ResourceCollectionResponseDocument<>)] = ResourceCollectionTopLinkTypes,
37+
[typeof(ResourceIdentifierResponseDocument<>)] = ResourceIdentifierTopLinkTypes,
38+
[typeof(NullableResourceIdentifierResponseDocument<>)] = ResourceIdentifierTopLinkTypes,
39+
[typeof(ResourceIdentifierCollectionResponseDocument<>)] = ResourceIdentifierCollectionTopLinkTypes,
40+
[typeof(ErrorResponseDocument)] = ErrorTopLinkTypes,
41+
[typeof(NullableToOneRelationshipInResponse<>)] = RelationshipLinkTypes,
42+
[typeof(ToManyRelationshipInResponse<>)] = RelationshipLinkTypes,
43+
[typeof(ToOneRelationshipInResponse<>)] = RelationshipLinkTypes,
44+
[typeof(ResourceDataInResponse<>)] = ResourceLinkTypes
45+
};
46+
47+
private static readonly Dictionary<LinkTypes, List<string>> LinkTypeToPropertyNamesMap = new()
48+
{
49+
[LinkTypes.Self] = ["self"],
50+
[LinkTypes.Related] = ["related"],
51+
[LinkTypes.DescribedBy] = ["describedby"],
52+
[LinkTypes.Pagination] =
53+
[
54+
"first",
55+
"last",
56+
"prev",
57+
"next"
58+
]
59+
};
60+
61+
private readonly Lazy<LinksVisibility> _lazyLinksVisibility;
62+
63+
public LinksVisibilitySchemaGenerator(IResourceGraph resourceGraph, IJsonApiOptions options)
64+
{
65+
ArgumentGuard.NotNull(resourceGraph);
66+
ArgumentGuard.NotNull(options);
67+
68+
_lazyLinksVisibility = new Lazy<LinksVisibility>(() => new LinksVisibility(resourceGraph, options), LazyThreadSafetyMode.ExecutionAndPublication);
69+
}
70+
71+
public void UpdateSchemaForTopLevel(Type modelType, OpenApiSchema fullSchemaForLinksContainer, SchemaRepository schemaRepository)
72+
{
73+
ArgumentGuard.NotNull(modelType);
74+
ArgumentGuard.NotNull(fullSchemaForLinksContainer);
75+
76+
Type lookupType = modelType.IsConstructedGenericType ? modelType.GetGenericTypeDefinition() : modelType;
77+
78+
if (LinksInJsonApiComponentTypes.TryGetValue(lookupType, out LinkTypes possibleLinkTypes))
79+
{
80+
UpdateLinksProperty(fullSchemaForLinksContainer, _lazyLinksVisibility.Value.TopLevelLinks, possibleLinkTypes, schemaRepository);
81+
}
82+
}
83+
84+
public void UpdateSchemaForResource(ResourceTypeInfo resourceTypeInfo, OpenApiSchema fullSchemaForResourceData, SchemaRepository schemaRepository)
85+
{
86+
ArgumentGuard.NotNull(resourceTypeInfo);
87+
ArgumentGuard.NotNull(fullSchemaForResourceData);
88+
89+
if (LinksInJsonApiComponentTypes.TryGetValue(resourceTypeInfo.ResourceDataOpenType, out LinkTypes possibleLinkTypes))
90+
{
91+
UpdateLinksProperty(fullSchemaForResourceData, _lazyLinksVisibility.Value.ResourceLinks, possibleLinkTypes, schemaRepository);
92+
}
93+
}
94+
95+
public void UpdateSchemaForRelationship(Type modelType, OpenApiSchema fullSchemaForRelationship, SchemaRepository schemaRepository)
96+
{
97+
ArgumentGuard.NotNull(modelType);
98+
ArgumentGuard.NotNull(fullSchemaForRelationship);
99+
100+
Type lookupType = modelType.GetGenericTypeDefinition();
101+
102+
if (LinksInJsonApiComponentTypes.TryGetValue(lookupType, out LinkTypes possibleLinkTypes))
103+
{
104+
UpdateLinksProperty(fullSchemaForRelationship, _lazyLinksVisibility.Value.RelationshipLinks, possibleLinkTypes, schemaRepository);
105+
}
106+
}
107+
108+
private void UpdateLinksProperty(OpenApiSchema fullSchemaForLinksContainer, LinkTypes visibleLinkTypes, LinkTypes possibleLinkTypes,
109+
SchemaRepository schemaRepository)
110+
{
111+
if ((visibleLinkTypes & possibleLinkTypes) == 0)
112+
{
113+
fullSchemaForLinksContainer.Required.Remove(JsonApiPropertyName.Links);
114+
fullSchemaForLinksContainer.Properties.Remove(JsonApiPropertyName.Links);
115+
}
116+
else if (visibleLinkTypes != possibleLinkTypes)
117+
{
118+
OpenApiSchema referenceSchemaForLinks = fullSchemaForLinksContainer.Properties[JsonApiPropertyName.Links];
119+
string linksSchemaId = referenceSchemaForLinks.AllOf[0].Reference.Id;
120+
121+
if (schemaRepository.Schemas.TryGetValue(linksSchemaId, out OpenApiSchema? fullSchemaForLinks))
122+
{
123+
UpdateLinkProperties(fullSchemaForLinks, visibleLinkTypes);
124+
}
125+
}
126+
}
127+
128+
private void UpdateLinkProperties(OpenApiSchema fullSchemaForLinks, LinkTypes availableLinkTypes)
129+
{
130+
foreach (string propertyName in LinkTypeToPropertyNamesMap.Where(pair => !availableLinkTypes.HasFlag(pair.Key)).SelectMany(pair => pair.Value))
131+
{
132+
fullSchemaForLinks.Required.Remove(propertyName);
133+
fullSchemaForLinks.Properties.Remove(propertyName);
134+
}
135+
}
136+
137+
private sealed class LinksVisibility
138+
{
139+
public LinkTypes TopLevelLinks { get; }
140+
public LinkTypes ResourceLinks { get; }
141+
public LinkTypes RelationshipLinks { get; }
142+
143+
public LinksVisibility(IResourceGraph resourceGraph, IJsonApiOptions options)
144+
{
145+
ArgumentGuard.NotNull(resourceGraph);
146+
ArgumentGuard.NotNull(options);
147+
148+
var unionTopLevelLinks = LinkTypes.None;
149+
var unionResourceLinks = LinkTypes.None;
150+
var unionRelationshipLinks = LinkTypes.None;
151+
152+
foreach (ResourceType resourceType in resourceGraph.GetResourceTypes())
153+
{
154+
LinkTypes topLevelLinks = GetTopLevelLinks(resourceType, options);
155+
unionTopLevelLinks |= topLevelLinks;
156+
157+
LinkTypes resourceLinks = GetResourceLinks(resourceType, options);
158+
unionResourceLinks |= resourceLinks;
159+
160+
LinkTypes relationshipLinks = GetRelationshipLinks(resourceType, options);
161+
unionRelationshipLinks |= relationshipLinks;
162+
}
163+
164+
TopLevelLinks = Normalize(unionTopLevelLinks);
165+
ResourceLinks = Normalize(unionResourceLinks);
166+
RelationshipLinks = Normalize(unionRelationshipLinks);
167+
}
168+
169+
private LinkTypes GetTopLevelLinks(ResourceType resourceType, IJsonApiOptions options)
170+
{
171+
return resourceType.TopLevelLinks != LinkTypes.NotConfigured ? resourceType.TopLevelLinks :
172+
options.TopLevelLinks == LinkTypes.NotConfigured ? LinkTypes.None : options.TopLevelLinks;
173+
}
174+
175+
private LinkTypes GetResourceLinks(ResourceType resourceType, IJsonApiOptions options)
176+
{
177+
return resourceType.ResourceLinks != LinkTypes.NotConfigured ? resourceType.ResourceLinks :
178+
options.ResourceLinks == LinkTypes.NotConfigured ? LinkTypes.None : options.ResourceLinks;
179+
}
180+
181+
private LinkTypes GetRelationshipLinks(ResourceType resourceType, IJsonApiOptions options)
182+
{
183+
LinkTypes unionRelationshipLinks = resourceType.RelationshipLinks != LinkTypes.NotConfigured ? resourceType.RelationshipLinks :
184+
options.RelationshipLinks == LinkTypes.NotConfigured ? LinkTypes.None : options.RelationshipLinks;
185+
186+
foreach (RelationshipAttribute relationship in resourceType.Relationships)
187+
{
188+
LinkTypes relationshipLinks = relationship.Links != LinkTypes.NotConfigured ? relationship.Links :
189+
relationship.LeftType.RelationshipLinks != LinkTypes.NotConfigured ? relationship.LeftType.RelationshipLinks :
190+
options.RelationshipLinks == LinkTypes.NotConfigured ? LinkTypes.None : options.RelationshipLinks;
191+
192+
unionRelationshipLinks |= relationshipLinks;
193+
}
194+
195+
return unionRelationshipLinks;
196+
}
197+
198+
private static LinkTypes Normalize(LinkTypes linkTypes)
199+
{
200+
return linkTypes != LinkTypes.None ? linkTypes & ~LinkTypes.None : linkTypes;
201+
}
202+
}
203+
}

Diff for: src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceDataSchemaGenerator.cs

+11-6
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,22 @@ internal sealed class ResourceDataSchemaGenerator
2222
private readonly SchemaGenerator _defaultSchemaGenerator;
2323
private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator;
2424
private readonly ResourceIdentifierSchemaGenerator _resourceIdentifierSchemaGenerator;
25+
private readonly LinksVisibilitySchemaGenerator _linksVisibilitySchemaGenerator;
2526
private readonly IResourceGraph _resourceGraph;
2627
private readonly IJsonApiOptions _options;
2728
private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider;
2829
private readonly RelationshipTypeFactory _relationshipTypeFactory;
2930
private readonly ResourceDocumentationReader _resourceDocumentationReader;
3031

3132
public ResourceDataSchemaGenerator(SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator,
32-
ResourceIdentifierSchemaGenerator resourceIdentifierSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options,
33-
ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider, RelationshipTypeFactory relationshipTypeFactory,
34-
ResourceDocumentationReader resourceDocumentationReader)
33+
ResourceIdentifierSchemaGenerator resourceIdentifierSchemaGenerator, LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator,
34+
IResourceGraph resourceGraph, IJsonApiOptions options, ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider,
35+
RelationshipTypeFactory relationshipTypeFactory, ResourceDocumentationReader resourceDocumentationReader)
3536
{
3637
ArgumentGuard.NotNull(defaultSchemaGenerator);
3738
ArgumentGuard.NotNull(resourceTypeSchemaGenerator);
3839
ArgumentGuard.NotNull(resourceIdentifierSchemaGenerator);
40+
ArgumentGuard.NotNull(linksVisibilitySchemaGenerator);
3941
ArgumentGuard.NotNull(resourceGraph);
4042
ArgumentGuard.NotNull(options);
4143
ArgumentGuard.NotNull(resourceFieldValidationMetadataProvider);
@@ -45,6 +47,7 @@ public ResourceDataSchemaGenerator(SchemaGenerator defaultSchemaGenerator, Resou
4547
_defaultSchemaGenerator = defaultSchemaGenerator;
4648
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;
4749
_resourceIdentifierSchemaGenerator = resourceIdentifierSchemaGenerator;
50+
_linksVisibilitySchemaGenerator = linksVisibilitySchemaGenerator;
4851
_resourceGraph = resourceGraph;
4952
_options = options;
5053
_resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider;
@@ -68,7 +71,7 @@ public OpenApiSchema GenerateSchema(Type resourceDataConstructedType, SchemaRepo
6871

6972
var resourceTypeInfo = ResourceTypeInfo.Create(resourceDataConstructedType, _resourceGraph);
7073

71-
var fieldSchemaBuilder = new ResourceFieldSchemaBuilder(_defaultSchemaGenerator, _resourceIdentifierSchemaGenerator,
74+
var fieldSchemaBuilder = new ResourceFieldSchemaBuilder(_defaultSchemaGenerator, _resourceIdentifierSchemaGenerator, _linksVisibilitySchemaGenerator,
7275
_resourceFieldValidationMetadataProvider, _relationshipTypeFactory, resourceTypeInfo);
7376

7477
OpenApiSchema effectiveFullSchemaForResourceData =
@@ -82,11 +85,13 @@ public OpenApiSchema GenerateSchema(Type resourceDataConstructedType, SchemaRepo
8285

8386
fullSchemaForResourceData.Description = _resourceDocumentationReader.GetDocumentationForType(resourceTypeInfo.ResourceType);
8487

85-
effectiveFullSchemaForResourceData.SetValuesInMetaToNullable();
86-
8788
SetResourceAttributes(effectiveFullSchemaForResourceData, fieldSchemaBuilder, schemaRepository);
8889
SetResourceRelationships(effectiveFullSchemaForResourceData, fieldSchemaBuilder, schemaRepository);
8990

91+
_linksVisibilitySchemaGenerator.UpdateSchemaForResource(resourceTypeInfo, effectiveFullSchemaForResourceData, schemaRepository);
92+
93+
effectiveFullSchemaForResourceData.SetValuesInMetaToNullable();
94+
9095
effectiveFullSchemaForResourceData.ReorderProperties(ResourceDataPropertyNamesInOrder);
9196

9297
return referenceSchemaForResourceData;

Diff for: src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldSchemaBuilder.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ internal sealed class ResourceFieldSchemaBuilder
3333

3434
private readonly SchemaGenerator _defaultSchemaGenerator;
3535
private readonly ResourceIdentifierSchemaGenerator _resourceIdentifierSchemaGenerator;
36+
private readonly LinksVisibilitySchemaGenerator _linksVisibilitySchemaGenerator;
3637
private readonly ResourceTypeInfo _resourceTypeInfo;
3738
private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider;
3839
private readonly RelationshipTypeFactory _relationshipTypeFactory;
@@ -42,17 +43,19 @@ internal sealed class ResourceFieldSchemaBuilder
4243
private readonly IDictionary<string, OpenApiSchema> _schemasForResourceFields;
4344

4445
public ResourceFieldSchemaBuilder(SchemaGenerator defaultSchemaGenerator, ResourceIdentifierSchemaGenerator resourceIdentifierSchemaGenerator,
45-
ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider, RelationshipTypeFactory relationshipTypeFactory,
46-
ResourceTypeInfo resourceTypeInfo)
46+
LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator, ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider,
47+
RelationshipTypeFactory relationshipTypeFactory, ResourceTypeInfo resourceTypeInfo)
4748
{
4849
ArgumentGuard.NotNull(defaultSchemaGenerator);
4950
ArgumentGuard.NotNull(resourceIdentifierSchemaGenerator);
51+
ArgumentGuard.NotNull(linksVisibilitySchemaGenerator);
5052
ArgumentGuard.NotNull(resourceTypeInfo);
5153
ArgumentGuard.NotNull(resourceFieldValidationMetadataProvider);
5254
ArgumentGuard.NotNull(relationshipTypeFactory);
5355

5456
_defaultSchemaGenerator = defaultSchemaGenerator;
5557
_resourceIdentifierSchemaGenerator = resourceIdentifierSchemaGenerator;
58+
_linksVisibilitySchemaGenerator = linksVisibilitySchemaGenerator;
5659
_resourceTypeInfo = resourceTypeInfo;
5760
_resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider;
5861
_relationshipTypeFactory = relationshipTypeFactory;
@@ -216,6 +219,8 @@ private OpenApiSchema CreateRelationshipReferenceSchema(Type relationshipSchemaT
216219

217220
if (IsRelationshipInResponseType(relationshipSchemaType))
218221
{
222+
_linksVisibilitySchemaGenerator.UpdateSchemaForRelationship(relationshipSchemaType, fullSchema, schemaRepository);
223+
219224
fullSchema.Required.Remove(JsonApiPropertyName.Data);
220225

221226
fullSchema.SetValuesInMetaToNullable();

0 commit comments

Comments
 (0)