From eb0f65c5ef8819e109df213e773faf8511126f5d Mon Sep 17 00:00:00 2001 From: flashwave Date: Fri, 4 Oct 2024 20:43:28 +0000 Subject: [PATCH] Some updates to the URL registry (feat. tests). --- TODO.md | 2 - VERSION | 2 +- src/Urls/ArrayUrlRegistry.php | 25 ++-- src/Urls/ScopedUrlRegistry.php | 18 ++- src/Urls/UrlRegistry.php | 8 +- src/Urls/UrlSourceTrait.php | 16 +++ tests/UrlRegistryTest.php | 227 +++++++++++++++++++++++++++++++++ 7 files changed, 279 insertions(+), 19 deletions(-) create mode 100644 src/Urls/UrlSourceTrait.php create mode 100644 tests/UrlRegistryTest.php diff --git a/TODO.md b/TODO.md index 8f1320a..0eb0661 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,3 @@ # TODO - Create similarly addressable GD2/Imagick wrappers. - - - Create a URL formatter. diff --git a/VERSION b/VERSION index 58214c6..7a25b4b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2410.32326 +0.2410.42042 diff --git a/src/Urls/ArrayUrlRegistry.php b/src/Urls/ArrayUrlRegistry.php index 31ea612..6022a95 100644 --- a/src/Urls/ArrayUrlRegistry.php +++ b/src/Urls/ArrayUrlRegistry.php @@ -1,7 +1,7 @@ , fragment: string}> */ private array $urls = []; - public function register(string $name, string $path, array $query = [], string $fragment = ''): void { - if(array_key_exists($name, $this->urls)) - throw new InvalidArgumentException('A URL format with $name has already been registered.'); - if($path === '') - throw new InvalidArgumentException('$path may not be empty.'); - if($path[0] !== '/') - throw new InvalidArgumentException('$path must begin with /.'); + public function register(UrlSource|string $nameOrSource, string $path = '', array $query = [], string $fragment = ''): void { + if($nameOrSource instanceof UrlSource) { + $nameOrSource->registerUrls($this); + return; + } - $this->urls[$name] = [ + if($nameOrSource === '') + throw new InvalidArgumentException('$nameOrSource may not be empty'); + if(array_key_exists($nameOrSource, $this->urls)) + throw new InvalidArgumentException('A URL format with $nameOrSource has already been registered'); + if($path === '') + throw new InvalidArgumentException('$path may not be empty'); + if(!str_starts_with($path, '/')) + throw new InvalidArgumentException('$path must begin with /'); + + $this->urls[$nameOrSource] = [ 'path' => $path, 'query' => $query, 'fragment' => $fragment, diff --git a/src/Urls/ScopedUrlRegistry.php b/src/Urls/ScopedUrlRegistry.php index 5a903fd..48fa088 100644 --- a/src/Urls/ScopedUrlRegistry.php +++ b/src/Urls/ScopedUrlRegistry.php @@ -1,7 +1,7 @@ registerUrls($this); + return; + } + + if($nameOrSource === '') + throw new InvalidArgumentException('$nameOrSource may not be empty'); + if($path === '') + throw new InvalidArgumentException('$path may not be empty'); + if(!str_starts_with($path, '/')) + throw new InvalidArgumentException('$path must begin with /'); + $this->registry->register( - $this->namePrefix . $name, + $this->namePrefix . $nameOrSource, $this->pathPrefix . $path, $query, $fragment diff --git a/src/Urls/UrlRegistry.php b/src/Urls/UrlRegistry.php index 7fdd2f8..bfa3d69 100644 --- a/src/Urls/UrlRegistry.php +++ b/src/Urls/UrlRegistry.php @@ -1,7 +1,7 @@ $query Query string variables of the URL. * @param string $fragment Fragment portion of the URL. * @throws InvalidArgumentException If $name has already been registered. * @throws InvalidArgumentException If $path if empty or does not start with a /. */ - function register(string $name, string $path, array $query = [], string $fragment = ''): void; + function register(UrlSource|string $nameOrSource, string $path = '', array $query = [], string $fragment = ''): void; /** * Format a URL. diff --git a/src/Urls/UrlSourceTrait.php b/src/Urls/UrlSourceTrait.php new file mode 100644 index 0000000..0e18cc8 --- /dev/null +++ b/src/Urls/UrlSourceTrait.php @@ -0,0 +1,16 @@ +register('soap', '/soap//soap-', ['soap' => ''], ''); + $this->assertEquals('/soap/beans/soap-beans?soap=beans#beans', $arrReg->format('soap', ['soap' => 'beans'])); + + $this->assertEquals('', $arrReg->format('does-not-exist', ['test' => 'test'])); + + $nameScopeOnly = $arrReg->scopeTo(namePrefix: 'pfx-'); + $nameScopeOnly->register('cat', '/meow', ['id' => 'c']); + $this->assertEquals('/meow?id=c50', $nameScopeOnly->format('cat', ['id' => '50'])); + $this->assertEquals('/meow?id=c', $arrReg->format('pfx-cat')); // this is a little undesirable but just keep your query args clean :) + $this->assertEquals('', $arrReg->format('cat', ['id' => '84'])); + + $pathScopeOnly = $arrReg->scopeTo(pathPrefix: '/prefix'); + $pathScopeOnly->register('dog', '/woof', ['type' => '']); + $this->assertEquals('/prefix/woof', $pathScopeOnly->format('dog')); + $this->assertEquals('/prefix/woof?type=borf', $arrReg->format('dog', ['woof' => 'borf'])); + + // we cannot access dog cus pfx- gets prepended + $this->assertEquals('', $nameScopeOnly->format('dog')); + + // we CAN access cat because path prefixing only applies to registration + // not sure when you'd want this behaviour but its entirely transparent to implement soooooooo why not lmao + $this->assertEquals('/meow?id=cactuallydog', $pathScopeOnly->format('pfx-cat', ['id' => 'actuallydog'])); + + // this is debatably the only scope impl that makes sense, but again zero overhead + $scopedReg = $arrReg->scopeTo('pfx2-', '/also'); + $scopedReg->register('fragmented', '/this/one/has/a/fragment', fragment: 'p'); + $this->assertEquals('/also/this/one/has/a/fragment#p9001', $scopedReg->format('fragmented', ['post' => 9001])); + $this->assertEquals('/also/this/one/has/a/fragment#p20001', $arrReg->format('pfx2-fragmented', ['post' => 20001])); + + $sourceClass = new class implements UrlSource { + use UrlSourceTrait; + + #[UrlFormat('method-a', '/method-a')] + public function a(): void {} + + #[UrlFormat('method-b', '/method-b', ['beans' => '', 'test' => ''])] + public function b(): void {} + + #[UrlFormat('method-c', '/method-c', fragment: '')] + public function c(): void {} + + #[UrlFormat('method-d', '/method-d', fragment: '')] + public function d(): void {} + + #[UrlFormat('method-e', '/method-e/', ['count' => ''])] + public function e(): void {} + }; + + // can't use path scope only because of the names + $arrReg->register($sourceClass); + $nameScopeOnly->register($sourceClass); + $scopedReg->register($sourceClass); + + $this->assertEquals('/method-a', $arrReg->format('method-a')); + $this->assertEquals('/method-b', $arrReg->format('method-b')); + $this->assertEquals('/method-b?beans=720', $arrReg->format('method-b', ['count' => 720])); + $this->assertEquals('/method-b?beans=720&test=wowzers', $arrReg->format('method-b', ['count' => 720, 'mode' => 'wowzers'])); + $this->assertEquals('/method-c', $arrReg->format('method-c')); + $this->assertEquals('/method-c#720', $arrReg->format('method-c', ['count' => 720])); + $this->assertEquals('/method-d', $arrReg->format('method-d')); + $this->assertEquals('/method-d#98342389', $arrReg->format('method-d', ['count' => 98342389])); + $this->assertEquals('/method-e/', $arrReg->format('method-e')); + $this->assertEquals('/method-e/wowzers', $arrReg->format('method-e', ['mode' => 'wowzers'])); + $this->assertEquals('/method-e/wowzers?count=720', $arrReg->format('method-e', ['count' => 720, 'mode' => 'wowzers'])); + + $this->assertEquals('/method-a', $arrReg->format('pfx-method-a')); + $this->assertEquals('/method-b', $arrReg->format('pfx-method-b')); + $this->assertEquals('/method-b?beans=720', $arrReg->format('pfx-method-b', ['count' => 720])); + $this->assertEquals('/method-b?beans=720&test=wowzers', $arrReg->format('pfx-method-b', ['count' => 720, 'mode' => 'wowzers'])); + $this->assertEquals('/method-c', $arrReg->format('pfx-method-c')); + $this->assertEquals('/method-c#720', $arrReg->format('pfx-method-c', ['count' => 720])); + $this->assertEquals('/method-d', $arrReg->format('pfx-method-d')); + $this->assertEquals('/method-d#98342389', $arrReg->format('pfx-method-d', ['count' => 98342389])); + $this->assertEquals('/method-e/', $arrReg->format('pfx-method-e')); + $this->assertEquals('/method-e/wowzers', $arrReg->format('pfx-method-e', ['mode' => 'wowzers'])); + $this->assertEquals('/method-e/wowzers?count=720', $arrReg->format('pfx-method-e', ['count' => 720, 'mode' => 'wowzers'])); + + $this->assertEquals('/also/method-a', $arrReg->format('pfx2-method-a')); + $this->assertEquals('/also/method-b', $arrReg->format('pfx2-method-b')); + $this->assertEquals('/also/method-b?beans=720', $arrReg->format('pfx2-method-b', ['count' => 720])); + $this->assertEquals('/also/method-b?beans=720&test=wowzers', $arrReg->format('pfx2-method-b', ['count' => 720, 'mode' => 'wowzers'])); + $this->assertEquals('/also/method-c', $arrReg->format('pfx2-method-c')); + $this->assertEquals('/also/method-c#720', $arrReg->format('pfx2-method-c', ['count' => 720])); + $this->assertEquals('/also/method-d', $arrReg->format('pfx2-method-d')); + $this->assertEquals('/also/method-d#98342389', $arrReg->format('pfx2-method-d', ['count' => 98342389])); + $this->assertEquals('/also/method-e/', $arrReg->format('pfx2-method-e')); + $this->assertEquals('/also/method-e/wowzers', $arrReg->format('pfx2-method-e', ['mode' => 'wowzers'])); + $this->assertEquals('/also/method-e/wowzers?count=720', $arrReg->format('pfx2-method-e', ['count' => 720, 'mode' => 'wowzers'])); + + $this->assertEquals('/method-a', $nameScopeOnly->format('method-a')); + $this->assertEquals('/method-b', $nameScopeOnly->format('method-b')); + $this->assertEquals('/method-b?beans=720', $nameScopeOnly->format('method-b', ['count' => 720])); + $this->assertEquals('/method-b?beans=720&test=wowzers', $nameScopeOnly->format('method-b', ['count' => 720, 'mode' => 'wowzers'])); + $this->assertEquals('/method-c', $nameScopeOnly->format('method-c')); + $this->assertEquals('/method-c#720', $nameScopeOnly->format('method-c', ['count' => 720])); + $this->assertEquals('/method-d', $nameScopeOnly->format('method-d')); + $this->assertEquals('/method-d#98342389', $nameScopeOnly->format('method-d', ['count' => 98342389])); + $this->assertEquals('/method-e/', $nameScopeOnly->format('method-e')); + $this->assertEquals('/method-e/wowzers', $nameScopeOnly->format('method-e', ['mode' => 'wowzers'])); + $this->assertEquals('/method-e/wowzers?count=720', $nameScopeOnly->format('method-e', ['count' => 720, 'mode' => 'wowzers'])); + + $this->assertEquals('/also/method-a', $scopedReg->format('method-a')); + $this->assertEquals('/also/method-b', $scopedReg->format('method-b')); + $this->assertEquals('/also/method-b?beans=720', $scopedReg->format('method-b', ['count' => 720])); + $this->assertEquals('/also/method-b?beans=720&test=wowzers', $scopedReg->format('method-b', ['count' => 720, 'mode' => 'wowzers'])); + $this->assertEquals('/also/method-c', $scopedReg->format('method-c')); + $this->assertEquals('/also/method-c#720', $scopedReg->format('method-c', ['count' => 720])); + $this->assertEquals('/also/method-d', $scopedReg->format('method-d')); + $this->assertEquals('/also/method-d#98342389', $scopedReg->format('method-d', ['count' => 98342389])); + $this->assertEquals('/also/method-e/', $scopedReg->format('method-e')); + $this->assertEquals('/also/method-e/wowzers', $scopedReg->format('method-e', ['mode' => 'wowzers'])); + $this->assertEquals('/also/method-e/wowzers?count=720', $scopedReg->format('method-e', ['count' => 720, 'mode' => 'wowzers'])); + + // now that there's more going on within the registry lets try this again! + $this->assertEquals('', $arrReg->format('does-not-exist', ['test' => 'test'])); + } + + public function testUrlVariableSerialisation(): void { + $registry = new ArrayUrlRegistry; + $registry->register('test', '/', ['p' => '']); + + $this->assertEquals('/', $registry->format('test')); + $this->assertEquals('/string', $registry->format('test', ['value' => 'string'])); + $this->assertEquals('/1234567890', $registry->format('test', ['value' => 1234567890])); + $this->assertEquals('/1234.5678', $registry->format('test', ['value' => 1234.5678])); + $this->assertEquals('/a,b,c', $registry->format('test', ['value' => ['a', 'b', 'c']])); + $this->assertEquals('/12,b,4.5', $registry->format('test', ['value' => [12, 'b', 4.5]])); + $this->assertEquals('/1', $registry->format('test', ['value' => true])); + $this->assertEquals('/', $registry->format('test', ['value' => false])); + $this->assertEquals('/', $registry->format('test', ['value' => null])); + $this->assertEquals('/', $registry->format('test', ['value' => []])); + + // "page" is a special case where 1 is considered default + // idk if it makes sense for this to be hardcoded, probably not but it makes sense 99.9% of the times so eat pant. + $this->assertEquals('/', $registry->format('test', ['value' => 0, 'page' => 0])); + $this->assertEquals('/1', $registry->format('test', ['value' => 1, 'page' => 1])); + $this->assertEquals('/2?p=2', $registry->format('test', ['value' => 2, 'page' => 2])); + $this->assertEquals('/?p=2', $registry->format('test', ['value' => 0, 'page' => 2])); + + $stringable = new class implements Stringable { + public function __toString(): string { + return 'meow'; + } + }; + + $this->assertEquals('/meow', $registry->format('test', ['value' => $stringable])); + } + + public function testUrlRegistrationBlankNameException(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('$nameOrSource may not be empty'); + + (new ArrayUrlRegistry)->register(''); + } + + public function testUrlRegistrationDupeNameException(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('A URL format with $nameOrSource has already been registered'); + + $registry = new ArrayUrlRegistry; + $registry->register('test', '/path1'); + $registry->register('test', '/path2'); + } + + public function testUrlRegistrationBlankPathException(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('$path may not be empty'); + + (new ArrayUrlRegistry)->register('test', ''); + } + + public function testUrlRegistrationNonRootPathException(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('$path must begin with /'); + + (new ArrayUrlRegistry)->register('test', 'soap'); + } + + public function testUrlRegistrationScopedNonRootPrefixException(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('$pathPrefix must start with a / if it is non-empty'); + + (new ArrayUrlRegistry)->scopeTo('beans', 'beans'); + } + + public function testUrlRegistrationScopedBlankNameException(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('$nameOrSource may not be empty'); + + (new ArrayUrlRegistry)->scopeTo('beans', '/beans')->register(''); + } + + public function testUrlRegistrationScopedBlankPathException(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('$path may not be empty'); + + (new ArrayUrlRegistry)->scopeTo('beans', '/beans')->register('test', ''); + } + + public function testUrlRegistrationScopedNonRootPathException(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('$path must begin with /'); + + (new ArrayUrlRegistry)->scopeTo('beans', '/beans')->register('test', 'soap'); + } +}