diff --git a/CHANGELOG.md b/CHANGELOG.md index 52fbd95..362e1a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +## [4.1.0] - 2024-06-26 + +### Fixed + +- [#17](https://github.com/laravel-json-api/core/pull/17) Fix incorrect `self` link in related resource responses, and + remove `related` link that should not exist. This has been incorrect for some time, but is definitely what + the [spec defines here.](https://jsonapi.org/format/1.0/#document-top-level) + ## [4.0.0] - 2024-03-12 ### Changed diff --git a/src/Core/Document/Links.php b/src/Core/Document/Links.php index 1d07f50..fdbd363 100644 --- a/src/Core/Document/Links.php +++ b/src/Core/Document/Links.php @@ -191,6 +191,25 @@ public function hasRelated(): bool return $this->has('related'); } + /** + * @return $this + */ + public function relatedAsSelf(): self + { + $related = $this->getRelated(); + + if ($related) { + $this->push(new Link( + key: 'self', + href: $related->href(), + meta: $related->meta(), + )); + return $this->forget('related'); + } + + return $this->forget('self'); + } + /** * Push links into the collection. * diff --git a/src/Core/Responses/Internal/RelatedResourceCollectionResponse.php b/src/Core/Responses/Internal/RelatedResourceCollectionResponse.php index db33285..14e9b0b 100644 --- a/src/Core/Responses/Internal/RelatedResourceCollectionResponse.php +++ b/src/Core/Responses/Internal/RelatedResourceCollectionResponse.php @@ -14,6 +14,7 @@ use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\Request; use Illuminate\Http\Response; +use LaravelJsonApi\Core\Document\Links; use LaravelJsonApi\Core\Resources\JsonApiResource; use LaravelJsonApi\Core\Responses\Concerns\HasEncodingParameters; use LaravelJsonApi\Core\Responses\Concerns\HasRelationship; @@ -68,4 +69,17 @@ public function toResponse($request) $this->headers() ); } + + /** + * Get all links. + * + * @return Links + */ + private function allLinks(): Links + { + return $this + ->linksForRelationship() + ->relatedAsSelf() + ->merge($this->links()); + } } diff --git a/src/Core/Responses/Internal/RelatedResourceResponse.php b/src/Core/Responses/Internal/RelatedResourceResponse.php index d03b896..0b9ea61 100644 --- a/src/Core/Responses/Internal/RelatedResourceResponse.php +++ b/src/Core/Responses/Internal/RelatedResourceResponse.php @@ -14,6 +14,7 @@ use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\Request; use Illuminate\Http\Response; +use LaravelJsonApi\Core\Document\Links; use LaravelJsonApi\Core\Resources\JsonApiResource; use LaravelJsonApi\Core\Responses\Concerns\HasEncodingParameters; use LaravelJsonApi\Core\Responses\Concerns\HasRelationship; @@ -52,6 +53,7 @@ public function toResponse($request) { $encoder = $this->server()->encoder(); + $links = $this->allLinks(); $document = $encoder ->withRequest($request) ->withIncludePaths($this->includePaths($request)) @@ -68,4 +70,17 @@ public function toResponse($request) $this->headers() ); } + + /** + * Get all links. + * + * @return Links + */ + private function allLinks(): Links + { + return $this + ->linksForRelationship() + ->relatedAsSelf() + ->merge($this->links()); + } } diff --git a/src/Core/Schema/Concerns/MatchesIds.php b/src/Core/Schema/Concerns/MatchesIds.php index 90f6346..ed40a34 100644 --- a/src/Core/Schema/Concerns/MatchesIds.php +++ b/src/Core/Schema/Concerns/MatchesIds.php @@ -13,7 +13,6 @@ trait MatchesIds { - /** * @var string */ @@ -39,7 +38,7 @@ public function pattern(): string * * @return $this */ - public function uuid(): self + public function uuid(): static { return $this->matchAs('[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}'); } @@ -49,7 +48,7 @@ public function uuid(): self * * @return $this */ - public function ulid(): self + public function ulid(): static { return $this->matchAs('[0-7][0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{25}'); } @@ -60,7 +59,7 @@ public function ulid(): self * @param string $pattern * @return $this */ - public function matchAs(string $pattern): self + public function matchAs(string $pattern): static { $this->pattern = $pattern; @@ -72,7 +71,7 @@ public function matchAs(string $pattern): self * * @return $this */ - public function matchCase(): self + public function matchCase(): static { $this->flags = 'D'; diff --git a/tests/Unit/Document/LinksTest.php b/tests/Unit/Document/LinksTest.php index 0fe8ead..c23073d 100644 --- a/tests/Unit/Document/LinksTest.php +++ b/tests/Unit/Document/LinksTest.php @@ -294,4 +294,23 @@ public function testOffsetUnset(): void $this->assertSame(['related' => $related], $links->all()); } + + public function testRelatedToSelfWithRelated(): void + { + $links = new Links( + new Link('self', '/api/posts/1/relationships/author'), + new Link('related', '/api/posts/1/author'), + ); + + $this->assertEquals(['self' => new Link('self', '/api/posts/1/author'),], $links->relatedAsSelf()->all()); + } + + public function testRelatedToSelfWithoutRelated(): void + { + $links = new Links( + new Link('self', '/api/posts/1/relationships/author'), + ); + + $this->assertTrue($links->relatedAsSelf()->isEmpty()); + } } diff --git a/tests/Unit/Schema/Concerns/MatchesIdsTest.php b/tests/Unit/Schema/Concerns/MatchesIdsTest.php new file mode 100644 index 0000000..2628ed5 --- /dev/null +++ b/tests/Unit/Schema/Concerns/MatchesIdsTest.php @@ -0,0 +1,82 @@ +assertSame('[0-9]+', $id->pattern()); + $this->assertTrue($id->match('1234')); + $this->assertFalse($id->match('123A45')); + } + + /** + * @return void + */ + public function testItIsUuid(): void + { + $id = new class () { + use MatchesIds; + }; + + $this->assertSame($id, $id->uuid()); + $this->assertSame('[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}', $id->pattern()); + $this->assertTrue($id->match('fca1509e-9178-45fd-8a2b-ae819d34f7e6')); + $this->assertFalse($id->match('fca1509e917845fd8a2bae819d34f7e6')); + } + + /** + * @return void + */ + public function testItIsUlid(): void + { + $id = new class () { + use MatchesIds; + }; + + $this->assertSame($id, $id->ulid()); + $this->assertSame('[0-7][0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{25}', $id->pattern()); + $this->assertTrue($id->match('01HT4PA8AZC8Q30ZGC5PEWZP0E')); + $this->assertFalse($id->match('01HT4PA8AZC8Q30ZGC5PEWZP0')); + } + + /** + * @return void + */ + public function testItIsCustom(): void + { + $id = new class () { + use MatchesIds; + }; + + $this->assertSame($id, $id->matchAs('[A-D]+')); + $this->assertSame('[A-D]+', $id->pattern()); + $this->assertTrue($id->match('abcd')); + $this->assertTrue($id->match('ABCD')); + $this->assertFalse($id->match('abcde')); + + $this->assertSame($id, $id->matchCase()); + $this->assertTrue($id->match('ABCD')); + $this->assertFalse($id->match('abcd')); + } +} \ No newline at end of file