diff --git a/couscous.yml b/couscous.yml index 90f2b9b095..7cf0b54ea4 100644 --- a/couscous.yml +++ b/couscous.yml @@ -78,6 +78,9 @@ menu: filtering: text: Filtering relativeUrl: filtering.html + includingrelationships: + text: Including Relationships + relativeUrl: includingrelationships.html pagination: text: Pagination relativeUrl: pagination.html diff --git a/docs/IncludingRelationships.md b/docs/IncludingRelationships.md new file mode 100644 index 0000000000..e11657ebdf --- /dev/null +++ b/docs/IncludingRelationships.md @@ -0,0 +1,54 @@ +--- +currentMenu: includingrelationships +--- + +# Including Relationships + +JADNC supports [request include params](http://jsonapi-resources.com/v0.9/guide/resources.html#Included-relationships-side-loading-resources) out of the box, for side loading related resources. + +Here’s an example from the spec: + +```http +GET /articles/1?include=comments HTTP/1.1 +Accept: application/vnd.api+json +``` + +Will get you the following payload: + +```json +{ + "data": { + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON API paints my bikeshed!" + }, + "relationships": { + "comments": { + "links": { + "self": "http://example.com/articles/1/relationships/comments", + "related": "http://example.com/articles/1/comments" + }, + "data": [ + { "type": "comments", "id": "5" }, + { "type": "comments", "id": "12" } + ] + } + } + }, + "included": [{ + "type": "comments", + "id": "5", + "attributes": { + "body": "First!" + } + }, { + "type": "comments", + "id": "12", + "attributes": { + "body": "I like XML better" + } + }] +} +``` + diff --git a/docs/Options.md b/docs/Options.md index ffba0bd685..681c502557 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -45,3 +45,39 @@ public IServiceProvider ConfigureServices(IServiceCollection services) { // ... } ``` + +## Relative Links + +All links are absolute by default. However, you can configure relative links: + +```csharp +public IServiceProvider ConfigureServices(IServiceCollection services) { + services.AddJsonApi( + opt => opt.RelativeLinks = true); + // ... +} +``` + + +```http +GET /api/v1/articles/4309 HTTP/1.1 +Accept: application/vnd.api+json +``` + +```json +{ + "type": "articles", + "id": "4309", + "attributes": { + "name": "Voluptas iure est molestias." + }, + "relationships": { + "author": { + "links": { + "self": "/api/v1/articles/4309/relationships/author", + "related": "/api/v1/articles/4309/author" + } + } + } +} +``` \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs index 83b77c169c..dced1225a9 100644 --- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs @@ -9,28 +9,30 @@ public class LinkBuilder public LinkBuilder(IJsonApiContext context) { - _context = context; + _context = context; } public string GetBasePath(HttpContext context, string entityName) { var r = context.Request; - return $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; + return (_context.Options.RelativeLinks) + ? $"{GetNamespaceFromPath(r.Path, entityName)}" + : $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; } private string GetNamespaceFromPath(string path, string entityName) { var nSpace = string.Empty; var segments = path.Split('/'); - - for(var i = 1; i < segments.Length; i++) + + for (var i = 1; i < segments.Length; i++) { - if(segments[i].ToLower() == entityName) + if (segments[i].ToLower() == entityName) break; nSpace += $"/{segments[i]}"; } - + return nSpace; } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index d651845773..2e24d52d53 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -14,6 +14,7 @@ public class JsonApiOptions public bool IncludeTotalRecordCount { get; set; } public bool AllowClientGeneratedIds { get; set; } public IContextGraph ContextGraph { get; set; } + public bool RelativeLinks { get; set; } public IContractResolver JsonContractResolver { get; set; } = new DasherizedResolver(); internal IContextGraphBuilder ContextGraphBuilder { get; } = new ContextGraphBuilder(); diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 80f2a0cd77..30ddeca2ae 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@  - 2.1.3 + 2.1.4 netstandard1.6 JsonApiDotNetCore JsonApiDotNetCore diff --git a/test/UnitTests/Builders/LinkBuilder_Tests.cs b/test/UnitTests/Builders/LinkBuilder_Tests.cs new file mode 100644 index 0000000000..69b135de03 --- /dev/null +++ b/test/UnitTests/Builders/LinkBuilder_Tests.cs @@ -0,0 +1,48 @@ +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; +using Moq; +using Xunit; + +namespace UnitTests +{ + public class LinkBuilder_Tests + { + [Theory] + [InlineData("http", "localhost", "/api/v1/articles", false, "http://localhost/api/v1")] + [InlineData("https", "localhost", "/api/v1/articles", false, "https://localhost/api/v1")] + [InlineData("http", "example.com", "/api/v1/articles", false, "http://example.com/api/v1")] + [InlineData("https", "example.com", "/api/v1/articles", false, "https://example.com/api/v1")] + [InlineData("https", "example.com", "/articles", false, "https://example.com")] + [InlineData("https", "example.com", "/articles", true, "")] + [InlineData("https", "example.com", "/api/v1/articles", true, "/api/v1")] + public void GetBasePath_Returns_Path_Before_Resource(string scheme, + string host, string path, bool isRelative, string expectedPath) + { + // arrange + const string resource = "articles"; + var jsonApiContextMock = new Mock(); + jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions + { + RelativeLinks = isRelative + }); + + var requestMock = new Mock(); + requestMock.Setup(m => m.Scheme).Returns(scheme); + requestMock.Setup(m => m.Host).Returns(new HostString(host)); + requestMock.Setup(m => m.Path).Returns(new PathString(path)); + + var contextMock = new Mock(); + contextMock.Setup(m => m.Request).Returns(requestMock.Object); + + var linkBuilder = new LinkBuilder(jsonApiContextMock.Object); + + // act + var basePath = linkBuilder.GetBasePath(contextMock.Object, resource); + + // assert + Assert.Equal(expectedPath, basePath); + } + } +}