Upstream thephpleague/uri-src state of the touched files before 6b1d90a020
This commit is contained in:
1 parent
c0b6c4bc5f
commit
cb44762108
3 files changed
+631
No files matched your search
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* League.Uri (https://uri.thephpleague.com)
|
||||
*
|
||||
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\Uri\UriTemplate;
|
||||
|
||||
use BackedEnum;
|
||||
use Deprecated;
|
||||
use League\Uri\Exceptions\SyntaxError;
|
||||
use Stringable;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_reduce;
|
||||
use function array_unique;
|
||||
use function preg_match_all;
|
||||
use function preg_replace;
|
||||
use function str_replace;
|
||||
use function strpbrk;
|
||||
|
||||
use const PREG_SET_ORDER;
|
||||
|
||||
/**
|
||||
* @internal The class exposes the internal representation of a Template and its usage
|
||||
*/
|
||||
final class Template implements Stringable
|
||||
{
|
||||
/**
|
||||
* Expression regular expression pattern.
|
||||
*/
|
||||
private const REGEXP_EXPRESSION_DETECTOR = '/(?<expression>\{[^}]*})/x';
|
||||
|
||||
/** @var array<Expression> */
|
||||
private readonly array $expressions;
|
||||
/** @var array<string> */
|
||||
public readonly array $variableNames;
|
||||
|
||||
private function __construct(public readonly string $value, Expression ...$expressions)
|
||||
{
|
||||
$this->expressions = $expressions;
|
||||
$this->variableNames = array_unique(
|
||||
array_merge(
|
||||
...array_map(
|
||||
static fn (Expression $expression): array => $expression->variableNames,
|
||||
$expressions
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SyntaxError if the template contains invalid expressions
|
||||
* @throws SyntaxError if the template contains invalid variable specification
|
||||
*/
|
||||
public static function new(BackedEnum|Stringable|string $template): self
|
||||
{
|
||||
if ($template instanceof BackedEnum) {
|
||||
$template = $template->value;
|
||||
}
|
||||
|
||||
$template = (string) $template;
|
||||
/** @var string $remainder */
|
||||
$remainder = preg_replace(self::REGEXP_EXPRESSION_DETECTOR, '', $template);
|
||||
false === strpbrk($remainder, '{}') || throw new SyntaxError('The template "'.$template.'" contains invalid expressions.');
|
||||
|
||||
preg_match_all(self::REGEXP_EXPRESSION_DETECTOR, $template, $founds, PREG_SET_ORDER);
|
||||
|
||||
return new self($template, ...array_values(
|
||||
array_reduce($founds, function (array $carry, array $found): array {
|
||||
if (!isset($carry[$found['expression']])) {
|
||||
$carry[$found['expression']] = Expression::new($found['expression']);
|
||||
}
|
||||
|
||||
return $carry;
|
||||
}, [])
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TemplateCanNotBeExpanded if the variables are invalid
|
||||
*/
|
||||
public function expand(iterable $variables = []): string
|
||||
{
|
||||
if (!$variables instanceof VariableBag) {
|
||||
$variables = new VariableBag($variables);
|
||||
}
|
||||
|
||||
return $this->expandAll($variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TemplateCanNotBeExpanded if the variables are invalid or missing
|
||||
*/
|
||||
public function expandOrFail(iterable $variables = []): string
|
||||
{
|
||||
if (!$variables instanceof VariableBag) {
|
||||
$variables = new VariableBag($variables);
|
||||
}
|
||||
|
||||
$missing = array_filter($this->variableNames, fn (string $name): bool => !isset($variables[$name]));
|
||||
if ([] !== $missing) {
|
||||
throw TemplateCanNotBeExpanded::dueToMissingVariables(...$missing);
|
||||
}
|
||||
|
||||
return $this->expandAll($variables);
|
||||
}
|
||||
|
||||
private function expandAll(VariableBag $variables): string
|
||||
{
|
||||
return array_reduce(
|
||||
$this->expressions,
|
||||
fn (string $uri, Expression $expr): string => str_replace($expr->value, $expr->expand($variables), $uri),
|
||||
$this->value
|
||||
);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATION WARNING! This method will be removed in the next major point release.
|
||||
*
|
||||
* @throws SyntaxError if the template contains invalid expressions
|
||||
* @throws SyntaxError if the template contains invalid variable specification
|
||||
* @deprecated Since version 7.0.0
|
||||
* @codeCoverageIgnore
|
||||
* @see Template::new()
|
||||
*
|
||||
* Create a new instance from a string.
|
||||
*
|
||||
*/
|
||||
#[Deprecated(message:'use League\Uri\UriTemplate\Template::new() instead', since:'league/uri:7.0.0')]
|
||||
public static function createFromString(Stringable|string $template): self
|
||||
{
|
||||
return self::new($template);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* League.Uri (https://uri.thephpleague.com)
|
||||
*
|
||||
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\Uri\UriTemplate;
|
||||
|
||||
use JsonException;
|
||||
use League\Uri\Exceptions\SyntaxError;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
#[CoversClass(Template::class)]
|
||||
final class TemplateTest extends TestCase
|
||||
{
|
||||
private static string $rootPath = __DIR__.'/../../vendor/uri-templates/uritemplate-test';
|
||||
|
||||
/** @var array<string> */
|
||||
private static array $testFilenames = [
|
||||
'spec-examples.json',
|
||||
'negative-tests.json',
|
||||
'extended-tests.json',
|
||||
];
|
||||
|
||||
#[DataProvider('uriTemplateSpecificationDataProvider')]
|
||||
#[Test]
|
||||
public function testItCompliesWithUriTemplatesExpansionTests(
|
||||
array $variables,
|
||||
string $input,
|
||||
string|array|false $expected
|
||||
): void {
|
||||
if (false === $expected) {
|
||||
$this->expectException(Throwable::class);
|
||||
}
|
||||
|
||||
$result = Template::new($input)->expand(new VariableBag($variables));
|
||||
|
||||
if (is_array($expected)) {
|
||||
self::assertContains($result, $expected);
|
||||
} else {
|
||||
self::assertSame($expected, $result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonException
|
||||
* @throws RuntimeException
|
||||
* @return iterable<string, array{
|
||||
* variables:array{string, string|int},
|
||||
* input:string,
|
||||
* expected:string|array<string>|false
|
||||
* }>
|
||||
*/
|
||||
public static function uriTemplateSpecificationDataProvider(): iterable
|
||||
{
|
||||
foreach (static::$testFilenames as $path) {
|
||||
$path = static::$rootPath.'/'.ltrim($path, '/');
|
||||
if (false === $content = file_get_contents($path)) {
|
||||
throw new RuntimeException("unable to connect to the path `$path`.");
|
||||
}
|
||||
|
||||
/** @var array $records */
|
||||
$records = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
|
||||
foreach ($records as $title => $testSuite) {
|
||||
$level = $testSuite['level'] ?? null;
|
||||
$variables = $testSuite['variables'];
|
||||
foreach ($testSuite['testcases'] as $offset => [$input, $expected]) {
|
||||
yield $title.' - '.$level.' # '.($offset + 1) => [
|
||||
'variables' => $variables,
|
||||
'input' => $input,
|
||||
'expected' => $expected,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[DataProvider('providesValidNotation')]
|
||||
public function testItCanBeInstantiatedWithAValidNotation(string $notation): void
|
||||
{
|
||||
self::assertSame($notation, Template::new($notation)->value);
|
||||
self::assertSame($notation, (string) Template::new($notation));
|
||||
}
|
||||
|
||||
public static function providesValidNotation(): iterable
|
||||
{
|
||||
return [
|
||||
'complex template' => ['http://example.com{+path}{/segments}{?query,more*,foo[]*}'],
|
||||
'template without expression' => ['foobar'],
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('providesInvalidNotation')]
|
||||
public function testItFailsToInstantiatedWithAnInvalidNotation(string $notation): void
|
||||
{
|
||||
self::expectException(SyntaxError::class);
|
||||
|
||||
Template::new($notation);
|
||||
}
|
||||
|
||||
public static function providesInvalidNotation(): iterable
|
||||
{
|
||||
return [
|
||||
['fooba{r'],
|
||||
['fooba}r'],
|
||||
['fooba}{r'],
|
||||
['{foo{bar'],
|
||||
['{foo}}bar'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('expectedVariableNames')]
|
||||
public function testGetVariableNames(string $template, array $expected): void
|
||||
{
|
||||
self::assertSame($expected, Template::new($template)->variableNames);
|
||||
}
|
||||
|
||||
public static function expectedVariableNames(): iterable
|
||||
{
|
||||
return [
|
||||
[
|
||||
'template' => '',
|
||||
'expected' => [],
|
||||
],
|
||||
[
|
||||
'template' => '{foo}{bar}{420}',
|
||||
'expected' => ['foo', 'bar', '420'],
|
||||
],
|
||||
[
|
||||
'template' => '{foo}{foo:2}{+foo}',
|
||||
'expected' => ['foo'],
|
||||
],
|
||||
[
|
||||
'template' => '{bar}{foo}',
|
||||
'expected' => ['bar', 'foo'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('providesExpansion')]
|
||||
public function testItCanExpandVariables(string $notation, array $variables, string $expected): void
|
||||
{
|
||||
self::assertSame($expected, Template::new($notation)->expand($variables));
|
||||
}
|
||||
|
||||
public static function providesExpansion(): iterable
|
||||
{
|
||||
return [
|
||||
'with variables' => [
|
||||
'notation' => 'foobar{var}',
|
||||
'variables' => ['var' => 'yolo'],
|
||||
'expected' => 'foobaryolo',
|
||||
],
|
||||
'with no variables' => [
|
||||
'notation' => 'foobar',
|
||||
'variables' => [],
|
||||
'expected' => 'foobar',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testExpandOrFailIfAtLeastOneVariableIsMissing(): void
|
||||
{
|
||||
$this->expectException(TemplateCanNotBeExpanded::class);
|
||||
|
||||
Template::new('{var}{baz}')->expandOrFail(['var' => 'bar']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_can_expand_with_expand_or_fail_when_all_variables_are_present(): void
|
||||
{
|
||||
self::assertSame('barfoo', Template::new('{var}{baz}')->expandOrFail(['var' => 'bar', 'baz' => 'foo']));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user