From 97d98d4e7f181ea5e90b868f08eaeb837a6edd9d Mon Sep 17 00:00:00 2001 From: Dayna Blackwell Date: Sat, 2 May 2026 10:33:35 -0700 Subject: [PATCH] fix: allow URIs without double slash in Resource and ResourceTemplate (RFC 3986) The URI regex required "://" after the scheme, rejecting valid URIs like urn:isbn:123, mailto:user@example.com, config:key, and data: URIs. RFC 3986 only requires ":" after the scheme; the "//" is part of the authority component which is optional. Changed both Resource.URI_PATTERN and ResourceTemplate.URI_TEMPLATE_PATTERN to require ":" instead of "://". Adds data-provider tests for URIs without double slash in both classes. Fixes #293 --- src/Schema/Resource.php | 6 +++--- src/Schema/ResourceTemplate.php | 6 +++--- tests/Unit/Schema/ResourceTemplateTest.php | 19 +++++++++++++++++++ tests/Unit/Schema/ResourceTest.php | 21 +++++++++++++++++++++ 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/Schema/Resource.php b/src/Schema/Resource.php index 049e19e7..28b8ae5e 100644 --- a/src/Schema/Resource.php +++ b/src/Schema/Resource.php @@ -40,10 +40,10 @@ class Resource implements \JsonSerializable private const RESOURCE_NAME_PATTERN = '/^[a-zA-Z0-9_-]+$/'; /** - * URI pattern regex - requires a valid scheme, followed by colon and optional path. - * Example patterns: config://, file://path, db://table, etc. + * URI pattern regex - requires a valid scheme followed by colon and optional path (RFC 3986). + * Example patterns: file://path, db://table, urn:isbn:123, config:key, etc. */ - private const URI_PATTERN = '/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]*$/'; + private const URI_PATTERN = '/^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/'; /** * @param string $uri the URI of this resource diff --git a/src/Schema/ResourceTemplate.php b/src/Schema/ResourceTemplate.php index b8c366dc..a19d0c76 100644 --- a/src/Schema/ResourceTemplate.php +++ b/src/Schema/ResourceTemplate.php @@ -37,10 +37,10 @@ class ResourceTemplate implements \JsonSerializable private const RESOURCE_NAME_PATTERN = '/^[a-zA-Z0-9_-]+$/'; /** - * URI Template pattern regex - requires a valid scheme, followed by colon and path with at least one placeholder. - * Example patterns: config://{key}, file://{path}/contents.txt, db://{table}/{id}, etc. + * URI Template pattern regex - requires a valid scheme followed by colon and path with at least one placeholder (RFC 3986). + * Example patterns: file://{path}/contents.txt, db://{table}/{id}, config:{key}, etc. */ - private const URI_TEMPLATE_PATTERN = '/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/.*{[^{}]+}.*/'; + private const URI_TEMPLATE_PATTERN = '/^[a-zA-Z][a-zA-Z0-9+.-]*:.*{[^{}]+}.*/'; /** * @param string $uriTemplate a URI template (according to RFC 6570) that can be used to construct resource URIs diff --git a/tests/Unit/Schema/ResourceTemplateTest.php b/tests/Unit/Schema/ResourceTemplateTest.php index 35391a12..417ee677 100644 --- a/tests/Unit/Schema/ResourceTemplateTest.php +++ b/tests/Unit/Schema/ResourceTemplateTest.php @@ -46,6 +46,25 @@ public function testConstructorInvalid(): void ); } + #[DataProvider('provideValidTemplatesWithoutDoubleSlash')] + public function testConstructorAcceptsTemplatesWithoutDoubleSlash(string $uriTemplate): void + { + $resource = new ResourceTemplate( + uriTemplate: $uriTemplate, + name: 'test-template', + ); + + $this->assertInstanceOf(ResourceTemplate::class, $resource); + $this->assertSame($uriTemplate, $resource->uriTemplate); + } + + public static function provideValidTemplatesWithoutDoubleSlash(): iterable + { + yield 'custom scheme without slashes' => ['config:{key}']; + yield 'custom scheme with slashes' => ['config://{key}']; + yield 'urn-style template' => ['urn:resource:{id}']; + } + public function testFromArrayValid(): void { $resource = ResourceTemplate::fromArray([ diff --git a/tests/Unit/Schema/ResourceTest.php b/tests/Unit/Schema/ResourceTest.php index 316f48fc..064625bb 100644 --- a/tests/Unit/Schema/ResourceTest.php +++ b/tests/Unit/Schema/ResourceTest.php @@ -46,6 +46,27 @@ public function testConstructorInvalid(): void ); } + #[DataProvider('provideValidUrisWithoutDoubleSlash')] + public function testConstructorAcceptsUrisWithoutDoubleSlash(string $uri): void + { + $resource = new Resource( + uri: $uri, + name: 'test-resource', + ); + + $this->assertInstanceOf(Resource::class, $resource); + $this->assertSame($uri, $resource->uri); + } + + public static function provideValidUrisWithoutDoubleSlash(): iterable + { + yield 'urn' => ['urn:isbn:0451450523']; + yield 'mailto' => ['mailto:user@example.com']; + yield 'data' => ['data:text/plain;base64,SGVsbG8=']; + yield 'custom scheme without slashes' => ['config:myapp/settings']; + yield 'custom scheme with slashes' => ['config://myapp/settings']; + } + public function testFromArrayValid(): void { $resource = Resource::fromArray([