[Schema] fix: allow URIs without double slash in Resource and ResourceTemplate (RFC 3986) (#297)

* 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

* Reword unit tests

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
This commit is contained in:
Dayna Blackwell 2026-05-08 01:14:08 -07:00 • committed by GitHub
commit 797472777f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 46 additions and 6 deletions

View file

@ -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

View file

@ -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

View file

@ -46,6 +46,25 @@ class ResourceTemplateTest extends TestCase
);
}
#[DataProvider('provideValidTemplates')]
public function testConstructorAcceptsTemplates(string $uriTemplate): void
{
$resource = new ResourceTemplate(
uriTemplate: $uriTemplate,
name: 'test-template',
);
$this->assertInstanceOf(ResourceTemplate::class, $resource);
$this->assertSame($uriTemplate, $resource->uriTemplate);
}
public static function provideValidTemplates(): 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([

View file

@ -46,6 +46,27 @@ class ResourceTest extends TestCase
);
}
#[DataProvider('provideValidUris')]
public function testConstructorAcceptsUris(string $uri): void
{
$resource = new Resource(
uri: $uri,
name: 'test-resource',
);
$this->assertInstanceOf(Resource::class, $resource);
$this->assertSame($uri, $resource->uri);
}
public static function provideValidUris(): 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([