Skip to content

Commit

Permalink
Hide links that are inaccessible
Browse files Browse the repository at this point in the history
  • Loading branch information
bkoelman committed Mar 27, 2024
1 parent 58b4ab6 commit 31de8b0
Show file tree
Hide file tree
Showing 19 changed files with 764 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,6 @@ private static void AddSchemaGenerators(IServiceCollection services)
services.TryAddSingleton<ResourceIdentifierSchemaGenerator>();
services.TryAddSingleton<AbstractResourceDataSchemaGenerator>();
services.TryAddSingleton<ResourceDataSchemaGenerator>();
services.TryAddSingleton<LinksVisibilitySchemaGenerator>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,27 @@ internal sealed class DocumentSchemaGenerator
private readonly SchemaGenerator _defaultSchemaGenerator;
private readonly AbstractResourceDataSchemaGenerator _abstractResourceDataSchemaGenerator;
private readonly ResourceDataSchemaGenerator _resourceDataSchemaGenerator;
private readonly LinksVisibilitySchemaGenerator _linksVisibilitySchemaGenerator;
private readonly IncludeDependencyScanner _includeDependencyScanner;
private readonly IResourceGraph _resourceGraph;
private readonly IJsonApiOptions _options;

public DocumentSchemaGenerator(SchemaGenerator defaultSchemaGenerator, AbstractResourceDataSchemaGenerator abstractResourceDataSchemaGenerator,
ResourceDataSchemaGenerator resourceDataSchemaGenerator, IncludeDependencyScanner includeDependencyScanner, IResourceGraph resourceGraph,
IJsonApiOptions options)
ResourceDataSchemaGenerator resourceDataSchemaGenerator, LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator,
IncludeDependencyScanner includeDependencyScanner, IResourceGraph resourceGraph, IJsonApiOptions options)
{
ArgumentGuard.NotNull(defaultSchemaGenerator);
ArgumentGuard.NotNull(abstractResourceDataSchemaGenerator);
ArgumentGuard.NotNull(resourceDataSchemaGenerator);
ArgumentGuard.NotNull(linksVisibilitySchemaGenerator);
ArgumentGuard.NotNull(includeDependencyScanner);
ArgumentGuard.NotNull(resourceGraph);
ArgumentGuard.NotNull(options);

_defaultSchemaGenerator = defaultSchemaGenerator;
_abstractResourceDataSchemaGenerator = abstractResourceDataSchemaGenerator;
_resourceDataSchemaGenerator = resourceDataSchemaGenerator;
_linksVisibilitySchemaGenerator = linksVisibilitySchemaGenerator;
_includeDependencyScanner = includeDependencyScanner;
_resourceGraph = resourceGraph;
_options = options;
Expand All @@ -65,10 +68,12 @@ public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepos

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

fullSchemaForDocument.SetValuesInMetaToNullable();

SetJsonApiVersion(fullSchemaForDocument, schemaRepository);

_linksVisibilitySchemaGenerator.UpdateSchemaForTopLevel(modelType, fullSchemaForDocument, schemaRepository);

fullSchemaForDocument.SetValuesInMetaToNullable();

return referenceSchemaForDocument;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
using JsonApiDotNetCore.Resources.Annotations;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace JsonApiDotNetCore.OpenApi.SwaggerComponents;

/// <summary>
/// Hides links that are never returned.
/// </summary>
/// <remarks>
/// Tradeoff: Special-casing links per resource type and per relationship means an explosion of expanded types, only because the links visibility may
/// vary. Furthermore, relationship links fallback to their left-type resource, whereas we generate right-type component schemas for relationships. To
/// 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,
/// when options == None, typeof(Blogs) == Self, and typeof(Posts) == Related, we'll keep Self | Related for both Blogs and Posts, and remove any other
/// links.
/// </remarks>
internal sealed class LinksVisibilitySchemaGenerator
{
private const LinkTypes ResourceTopLinkTypes = LinkTypes.Self | LinkTypes.DescribedBy;
private const LinkTypes ResourceCollectionTopLinkTypes = LinkTypes.Self | LinkTypes.DescribedBy | LinkTypes.Pagination;
private const LinkTypes ResourceIdentifierTopLinkTypes = LinkTypes.Self | LinkTypes.Related | LinkTypes.DescribedBy;
private const LinkTypes ResourceIdentifierCollectionTopLinkTypes = LinkTypes.Self | LinkTypes.Related | LinkTypes.DescribedBy | LinkTypes.Pagination;
private const LinkTypes ErrorTopLinkTypes = LinkTypes.Self | LinkTypes.DescribedBy;
private const LinkTypes RelationshipLinkTypes = LinkTypes.Self | LinkTypes.Related;
private const LinkTypes ResourceLinkTypes = LinkTypes.Self;

private static readonly Dictionary<Type, LinkTypes> LinksInJsonApiComponentTypes = new()
{
[typeof(NullableSecondaryResourceResponseDocument<>)] = ResourceTopLinkTypes,
[typeof(PrimaryResourceResponseDocument<>)] = ResourceTopLinkTypes,
[typeof(SecondaryResourceResponseDocument<>)] = ResourceTopLinkTypes,
[typeof(ResourceCollectionResponseDocument<>)] = ResourceCollectionTopLinkTypes,
[typeof(ResourceIdentifierResponseDocument<>)] = ResourceIdentifierTopLinkTypes,
[typeof(NullableResourceIdentifierResponseDocument<>)] = ResourceIdentifierTopLinkTypes,
[typeof(ResourceIdentifierCollectionResponseDocument<>)] = ResourceIdentifierCollectionTopLinkTypes,
[typeof(ErrorResponseDocument)] = ErrorTopLinkTypes,
[typeof(NullableToOneRelationshipInResponse<>)] = RelationshipLinkTypes,
[typeof(ToManyRelationshipInResponse<>)] = RelationshipLinkTypes,
[typeof(ToOneRelationshipInResponse<>)] = RelationshipLinkTypes,
[typeof(ResourceDataInResponse<>)] = ResourceLinkTypes
};

private static readonly Dictionary<LinkTypes, List<string>> LinkTypeToPropertyNamesMap = new()
{
[LinkTypes.Self] = ["self"],
[LinkTypes.Related] = ["related"],
[LinkTypes.DescribedBy] = ["describedby"],
[LinkTypes.Pagination] =
[
"first",
"last",
"prev",
"next"
]
};

private readonly Lazy<LinksVisibility> _lazyLinksVisibility;

public LinksVisibilitySchemaGenerator(IResourceGraph resourceGraph, IJsonApiOptions options)
{
ArgumentGuard.NotNull(resourceGraph);
ArgumentGuard.NotNull(options);

_lazyLinksVisibility = new Lazy<LinksVisibility>(() => new LinksVisibility(resourceGraph, options), LazyThreadSafetyMode.ExecutionAndPublication);
}

public void UpdateSchemaForTopLevel(Type modelType, OpenApiSchema fullSchemaForLinksContainer, SchemaRepository schemaRepository)
{
ArgumentGuard.NotNull(modelType);
ArgumentGuard.NotNull(fullSchemaForLinksContainer);

Type lookupType = modelType.IsConstructedGenericType ? modelType.GetGenericTypeDefinition() : modelType;

if (LinksInJsonApiComponentTypes.TryGetValue(lookupType, out LinkTypes possibleLinkTypes))
{
UpdateLinksProperty(fullSchemaForLinksContainer, _lazyLinksVisibility.Value.TopLevelLinks, possibleLinkTypes, schemaRepository);
}
}

public void UpdateSchemaForResource(ResourceTypeInfo resourceTypeInfo, OpenApiSchema fullSchemaForResourceData, SchemaRepository schemaRepository)
{
ArgumentGuard.NotNull(resourceTypeInfo);
ArgumentGuard.NotNull(fullSchemaForResourceData);

if (LinksInJsonApiComponentTypes.TryGetValue(resourceTypeInfo.ResourceDataOpenType, out LinkTypes possibleLinkTypes))
{
UpdateLinksProperty(fullSchemaForResourceData, _lazyLinksVisibility.Value.ResourceLinks, possibleLinkTypes, schemaRepository);
}
}

public void UpdateSchemaForRelationship(Type modelType, OpenApiSchema fullSchemaForRelationship, SchemaRepository schemaRepository)
{
ArgumentGuard.NotNull(modelType);
ArgumentGuard.NotNull(fullSchemaForRelationship);

Type lookupType = modelType.GetGenericTypeDefinition();

if (LinksInJsonApiComponentTypes.TryGetValue(lookupType, out LinkTypes possibleLinkTypes))
{
UpdateLinksProperty(fullSchemaForRelationship, _lazyLinksVisibility.Value.RelationshipLinks, possibleLinkTypes, schemaRepository);
}
}

private void UpdateLinksProperty(OpenApiSchema fullSchemaForLinksContainer, LinkTypes visibleLinkTypes, LinkTypes possibleLinkTypes,
SchemaRepository schemaRepository)
{
if ((visibleLinkTypes & possibleLinkTypes) == 0)
{
fullSchemaForLinksContainer.Required.Remove(JsonApiPropertyName.Links);
fullSchemaForLinksContainer.Properties.Remove(JsonApiPropertyName.Links);
}
else if (visibleLinkTypes != possibleLinkTypes)
{
OpenApiSchema referenceSchemaForLinks = fullSchemaForLinksContainer.Properties[JsonApiPropertyName.Links];
string linksSchemaId = referenceSchemaForLinks.AllOf[0].Reference.Id;

if (schemaRepository.Schemas.TryGetValue(linksSchemaId, out OpenApiSchema? fullSchemaForLinks))
{
UpdateLinkProperties(fullSchemaForLinks, visibleLinkTypes);
}
}
}

private void UpdateLinkProperties(OpenApiSchema fullSchemaForLinks, LinkTypes availableLinkTypes)
{
foreach (string propertyName in LinkTypeToPropertyNamesMap.Where(pair => !availableLinkTypes.HasFlag(pair.Key)).SelectMany(pair => pair.Value))
{
fullSchemaForLinks.Required.Remove(propertyName);
fullSchemaForLinks.Properties.Remove(propertyName);
}
}

private sealed class LinksVisibility
{
public LinkTypes TopLevelLinks { get; }
public LinkTypes ResourceLinks { get; }
public LinkTypes RelationshipLinks { get; }

public LinksVisibility(IResourceGraph resourceGraph, IJsonApiOptions options)
{
ArgumentGuard.NotNull(resourceGraph);
ArgumentGuard.NotNull(options);

var unionTopLevelLinks = LinkTypes.None;
var unionResourceLinks = LinkTypes.None;
var unionRelationshipLinks = LinkTypes.None;

foreach (ResourceType resourceType in resourceGraph.GetResourceTypes())
{
LinkTypes topLevelLinks = GetTopLevelLinks(resourceType, options);
unionTopLevelLinks |= topLevelLinks;

LinkTypes resourceLinks = GetResourceLinks(resourceType, options);
unionResourceLinks |= resourceLinks;

LinkTypes relationshipLinks = GetRelationshipLinks(resourceType, options);
unionRelationshipLinks |= relationshipLinks;
}

TopLevelLinks = Normalize(unionTopLevelLinks);
ResourceLinks = Normalize(unionResourceLinks);
RelationshipLinks = Normalize(unionRelationshipLinks);
}

private LinkTypes GetTopLevelLinks(ResourceType resourceType, IJsonApiOptions options)
{
return resourceType.TopLevelLinks != LinkTypes.NotConfigured ? resourceType.TopLevelLinks :
options.TopLevelLinks == LinkTypes.NotConfigured ? LinkTypes.None : options.TopLevelLinks;
}

private LinkTypes GetResourceLinks(ResourceType resourceType, IJsonApiOptions options)
{
return resourceType.ResourceLinks != LinkTypes.NotConfigured ? resourceType.ResourceLinks :
options.ResourceLinks == LinkTypes.NotConfigured ? LinkTypes.None : options.ResourceLinks;
}

private LinkTypes GetRelationshipLinks(ResourceType resourceType, IJsonApiOptions options)
{
LinkTypes unionRelationshipLinks = resourceType.RelationshipLinks != LinkTypes.NotConfigured ? resourceType.RelationshipLinks :
options.RelationshipLinks == LinkTypes.NotConfigured ? LinkTypes.None : options.RelationshipLinks;

foreach (RelationshipAttribute relationship in resourceType.Relationships)
{
LinkTypes relationshipLinks = relationship.Links != LinkTypes.NotConfigured ? relationship.Links :
relationship.LeftType.RelationshipLinks != LinkTypes.NotConfigured ? relationship.LeftType.RelationshipLinks :
options.RelationshipLinks == LinkTypes.NotConfigured ? LinkTypes.None : options.RelationshipLinks;

unionRelationshipLinks |= relationshipLinks;
}

return unionRelationshipLinks;
}

private static LinkTypes Normalize(LinkTypes linkTypes)
{
return linkTypes != LinkTypes.None ? linkTypes & ~LinkTypes.None : linkTypes;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,22 @@ internal sealed class ResourceDataSchemaGenerator
private readonly SchemaGenerator _defaultSchemaGenerator;
private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator;
private readonly ResourceIdentifierSchemaGenerator _resourceIdentifierSchemaGenerator;
private readonly LinksVisibilitySchemaGenerator _linksVisibilitySchemaGenerator;
private readonly IResourceGraph _resourceGraph;
private readonly IJsonApiOptions _options;
private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider;
private readonly RelationshipTypeFactory _relationshipTypeFactory;
private readonly ResourceDocumentationReader _resourceDocumentationReader;

public ResourceDataSchemaGenerator(SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator,
ResourceIdentifierSchemaGenerator resourceIdentifierSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options,
ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider, RelationshipTypeFactory relationshipTypeFactory,
ResourceDocumentationReader resourceDocumentationReader)
ResourceIdentifierSchemaGenerator resourceIdentifierSchemaGenerator, LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator,
IResourceGraph resourceGraph, IJsonApiOptions options, ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider,
RelationshipTypeFactory relationshipTypeFactory, ResourceDocumentationReader resourceDocumentationReader)
{
ArgumentGuard.NotNull(defaultSchemaGenerator);
ArgumentGuard.NotNull(resourceTypeSchemaGenerator);
ArgumentGuard.NotNull(resourceIdentifierSchemaGenerator);
ArgumentGuard.NotNull(linksVisibilitySchemaGenerator);
ArgumentGuard.NotNull(resourceGraph);
ArgumentGuard.NotNull(options);
ArgumentGuard.NotNull(resourceFieldValidationMetadataProvider);
Expand All @@ -45,6 +47,7 @@ public ResourceDataSchemaGenerator(SchemaGenerator defaultSchemaGenerator, Resou
_defaultSchemaGenerator = defaultSchemaGenerator;
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;
_resourceIdentifierSchemaGenerator = resourceIdentifierSchemaGenerator;
_linksVisibilitySchemaGenerator = linksVisibilitySchemaGenerator;
_resourceGraph = resourceGraph;
_options = options;
_resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider;
Expand All @@ -68,7 +71,7 @@ public OpenApiSchema GenerateSchema(Type resourceDataConstructedType, SchemaRepo

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

var fieldSchemaBuilder = new ResourceFieldSchemaBuilder(_defaultSchemaGenerator, _resourceIdentifierSchemaGenerator,
var fieldSchemaBuilder = new ResourceFieldSchemaBuilder(_defaultSchemaGenerator, _resourceIdentifierSchemaGenerator, _linksVisibilitySchemaGenerator,
_resourceFieldValidationMetadataProvider, _relationshipTypeFactory, resourceTypeInfo);

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

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

effectiveFullSchemaForResourceData.SetValuesInMetaToNullable();

SetResourceAttributes(effectiveFullSchemaForResourceData, fieldSchemaBuilder, schemaRepository);
SetResourceRelationships(effectiveFullSchemaForResourceData, fieldSchemaBuilder, schemaRepository);

_linksVisibilitySchemaGenerator.UpdateSchemaForResource(resourceTypeInfo, effectiveFullSchemaForResourceData, schemaRepository);

effectiveFullSchemaForResourceData.SetValuesInMetaToNullable();

effectiveFullSchemaForResourceData.ReorderProperties(ResourceDataPropertyNamesInOrder);

return referenceSchemaForResourceData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ internal sealed class ResourceFieldSchemaBuilder

private readonly SchemaGenerator _defaultSchemaGenerator;
private readonly ResourceIdentifierSchemaGenerator _resourceIdentifierSchemaGenerator;
private readonly LinksVisibilitySchemaGenerator _linksVisibilitySchemaGenerator;
private readonly ResourceTypeInfo _resourceTypeInfo;
private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider;
private readonly RelationshipTypeFactory _relationshipTypeFactory;
Expand All @@ -42,17 +43,19 @@ internal sealed class ResourceFieldSchemaBuilder
private readonly IDictionary<string, OpenApiSchema> _schemasForResourceFields;

public ResourceFieldSchemaBuilder(SchemaGenerator defaultSchemaGenerator, ResourceIdentifierSchemaGenerator resourceIdentifierSchemaGenerator,
ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider, RelationshipTypeFactory relationshipTypeFactory,
ResourceTypeInfo resourceTypeInfo)
LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator, ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider,
RelationshipTypeFactory relationshipTypeFactory, ResourceTypeInfo resourceTypeInfo)
{
ArgumentGuard.NotNull(defaultSchemaGenerator);
ArgumentGuard.NotNull(resourceIdentifierSchemaGenerator);
ArgumentGuard.NotNull(linksVisibilitySchemaGenerator);
ArgumentGuard.NotNull(resourceTypeInfo);
ArgumentGuard.NotNull(resourceFieldValidationMetadataProvider);
ArgumentGuard.NotNull(relationshipTypeFactory);

_defaultSchemaGenerator = defaultSchemaGenerator;
_resourceIdentifierSchemaGenerator = resourceIdentifierSchemaGenerator;
_linksVisibilitySchemaGenerator = linksVisibilitySchemaGenerator;
_resourceTypeInfo = resourceTypeInfo;
_resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider;
_relationshipTypeFactory = relationshipTypeFactory;
Expand Down Expand Up @@ -216,6 +219,8 @@ private OpenApiSchema CreateRelationshipReferenceSchema(Type relationshipSchemaT

if (IsRelationshipInResponseType(relationshipSchemaType))
{
_linksVisibilitySchemaGenerator.UpdateSchemaForRelationship(relationshipSchemaType, fullSchema, schemaRepository);

fullSchema.Required.Remove(JsonApiPropertyName.Data);

fullSchema.SetValuesInMetaToNullable();
Expand Down
Loading

0 comments on commit 31de8b0

Please sign in to comment.