From 86ff1966e9d2ab26c7a6b5132e4c4fe8d4d50593 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sat, 23 Mar 2024 17:25:16 +0100 Subject: [PATCH] Add support for configuring the visibility of the "describedby" link --- .../Resources/Annotations/LinkTypes.shared.cs | 13 ++++++----- .../Annotations/RelationshipAttribute.cs | 2 +- .../RelationshipAttribute.netstandard.cs | 2 +- .../ResourceLinksAttribute.shared.cs | 6 ++--- .../Serialization/Response/LinkBuilder.cs | 13 +++++++---- .../UnitTests/Links/LinkInclusionTests.cs | 23 ++++++++++++++++++- 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/LinkTypes.shared.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/LinkTypes.shared.cs index 7e996828b9..632b8b9ed3 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/LinkTypes.shared.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/LinkTypes.shared.cs @@ -3,10 +3,11 @@ namespace JsonApiDotNetCore.Resources.Annotations; [Flags] public enum LinkTypes { - Self = 1 << 0, - Related = 1 << 1, - Pagination = 1 << 2, - NotConfigured = 1 << 3, - None = 1 << 4, - All = Self | Related | Pagination + NotConfigured = 0, + None = 1 << 0, + Self = 1 << 1, + Related = 1 << 2, + DescribedBy = 1 << 3, + Pagination = 1 << 4, + All = Self | Related | DescribedBy | Pagination } diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs index 0b4848ada1..492af08c60 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs @@ -70,7 +70,7 @@ internal set /// Configures which links to write in the relationship-level links object for this relationship. Defaults to , /// which falls back to and then falls back to RelationshipLinks in global options. /// - public LinkTypes Links { get; set; } = LinkTypes.NotConfigured; + public LinkTypes Links { get; set; } /// /// Whether or not this relationship can be included using the include query string parameter. This is true by default. diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.netstandard.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.netstandard.cs index d7af592564..054d7b1af3 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.netstandard.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.netstandard.cs @@ -9,7 +9,7 @@ namespace JsonApiDotNetCore.Resources.Annotations; public abstract class RelationshipAttribute : ResourceFieldAttribute { /// - public LinkTypes Links { get; set; } = LinkTypes.NotConfigured; + public LinkTypes Links { get; set; } /// [Obsolete("Use AllowInclude in Capabilities instead.")] diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceLinksAttribute.shared.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceLinksAttribute.shared.cs index 010f87db5e..2c2c353f3a 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceLinksAttribute.shared.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceLinksAttribute.shared.cs @@ -13,18 +13,18 @@ public sealed class ResourceLinksAttribute : Attribute /// Configures which links to write in the top-level links object for this resource type. Defaults to , which falls /// back to TopLevelLinks in global options. /// - public LinkTypes TopLevelLinks { get; set; } = LinkTypes.NotConfigured; + public LinkTypes TopLevelLinks { get; set; } /// /// Configures which links to write in the resource-level links object for this resource type. Defaults to , which /// falls back to ResourceLinks in global options. /// - public LinkTypes ResourceLinks { get; set; } = LinkTypes.NotConfigured; + public LinkTypes ResourceLinks { get; set; } /// /// Configures which links to write in the relationship-level links object for all relationships of this resource type. Defaults to /// , which falls back to RelationshipLinks in global options. This can be overruled per relationship by setting /// . /// - public LinkTypes RelationshipLinks { get; set; } = LinkTypes.NotConfigured; + public LinkTypes RelationshipLinks { get; set; } } diff --git a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs index 7740141002..ffb8b8e9b7 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs @@ -100,12 +100,15 @@ private static string NoAsyncSuffix(string actionName) SetPaginationInTopLevelLinks(resourceType!, links); } - string? documentDescriptionUrl = _documentDescriptionLinkProvider.GetUrl(); - - if (!string.IsNullOrEmpty(documentDescriptionUrl)) + if (ShouldIncludeTopLevelLink(LinkTypes.DescribedBy, resourceType)) { - var requestUri = new Uri(HttpContext.Request.GetEncodedUrl()); - links.DescribedBy = UriNormalizer.Normalize(documentDescriptionUrl, _options.UseRelativeLinks, requestUri); + string? documentDescriptionUrl = _documentDescriptionLinkProvider.GetUrl(); + + if (!string.IsNullOrEmpty(documentDescriptionUrl)) + { + var requestUri = new Uri(HttpContext.Request.GetEncodedUrl()); + links.DescribedBy = UriNormalizer.Normalize(documentDescriptionUrl, _options.UseRelativeLinks, requestUri); + } } return links.HasValue() ? links : null; diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs index 94b9cb9386..9b8890618a 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs @@ -22,36 +22,49 @@ public sealed class LinkInclusionTests [InlineData(LinkTypes.NotConfigured, LinkTypes.None, LinkTypes.None)] [InlineData(LinkTypes.NotConfigured, LinkTypes.Self, LinkTypes.Self)] [InlineData(LinkTypes.NotConfigured, LinkTypes.Related, LinkTypes.Related)] + [InlineData(LinkTypes.NotConfigured, LinkTypes.DescribedBy, LinkTypes.DescribedBy)] [InlineData(LinkTypes.NotConfigured, LinkTypes.Pagination, LinkTypes.Pagination)] [InlineData(LinkTypes.NotConfigured, LinkTypes.All, LinkTypes.All)] [InlineData(LinkTypes.None, LinkTypes.NotConfigured, LinkTypes.None)] [InlineData(LinkTypes.None, LinkTypes.None, LinkTypes.None)] [InlineData(LinkTypes.None, LinkTypes.Self, LinkTypes.None)] [InlineData(LinkTypes.None, LinkTypes.Related, LinkTypes.None)] + [InlineData(LinkTypes.None, LinkTypes.DescribedBy, LinkTypes.None)] [InlineData(LinkTypes.None, LinkTypes.Pagination, LinkTypes.None)] [InlineData(LinkTypes.None, LinkTypes.All, LinkTypes.None)] [InlineData(LinkTypes.Self, LinkTypes.NotConfigured, LinkTypes.Self)] [InlineData(LinkTypes.Self, LinkTypes.None, LinkTypes.Self)] [InlineData(LinkTypes.Self, LinkTypes.Self, LinkTypes.Self)] [InlineData(LinkTypes.Self, LinkTypes.Related, LinkTypes.Self)] + [InlineData(LinkTypes.Self, LinkTypes.DescribedBy, LinkTypes.Self)] [InlineData(LinkTypes.Self, LinkTypes.Pagination, LinkTypes.Self)] [InlineData(LinkTypes.Self, LinkTypes.All, LinkTypes.Self)] [InlineData(LinkTypes.Related, LinkTypes.NotConfigured, LinkTypes.Related)] [InlineData(LinkTypes.Related, LinkTypes.None, LinkTypes.Related)] [InlineData(LinkTypes.Related, LinkTypes.Self, LinkTypes.Related)] [InlineData(LinkTypes.Related, LinkTypes.Related, LinkTypes.Related)] + [InlineData(LinkTypes.Related, LinkTypes.DescribedBy, LinkTypes.Related)] [InlineData(LinkTypes.Related, LinkTypes.Pagination, LinkTypes.Related)] [InlineData(LinkTypes.Related, LinkTypes.All, LinkTypes.Related)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.NotConfigured, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.None, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.Self, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.Related, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.DescribedBy, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.Pagination, LinkTypes.DescribedBy)] + [InlineData(LinkTypes.DescribedBy, LinkTypes.All, LinkTypes.DescribedBy)] [InlineData(LinkTypes.Pagination, LinkTypes.NotConfigured, LinkTypes.Pagination)] [InlineData(LinkTypes.Pagination, LinkTypes.None, LinkTypes.Pagination)] [InlineData(LinkTypes.Pagination, LinkTypes.Self, LinkTypes.Pagination)] [InlineData(LinkTypes.Pagination, LinkTypes.Related, LinkTypes.Pagination)] + [InlineData(LinkTypes.Pagination, LinkTypes.DescribedBy, LinkTypes.Pagination)] [InlineData(LinkTypes.Pagination, LinkTypes.Pagination, LinkTypes.Pagination)] [InlineData(LinkTypes.Pagination, LinkTypes.All, LinkTypes.Pagination)] [InlineData(LinkTypes.All, LinkTypes.NotConfigured, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.None, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.Self, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.Related, LinkTypes.All)] + [InlineData(LinkTypes.All, LinkTypes.DescribedBy, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.Pagination, LinkTypes.All)] [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.All)] public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInResourceType, LinkTypes linksInOptions, LinkTypes expected) @@ -88,7 +101,7 @@ public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInReso var linkGenerator = new FakeLinkGenerator(); var controllerResourceMapping = new FakeControllerResourceMapping(); var paginationParser = new PaginationParser(); - var documentDescriptionLinkProvider = new NoDocumentDescriptionLinkProvider(); + var documentDescriptionLinkProvider = new NonEmptyDocumentDescriptionLinkProvider(); var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping, paginationParser, documentDescriptionLinkProvider); @@ -435,4 +448,12 @@ public override string GetUriByAddress(TAddress address, RouteValueDic throw new NotImplementedException(); } } + + private sealed class NonEmptyDocumentDescriptionLinkProvider : IDocumentDescriptionLinkProvider + { + public string GetUrl() + { + return "openapi.yaml"; + } + } }