Skip to content

Enable setting the "describedby" top-level link #1495

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

Merged
merged 1 commit into from
Mar 10, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ public void ConfigureServiceContainer(ICollection<Type> dbContextTypes)
_services.TryAddScoped<ISparseFieldSetCache, SparseFieldSetCache>();
_services.TryAddScoped<IQueryLayerComposer, QueryLayerComposer>();
_services.TryAddScoped<IInverseNavigationResolver, InverseNavigationResolver>();
_services.TryAddSingleton<IDocumentDescriptionLinkProvider, NoDocumentDescriptionLinkProvider>();
}

private void AddMiddlewareLayer()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using JsonApiDotNetCore.Configuration;

namespace JsonApiDotNetCore.Serialization.Response;

/// <summary>
/// Provides the value for the "describedby" link in https://jsonapi.org/format/#document-top-level.
/// </summary>
public interface IDocumentDescriptionLinkProvider
{
/// <summary>
/// Gets the URL for the "describedby" link, or <c>null</c> when unavailable.
/// </summary>
/// <remarks>
/// The returned URL can be absolute or relative. If possible, it gets converted based on <see cref="IJsonApiOptions.UseRelativeLinks" />.
/// </remarks>
string? GetUrl();
}
16 changes: 15 additions & 1 deletion src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ public class LinkBuilder : ILinkBuilder
private static readonly string GetRelationshipControllerActionName =
NoAsyncSuffix(nameof(BaseJsonApiController<Identifiable<int>, int>.GetRelationshipAsync));

private static readonly UriNormalizer UriNormalizer = new();

private readonly IJsonApiOptions _options;
private readonly IJsonApiRequest _request;
private readonly IPaginationContext _paginationContext;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly LinkGenerator _linkGenerator;
private readonly IControllerResourceMapping _controllerResourceMapping;
private readonly IPaginationParser _paginationParser;
private readonly IDocumentDescriptionLinkProvider _documentDescriptionLinkProvider;

private HttpContext HttpContext
{
Expand All @@ -50,14 +53,16 @@ private HttpContext HttpContext
}

public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, IHttpContextAccessor httpContextAccessor,
LinkGenerator linkGenerator, IControllerResourceMapping controllerResourceMapping, IPaginationParser paginationParser)
LinkGenerator linkGenerator, IControllerResourceMapping controllerResourceMapping, IPaginationParser paginationParser,
IDocumentDescriptionLinkProvider documentDescriptionLinkProvider)
{
ArgumentGuard.NotNull(options);
ArgumentGuard.NotNull(request);
ArgumentGuard.NotNull(paginationContext);
ArgumentGuard.NotNull(linkGenerator);
ArgumentGuard.NotNull(controllerResourceMapping);
ArgumentGuard.NotNull(paginationParser);
ArgumentGuard.NotNull(documentDescriptionLinkProvider);

_options = options;
_request = request;
Expand All @@ -66,6 +71,7 @@ public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPagination
_linkGenerator = linkGenerator;
_controllerResourceMapping = controllerResourceMapping;
_paginationParser = paginationParser;
_documentDescriptionLinkProvider = documentDescriptionLinkProvider;
}

private static string NoAsyncSuffix(string actionName)
Expand Down Expand Up @@ -94,6 +100,14 @@ private static string NoAsyncSuffix(string actionName)
SetPaginationInTopLevelLinks(resourceType!, links);
}

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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace JsonApiDotNetCore.Serialization.Response;

/// <summary>
/// Provides no value for the "describedby" link in https://jsonapi.org/format/#document-top-level.
/// </summary>
public sealed class NoDocumentDescriptionLinkProvider : IDocumentDescriptionLinkProvider
{
/// <summary>
/// Always returns <c>null</c>.
/// </summary>
public string? GetUrl()
{
return null;
}
}
80 changes: 80 additions & 0 deletions src/JsonApiDotNetCore/Serialization/Response/UriNormalizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace JsonApiDotNetCore.Serialization.Response;

