diff --git a/src/Rest/Request/ResourceUrlRegex.php b/src/Rest/Request/ResourceUrlRegex.php index d717fdf2..c7968b9d 100644 --- a/src/Rest/Request/ResourceUrlRegex.php +++ b/src/Rest/Request/ResourceUrlRegex.php @@ -29,12 +29,16 @@ namespace Conjoon\Rest\Request; +use Conjoon\Core\Contract\Arrayable; + /** * Provides functionality to map urls to url-templates, for retrieving information about * associated resource targets and path parameters. * * @example * $regex = new ResourceUrlRegex("/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", MessageItem::class); + * $regex->getPathParameterNames(); + * // ["mailFolderId", "messageItemId"]; * * $regex->hasResourceId("http://localhost/MailFolders/INBOX/MessageItems/123"); // true * $regex->getResourceName("http://localhost/MailFolders/INBOX/MessageItems/123"); // "MessageItem" @@ -42,7 +46,7 @@ * $regex->getParameters("http://localhost/MailFolders/INBOX/MessageItems/123"); * // ["mailFolderId" => "INBOX", "messageItemId" => "123"] */ -class ResourceUrlRegex +class ResourceUrlRegex implements Arrayable { /** * @var ?string @@ -52,12 +56,12 @@ class ResourceUrlRegex /** * @var string */ - private string $urlTemplate; + private readonly string $urlTemplate; /** * @var string */ - private string $resourceName; + private readonly string $resourceName; /** @@ -161,7 +165,7 @@ public function getRegexString(): string */ public function hasResourceId(string $url): ?bool { - $matches = $this->getMatches($url); + $matches = $this->getMatch($url); if (!$matches) { return null; } @@ -195,7 +199,7 @@ public function hasResourceId(string $url): ?bool */ public function getPathParameters(string $url): ?array { - $matches = $this->getMatches($url); + $matches = $this->getMatch($url); if (!$matches) { return null; } @@ -221,7 +225,7 @@ public function getPathParameters(string $url): ?array */ public function getResourceName(string $url): ?string { - if (!$this->getMatches($url)) { + if (!$this->getMatch($url)) { return null; } @@ -256,7 +260,7 @@ public function getUrlTemplate(): string * @param string $url * @return array>|null */ - protected function getMatches(string $url): ?array + public function getMatch(string $url): ?array { $regex = $this->getRegexString(); @@ -268,4 +272,16 @@ protected function getMatches(string $url): ?array return $matches; } + + + /** + * @inheritdoc + * @return array + */ + public function toArray(): array + { + return [ + $this->getUrlTemplate(), $this->resourceName + ]; + } } diff --git a/src/Rest/Request/ResourceUrlRegexList.php b/src/Rest/Request/ResourceUrlRegexList.php new file mode 100644 index 00000000..ae05df6f --- /dev/null +++ b/src/Rest/Request/ResourceUrlRegexList.php @@ -0,0 +1,128 @@ + + */ +class ResourceUrlRegexList extends AbstractList +{ + /** + * @var array + */ + private array $matches = []; + + /** + * @inheritdoc + */ + public function getEntityType(): string + { + return ResourceUrlRegex::class; + } + + + /** + * @inheritdoc + */ + public function offsetSet($offset, $value): void + { + $this->matches = []; + parent::offsetSet($offset, $value); + } + + + /** + * @inheritdoc + */ + public function offsetUnset($offset): void + { + $this->matches = []; + parent::offsetUnset($offset); + } + + + /** + * Probes all available entries in this list for a match with the specified + * url, and returns the ResourceUrlRegex that matched the Url. + * If no match was found, null will be returned. Results are cached so that for a + * specific url the list is inspected only once. + * + * @param Url $url + * @return ResourceUrlRegex|null + * + * @see offsetSet + * @see offsetUnset + */ + public function getMatch(Url $url): ?ResourceUrlRegex + { + $urlStr = $url->toString(); + + if (array_key_exists($urlStr, $this->matches)) { + return $this->matches[$urlStr]; + } + + $this->matches[$urlStr] = null; + foreach ($this->data as $resourceUrlRegex) { + $match = $resourceUrlRegex->getMatch($urlStr); + if ($match !== null) { + $this->matches[$urlStr] = $resourceUrlRegex; + break; + } + } + + return $this->matches[$urlStr]; + } + + + /** + * @inheritdoc + * @return array > + */ + public function toArray(): array + { + $res = []; + + foreach ($this->data as $entry) { + /** @var ResourceUrlRegex $entry */ + $res[] = $entry->toArray(); + } + + return $res; + } +} diff --git a/tests/JsonApi/Request/ResourceUrlRegexListTest.php b/tests/Rest/Request/ResourceUrlRegexListTest.php similarity index 52% rename from tests/JsonApi/Request/ResourceUrlRegexListTest.php rename to tests/Rest/Request/ResourceUrlRegexListTest.php index 996e5d4f..1ebca3b0 100644 --- a/tests/JsonApi/Request/ResourceUrlRegexListTest.php +++ b/tests/Rest/Request/ResourceUrlRegexListTest.php @@ -27,11 +27,14 @@ declare(strict_types=1); -namespace Tests\Conjoon\JsonApi\Request; +namespace Tests\Conjoon\Rest\Request; use Conjoon\Core\AbstractList; -use Conjoon\JsonApi\Request\ResourceUrlRegex; -use Conjoon\JsonApi\Request\ResourceUrlRegexList; +use Conjoon\Rest\Request\ResourceUrlRegex; +use Conjoon\Rest\Request\ResourceUrlRegexList; +use Conjoon\Http\Url; +use PHPUnit\Framework\MockObject\MockObject; +use ReflectionException; use Tests\TestCase; /** @@ -41,8 +44,9 @@ class ResourceUrlRegexListTest extends TestCase { /** * Tests constructor + * @return void */ - public function testClass() + public function testClass(): void { $list = $this->createList(); $this->assertInstanceOf(AbstractList::class, $list); @@ -52,14 +56,70 @@ public function testClass() /** - * Tests toArray + * Tests getMatch() + * @return void + * @throws ReflectionException + */ + public function testGetMatch(): void + { + $list = $this->createList(); + $url = new Url("url"); + $urlStr = $url->toString(); + + $matches = $this->makeAccessible($list, "matches", true); + + $getRegexMock = function ($returnValue, $numCalls) use ($urlStr) { + $regex = $this->getMockBuilder(ResourceUrlRegex::class) + ->disableOriginalConstructor() + ->onlyMethods(["getMatch"]) + ->getMock(); + $regex->expects($this->exactly($numCalls)) + ->method("getMatch") + ->with($urlStr)->willReturn($returnValue); + + return $regex; + }; + $regex1 = $getRegexMock(null, 2); + $regex2 = $getRegexMock([], 2); + $regex3 = $getRegexMock(null, 0); + $regex4 = $getRegexMock(null, 0); + + $list[] = $regex1; + $list[] = $regex2; + $list[] = $regex3; + + $this->assertSame($regex2, $list->getMatch($url)); + // force re-call to make sure cache is used + $this->assertSame($regex2, $list->getMatch($url)); + + /** @phpstan-ignore-next-line */ + $this->assertNotEmpty($matches->getValue($list)); + + $list[] = $regex4; + /** @phpstan-ignore-next-line */ + $this->assertEmpty($matches->getValue($list)); + + $this->assertSame( + $regex2, + $list->getMatch($url) + ); + + unset($list[4]); + /** @phpstan-ignore-next-line */ + $this->assertEmpty($matches->getValue($list)); + } + + + /** + * Tests toArray() * @return void */ - public function testToArray() + public function testToArray(): void { $list = new ResourceUrlRegexList(); $this->assertEquals([], $list->toArray()); + /** @var ResourceUrlRegex&MockObject $resourceUrlRegex */ $resourceUrlRegex = $this->createMockForAbstract( ResourceUrlRegex::class, ["toArray"], diff --git a/tests/Rest/Request/ResourceUrlRegexTest.php b/tests/Rest/Request/ResourceUrlRegexTest.php index daefad0f..8542eda1 100644 --- a/tests/Rest/Request/ResourceUrlRegexTest.php +++ b/tests/Rest/Request/ResourceUrlRegexTest.php @@ -29,7 +29,9 @@ namespace Tests\Conjoon\Rest\Request; +use Conjoon\Core\Contract\Arrayable; use Conjoon\Rest\Request\ResourceUrlRegex; +use PHPUnit\Framework\MockObject\MockObject; use ReflectionException; use ReflectionMethod; use Tests\TestCase; @@ -45,7 +47,7 @@ class ResourceUrlRegexTest extends TestCase public function testClass(): void { $resourceUrlRegex = new ResourceUrlRegex("tpl", "Resource"); - + $this->assertInstanceOf(Arrayable::class, $resourceUrlRegex); $this->assertSame("tpl", $resourceUrlRegex->getUrlTemplate()); } @@ -62,7 +64,8 @@ public function testPrepareRegex(): void ], [ "input" => "/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", - "output" => "/\/MailAccounts\/(?[^\?\/]+)\/MailFolders\/(?[^\?\/]+)\/MessageItems(\??[^\/]*$|\/(?[^\?\/]+))\??[^\/]*$/m" + "output" => "/\/MailAccounts\/(?[^\?\/]+)\/MailFolders\/(?[^\?\/]+)" . + "\/MessageItems(\??[^\/]*$|\/(?[^\?\/]+))\??[^\/]*$/m" ], [ "input" => "/MailAccounts/{mailAccountId}", @@ -85,6 +88,68 @@ public function testPrepareRegex(): void } + /** + * Tests getMatch() + * @return void + */ + public function testGetMatch(): void + { + $tests = [ + [ + "input" => ["/MailAccounts/MailFolders", "MailFolders"], + "args" => ["MailFolders/3"], + "output" => null + ], + [ + "input" => ["/MailAccounts/MailFolders/{mailFolderId}", "MailFolders"], + "args" => ["https://local.dev/MailAccounts/MailFolders/3"], + "output" => [] + ], + [ + "input" => ["/MailAccounts/MailFolders/{mailFolderId}", "MailFolders"], + "args" => ["https://local.dev/MailAccounts/MailFolders/"], + "output" => null + ], + [ + "input" => [ + "/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", + "MessageItem" + ], + "args" => ["https://localhost/MailAccounts/dev/MailFolders/Inbox.Sent%20Drafts/MessageItems/53432"], + "output" => [] + ], + [ + "input" => [ + "/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", + "MessageItem" + ], + "args" => ["https://localhost/MailAccounts/dev/MailFolders/Inbox.Sent%20Drafts/MessageItems"], + "output" => [] + ], + [ + "input" => [ + "/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", + "MessageItem" + ], + "args" => ["https://localhost/MailAccounts/dev/MailFolders/Inbox.Sent%20Drafts"], + "output" => null + ] + ]; + + + foreach ($tests as $test) { + ["input" => $input, "output" => $output, "args" => $args] = $test; + $resourceUrlRegex = new ResourceUrlRegex(...$input); + + $match = $resourceUrlRegex->getMatch(...$args); + if ($output === null) { + $this->assertNull($match); + } else { + $this->assertIsArray($match); + } + } + } + /** * Tests getRegexString() * @return void @@ -136,17 +201,26 @@ public function testHasResourceId(): void "output" => null ], [ - "input" => ["/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", "MessageItem"], + "input" => [ + "/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", + "MessageItem" + ], "args" => ["https://localhost/MailAccounts/dev/MailFolders/Inbox.Sent%20Drafts/MessageItems/53432"], "output" => true ], [ - "input" => ["/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", "MessageItem"], + "input" => [ + "/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", + "MessageItem" + ], "args" => ["https://localhost/MailAccounts/dev/MailFolders/Inbox.Sent%20Drafts/MessageItems"], "output" => false ], [ - "input" => ["/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", "MessageItem"], + "input" => [ + "/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", + "MessageItem" + ], "args" => ["https://localhost/MailAccounts/dev/MailFolders/Inbox.Sent%20Drafts"], "output" => null ] @@ -200,7 +274,8 @@ public function testGetPathParameterNames(): void ], [ - "input" => "/Mail/{mail}/Accounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", + "input" => "/Mail/{mail}/Accounts/{mailAccountId}/MailFolders/" . + "{mailFolderId}/MessageItems/{messageItemId}", "output" => ["mail", "mailAccountId", "mailFolderId", "messageItemId"] ] ]; @@ -209,6 +284,7 @@ public function testGetPathParameterNames(): void foreach ($tests as $test) { ["input" => $input, "output" => $output] = $test; $resourceUrlRegex = new ResourceUrlRegex($input, "Resource"); + $this->assertSame($output, $resourceUrlRegex->getPathParameterNames()); // always produces same result $this->assertSame($output, $resourceUrlRegex->getPathParameterNames()); @@ -237,7 +313,9 @@ public function testGetPathParameters(): void [ "input" => "/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", "args" => ["/MailAccounts/dev/MailFolders/Inbox.Drafts%20Sent/MessageItems/randomId"], - "output" => ["mailAccountId" => "dev", "mailFolderId" => "Inbox.Drafts%20Sent", "messageItemId" => "randomId"] + "output" => [ + "mailAccountId" => "dev", "mailFolderId" => "Inbox.Drafts%20Sent", "messageItemId" => "randomId" + ] ], [ @@ -250,7 +328,11 @@ public function testGetPathParameters(): void foreach ($tests as $test) { ["input" => $input, "args" => $args, "output" => $output] = $test; - $resourceUrlRegex = new ResourceUrlRegex($input, "Resource"); + + $resourceUrlRegex = $this->getMockProxy([$input, "Resource"]); + + $resourceUrlRegex->expects($this->exactly(2))->method("getMatch"); + $this->assertSame($output, $resourceUrlRegex->getPathParameters(...$args)); // always produces same result $this->assertSame($output, $resourceUrlRegex->getPathParameters(...$args)); @@ -277,23 +359,61 @@ public function testGetResourceName(): void "output" => "Resource" ], [ - "input" => ["/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", "Resource"], + "input" => [ + "/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", + "Resource" + ], "args" => ["/Mail/4/Accounts/dev/MailFolders/Inbox.Drafts%20Sent/MessageItems/randomId"], "output" => null ], [ - "input" => ["/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", "MessageItem"], + "input" => [ + "/MailAccounts/{mailAccountId}/MailFolders/{mailFolderId}/MessageItems/{messageItemId}", + "MessageItem" + ], "args" => ["/MailAccounts/dev/MailFolders/Inbox.Drafts%20Sent/MessageItems"], "output" => "MessageItem" ] ]; - foreach ($tests as $test) { ["input" => $input, "args" => $args, "output" => $output] = $test; - $resourceUrlRegex = new ResourceUrlRegex(...$input); + + $resourceUrlRegex = $this->getMockProxy($input); + $resourceUrlRegex->expects($this->exactly(1))->method("getMatch")->with($args[0]); + $this->assertSame($output, $resourceUrlRegex->getResourceName(...$args)); } } + + + /** + * @return void + */ + public function testToArray() + { + $resourceUrlRegex = new ResourceUrlRegex("/MailAccounts/MailFolders", "Resource"); + + $this->assertSame( + [ + "/MailAccounts/MailFolders", "Resource" + ], + $resourceUrlRegex->toArray() + ); + } + + + /** + * @param array $args + * @return MockObject&ResourceUrlRegex + */ + protected function getMockProxy(array $args): MockObject&ResourceUrlRegex + { + return $this->getMockBuilder(ResourceUrlRegex::class) + ->onlyMethods(["getMatch"]) + ->enableProxyingToOriginalMethods() + ->setConstructorArgs($args) + ->getMock(); + } }