internal sealed class UriNormalizer
{
/// <summary>
/// Converts a URL to absolute or relative format, if possible.
/// </summary>
/// <param name="sourceUrl">
/// The absolute or relative URL to normalize.
/// </param>
/// <param name="preferRelative">
/// Whether to convert <paramref name="sourceUrl" /> to absolute or relative format.
/// </param>
/// <param name="requestUri">
/// The URL of the current HTTP request, whose path and query string are discarded.
/// </param>
public string Normalize(string sourceUrl, bool preferRelative, Uri requestUri)
{
var sourceUri = new Uri(sourceUrl, UriKind.RelativeOrAbsolute);
Uri baseUri = RemovePathFromAbsoluteUri(requestUri);

if (!sourceUri.IsAbsoluteUri && !preferRelative)
{
var absoluteUri = new Uri(baseUri, sourceUrl);
return absoluteUri.AbsoluteUri;
}

if (sourceUri.IsAbsoluteUri && preferRelative)
{
if (AreSameServer(baseUri, sourceUri))
{
Uri relativeUri = baseUri.MakeRelativeUri(sourceUri);
return relativeUri.ToString();
}
}

return sourceUrl;
}

private static Uri RemovePathFromAbsoluteUri(Uri uri)
{
var requestUriBuilder = new UriBuilder(uri)
{
Path = null
};

return requestUriBuilder.Uri;
}

private static bool AreSameServer(Uri left, Uri right)
{
// Custom implementation because Uri.Equals() ignores the casing of username/password.

string leftScheme = left.GetComponents(UriComponents.Scheme, UriFormat.UriEscaped);
string rightScheme = right.GetComponents(UriComponents.Scheme, UriFormat.UriEscaped);

if (!string.Equals(leftScheme, rightScheme, StringComparison.OrdinalIgnoreCase))
{
return false;
}

string leftServer = left.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped);
string rightServer = right.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped);

if (!string.Equals(leftServer, rightServer, StringComparison.OrdinalIgnoreCase))
{
return false;
}

string leftUserInfo = left.GetComponents(UriComponents.UserInfo, UriFormat.UriEscaped);
string rightUserInfo = right.GetComponents(UriComponents.UserInfo, UriFormat.UriEscaped);

if (!string.Equals(leftUserInfo, rightUserInfo))
{
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
Expand Down Expand Up @@ -101,6 +102,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.ManyValue.ShouldHaveCount(1);

Expand Down Expand Up @@ -167,6 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

string albumLink = $"{HostPrefix}{PathPrefix}/photoAlbums/{photo.Album.StringId}";

Expand Down Expand Up @@ -211,6 +214,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.ManyValue.ShouldHaveCount(1);

Expand Down Expand Up @@ -259,6 +263,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.Should().BeNull();
Expand Down Expand Up @@ -293,6 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.ManyValue.ShouldHaveCount(1);
responseDocument.Data.ManyValue[0].Links.Should().BeNull();
Expand Down Expand Up @@ -354,6 +360,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
Expand Down Expand Up @@ -437,6 +444,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

string photoLink = $"{HostPrefix}{PathPrefix}/photos/{existingPhoto.StringId}";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
Expand Down Expand Up @@ -101,6 +102,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.ManyValue.ShouldHaveCount(1);

Expand Down Expand Up @@ -167,6 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

string albumLink = $"{HostPrefix}{PathPrefix}/photoAlbums/{photo.Album.StringId}";

Expand Down Expand Up @@ -211,6 +214,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.ManyValue.ShouldHaveCount(1);

Expand Down Expand Up @@ -259,6 +263,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.Should().BeNull();
Expand Down Expand Up @@ -293,6 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.ManyValue.ShouldHaveCount(1);
responseDocument.Data.ManyValue[0].Links.Should().BeNull();
Expand Down Expand Up @@ -354,6 +360,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
Expand Down Expand Up @@ -437,6 +444,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
responseDocument.Links.DescribedBy.Should().BeNull();

string photoLink = $"{HostPrefix}{PathPrefix}/photos/{existingPhoto.StringId}";

Expand Down
Loading