diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85ef7d9..1ef3b2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macOS-latest] - php-version: ['7.4', '8.0'] + php-version: ['8.1'] name: 'PHPUnit' steps: - name: Checkout @@ -19,17 +19,8 @@ jobs: php-version: ${{ matrix.php-version }} extensions: mbstring, intl coverage: xdebug - - name: Get Composer Cache Directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Install Dependencies - run: composer install + - name: Composer + uses: "ramsey/composer-install@v2" - name: PHPUnit run: vendor/bin/phpunit --coverage-clover=coverage.clover - uses: codecov/codecov-action@v1 @@ -39,7 +30,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['7.4', '8.0'] + php-version: ['8.1'] name: 'Psalm' steps: - name: Checkout @@ -49,24 +40,15 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: mbstring, intl - - name: Get Composer Cache Directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Install Dependencies - run: composer install + - name: Composer + uses: "ramsey/composer-install@v2" - name: Psalm run: vendor/bin/psalm --shepherd cs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['7.4'] + php-version: ['8.1'] name: 'CS' steps: - name: Checkout @@ -76,16 +58,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: mbstring, intl - - name: Get Composer Cache Directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-${{ matrix.php-version }}-composer- - - name: Install Dependencies - run: composer install --no-progress + - name: Composer + uses: "ramsey/composer-install@v2" - name: CS - run: vendor/bin/php-cs-fixer fix --diff --dry-run --diff-format udiff + run: vendor/bin/php-cs-fixer fix --diff --dry-run diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 100% rename from .php_cs.dist rename to .php-cs-fixer.dist.php diff --git a/README.md b/README.md index bf10adc..478da25 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,7 @@ use Innmind\Immutable\Map; use Innmind\Url\Url; $url = Template::of('http://example.com/dictionary/{term:1}/{term}')->expand( - Map::of('string', 'scalar|array') - ('term', 'dog') + Map::of(['term', 'dog']), ); $url instanceof Url; // true $url->toString(); // http://example.com/dictionary/d/dog diff --git a/composer.json b/composer.json index 31208c5..1ec2510 100644 --- a/composer.json +++ b/composer.json @@ -8,16 +8,16 @@ "authors": [ { "name": "Baptiste Langlade", - "email": "langlade.baptiste@gmail.com" + "email": "baptiste.langlade@hey.com" } ], "support": { "issues": "http://github.com/Innmind/UrlTemplate/issues" }, "require": { - "php": "~7.4|~8.0", - "innmind/url": "~3.0", - "innmind/immutable": "~3.1" + "php": "~8.1", + "innmind/url": "~4.1", + "innmind/immutable": "~4.3" }, "autoload": { "psr-4": { @@ -33,6 +33,6 @@ "phpunit/phpunit": "~9.0", "vimeo/psalm": "~4.4", "innmind/black-box": "^4.16", - "innmind/coding-standard": "^1.1" + "innmind/coding-standard": "~2.0" } } diff --git a/src/Exception/ExpressionLimitCantBeNegative.php b/src/Exception/ExpressionLimitCantBeNegative.php deleted file mode 100644 index ec42071..0000000 --- a/src/Exception/ExpressionLimitCantBeNegative.php +++ /dev/null @@ -1,12 +0,0 @@ - */ - public static function of(Str $string): self; + public static function of(Str $string): Maybe; + + public function expansion(): Expression\Expansion; /** - * @param Map $variables + * @param Map|list> $variables */ public function expand(Map $variables): string; public function regex(): string; diff --git a/src/Expression/Expansion.php b/src/Expression/Expansion.php new file mode 100644 index 0000000..310cc2f --- /dev/null +++ b/src/Expression/Expansion.php @@ -0,0 +1,143 @@ +matches(\sprintf( + '~^\{[\+#\./;\?&]?%s(\*|:\d*)?(,%s(\*|:\d*)?)*\}$~', + Name::characters(), + Name::characters(), + )); + } + + public function clean(Str $value): Str + { + $drop = match ($this) { + self::simple => 1, + default => 2, + }; + + return $value->drop($drop)->dropEnd(1); + } + + public function cleanExplode(Str $value): Str + { + return $this->clean($value)->dropEnd(1); + } + + public function matches(Str $value): bool + { + return $value->matches(\sprintf( + '~^\{%s%s\}$~', + $this->regex(), + Name::characters(), + )); + } + + public function matchesExplode(Str $value): bool + { + return $value->matches(\sprintf( + '~^\{%s%s\*\}$~', + $this->regex(), + Name::characters(), + )); + } + + public function matchesLimit(Str $value): bool + { + return $value->matches(\sprintf( + '~^\{%s%s:\d+\}$~', + $this->regex(), + Name::characters(), + )); + } + + public function matchesMany(Str $value): bool + { + return $value->matches(\sprintf( + '~^\{%s%s(,%s)*\}$~', + $this->regex(), + Name::characters(), + Name::characters(), + )); + } + + public function continuation(): self + { + return match ($this) { + self::query => self::queryContinuation, + default => $this, + }; + } + + public function separator(): string + { + return match ($this) { + self::simple => ',', + self::reserved => ',', + self::fragment => ',', + default => '', + }; + } + + public function separatorRegex(): string + { + return match ($this->separator()) { + '' => '', + ',' => '\\,', + }; + } + + public function explodeSeparator(): string + { + return match ($this) { + self::label => '.', + self::path => '/', + default => ',', + }; + } + + public function regex(): string + { + return match ($this) { + self::simple => '', + default => '\\'.$this->toString(), + }; + } + + public function toString(): string + { + return match ($this) { + self::simple => '', + self::reserved => '+', + self::fragment => '#', + self::label => '.', + self::path => '/', + self::parameter => ';', + self::query => '?', + self::queryContinuation => '&', + }; + } +} diff --git a/src/Expression/Level1.php b/src/Expression/Level1.php index ca483b5..14fe86c 100644 --- a/src/Expression/Level1.php +++ b/src/Expression/Level1.php @@ -6,58 +6,71 @@ use Innmind\UrlTemplate\{ Expression, UrlEncode, - Exception\DomainException, Exception\OnlyScalarCanBeExpandedForExpression, }; use Innmind\Immutable\{ Map, Str, + Maybe, }; +/** + * @psalm-immutable + */ final class Level1 implements Expression { private Name $name; private UrlEncode $encode; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name $name) + private function __construct(Name $name) { $this->name = $name; $this->encode = new UrlEncode; } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{[a-zA-Z0-9_]+\}$~')) { - throw new DomainException($string->toString()); - } - - return new self(new Name($string->trim('{}')->toString())); + /** @var Maybe */ + return Name::one($string, Expansion::simple)->map( + static fn($name) => new self($name), + ); } - public function expand(Map $variables): string + /** + * @psalm-pure + */ + public static function named(Name $name): self { - if (!$variables->contains($this->name->toString())) { - return ''; - } - - $variable = $variables->get($this->name->toString()); + return new self($name); + } - if (\is_array($variable)) { - throw new OnlyScalarCanBeExpandedForExpression($this->name->toString()); - } + public function expansion(): Expansion + { + return Expansion::simple; + } - return ($this->encode)((string) $variable); + public function expand(Map $variables): string + { + /** @psalm-suppress InvalidArgument Because of the filter */ + return $variables + ->get($this->name->toString()) + ->filter(\is_string(...)) + ->match( + fn(string $variable) => ($this->encode)($variable), + static fn() => '', + ); } public function regex(): string { - return $this->regex ?? $this->regex = "(?<{$this->name->toString()}>[a-zA-Z0-9\%\-\.\_\~]*)"; + return "(?<{$this->name->toString()}>[a-zA-Z0-9\%\-\.\_\~]*)"; } public function toString(): string { - return $this->string ?? $this->string = "{{$this->name->toString()}}"; + return "{{$this->name->toString()}}"; } } diff --git a/src/Expression/Level2/Fragment.php b/src/Expression/Level2/Fragment.php index 61f6b28..f1d4e7b 100644 --- a/src/Expression/Level2/Fragment.php +++ b/src/Expression/Level2/Fragment.php @@ -6,59 +6,64 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, UrlEncode, - Exception\DomainException, - Exception\OnlyScalarCanBeExpandedForExpression, }; use Innmind\Immutable\{ Map, Str, + Maybe, }; +/** + * @psalm-immutable + */ final class Fragment implements Expression { private Name $name; private UrlEncode $encode; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name $name) + private function __construct(Name $name) { $this->name = $name; $this->encode = UrlEncode::allowReservedCharacters(); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{#[a-zA-Z0-9_]+\}$~')) { - throw new DomainException($string->toString()); - } + /** @var Maybe */ + return Name::one($string, Expansion::fragment)->map( + static fn($name) => new self($name), + ); + } - return new self(new Name($string->trim('{#}')->toString())); + public function expansion(): Expansion + { + return Expansion::fragment; } public function expand(Map $variables): string { - if (!$variables->contains($this->name->toString())) { - return ''; - } - - $variable = $variables->get($this->name->toString()); - - if (\is_array($variable)) { - throw new OnlyScalarCanBeExpandedForExpression($this->name->toString()); - } - - return '#'.($this->encode)((string) $variable); + /** @psalm-suppress InvalidArgument Because of the filter */ + return $variables + ->get($this->name->toString()) + ->filter(\is_string(...)) + ->match( + fn(string $variable) => '#'.($this->encode)($variable), + static fn() => '', + ); } public function regex(): string { - return $this->regex ?? $this->regex = "\#(?<{$this->name->toString()}>[a-zA-Z0-9\%:/\?#\[\]@!\$&'\(\)\*\+,;=\-\.\_\~]*)"; + return "\#(?<{$this->name->toString()}>[a-zA-Z0-9\%:/\?#\[\]@!\$&'\(\)\*\+,;=\-\.\_\~]*)"; } public function toString(): string { - return $this->string ?? $this->string = "{#{$this->name->toString()}}"; + return "{#{$this->name->toString()}}"; } } diff --git a/src/Expression/Level2/Reserved.php b/src/Expression/Level2/Reserved.php index 63e1a0a..0cef2cf 100644 --- a/src/Expression/Level2/Reserved.php +++ b/src/Expression/Level2/Reserved.php @@ -6,59 +6,72 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, UrlEncode, - Exception\DomainException, - Exception\OnlyScalarCanBeExpandedForExpression, }; use Innmind\Immutable\{ Map, Str, + Maybe, }; +/** + * @psalm-immutable + */ final class Reserved implements Expression { private Name $name; private UrlEncode $encode; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name $name) + private function __construct(Name $name) { $this->name = $name; $this->encode = UrlEncode::allowReservedCharacters(); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{\+[a-zA-Z0-9_]+\}$~')) { - throw new DomainException($string->toString()); - } - - return new self(new Name($string->trim('{+}')->toString())); + /** @var Maybe */ + return Name::one($string, Expansion::reserved)->map( + static fn($name) => new self($name), + ); } - public function expand(Map $variables): string + /** + * @psalm-pure + */ + public static function named(Name $name): self { - if (!$variables->contains($this->name->toString())) { - return ''; - } - - $variable = $variables->get($this->name->toString()); + return new self($name); + } - if (\is_array($variable)) { - throw new OnlyScalarCanBeExpandedForExpression($this->name->toString()); - } + public function expansion(): Expansion + { + return Expansion::reserved; + } - return ($this->encode)((string) $variable); + public function expand(Map $variables): string + { + /** @psalm-suppress InvalidArgument Because of the filter */ + return $variables + ->get($this->name->toString()) + ->filter(\is_string(...)) + ->match( + fn(string $variable) => ($this->encode)($variable), + static fn() => '', + ); } public function regex(): string { - return $this->regex ?? $this->regex = "(?<{$this->name->toString()}>[a-zA-Z0-9\%:/\?#\[\]@!\$&'\(\)\*\+,;=\-\.\_\~]*)"; + return "(?<{$this->name->toString()}>[a-zA-Z0-9\%:/\?#\[\]@!\$&'\(\)\*\+,;=\-\.\_\~]*)"; } public function toString(): string { - return $this->string ?? $this->string = "{+{$this->name->toString()}}"; + return"{+{$this->name->toString()}}"; } } diff --git a/src/Expression/Level3.php b/src/Expression/Level3.php index d370778..350ea1c 100644 --- a/src/Expression/Level3.php +++ b/src/Expression/Level3.php @@ -3,89 +3,75 @@ namespace Innmind\UrlTemplate\Expression; -use Innmind\UrlTemplate\{ - Expression, - Exception\DomainException, -}; +use Innmind\UrlTemplate\Expression; use Innmind\Immutable\{ Map, Sequence, Str, -}; -use function Innmind\Immutable\{ - unwrap, - join, + Maybe, }; +/** + * @psalm-immutable + */ final class Level3 implements Expression { /** @var Sequence */ private Sequence $names; /** @var Sequence */ private Sequence $expressions; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name ...$names) + /** + * @param Sequence $names + */ + private function __construct(Sequence $names) { - /** @var Sequence */ - $this->names = Sequence::of(Name::class, ...$names); - /** @var Sequence */ - $this->expressions = $this->names->mapTo( - Level1::class, - static fn(Name $name) => new Level1($name), - ); + $this->names = $names; + $this->expressions = $this->names->map(Level1::named(...)); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{[a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)+\}$~')) { - throw new DomainException($string->toString()); - } - - /** @var Sequence $names */ - $names = $string - ->trim('{}') - ->split(',') - ->reduce( - Sequence::of(Name::class), - static fn(Sequence $names, Str $name): Sequence => ($names)(new Name($name->toString())), - ); + /** @var Maybe */ + return Name::many($string, Expansion::simple)->map( + static fn($names) => new self($names), + ); + } - return new self(...unwrap($names)); + public function expansion(): Expansion + { + return Expansion::simple; } public function expand(Map $variables): string { - $expanded = $this->expressions->mapTo( - 'string', + $expanded = $this->expressions->map( static fn($expression) => $expression->expand($variables), ); - return join(',', $expanded)->toString(); + return Str::of(',')->join($expanded)->toString(); } public function regex(): string { /** @psalm-suppress InvalidArgument */ - return $this->regex ?? $this->regex = join( - ',', - $this->names->mapTo( - 'string', + return Str::of(',') + ->join($this->names->map( static fn(Name $name) => "(?<{$name->toString()}>[a-zA-Z0-9\%\-\.\_\~]*)", - ), - )->toString(); + )) + ->toString(); } public function toString(): string { - return $this->string ?? $this->string = join( - ',', - $this->names->mapTo( - 'string', + /** @psalm-suppress InvalidArgument */ + return Str::of(',') + ->join($this->names->map( static fn($name) => $name->toString(), - ), - ) + )) ->prepend('{') ->append('}') ->toString(); diff --git a/src/Expression/Level3/Fragment.php b/src/Expression/Level3/Fragment.php index bb52f24..4f720b2 100644 --- a/src/Expression/Level3/Fragment.php +++ b/src/Expression/Level3/Fragment.php @@ -6,91 +6,81 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level2, - Exception\DomainException, }; use Innmind\Immutable\{ Map, Sequence, Str, -}; -use function Innmind\Immutable\{ - join, - unwrap, + Maybe, }; +/** + * @psalm-immutable + */ final class Fragment implements Expression { /** @var Sequence */ private Sequence $names; /** @var Sequence */ private Sequence $expressions; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name ...$names) + /** + * @param Sequence $names + */ + private function __construct(Sequence $names) { - /** @var Sequence */ - $this->names = Sequence::of(Name::class, ...$names); + $this->names = $names; /** @var Sequence */ - $this->expressions = $this->names->mapTo( - Expression::class, - static fn(Name $name) => new Level2\Reserved($name), - ); + $this->expressions = $this->names->map(Level2\Reserved::named(...)); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{#[a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)+\}$~')) { - throw new DomainException($string->toString()); - } - - /** @var Sequence $names */ - $names = $string - ->trim('{#}') - ->split(',') - ->reduce( - Sequence::of(Name::class), - static fn(Sequence $names, Str $name): Sequence => ($names)(new Name($name->toString())), - ); + /** @var Maybe */ + return Name::many($string, Expansion::fragment)->map( + static fn($names) => new self($names), + ); + } - return new self(...unwrap($names)); + public function expansion(): Expansion + { + return Expansion::fragment; } public function expand(Map $variables): string { - $expanded = $this->expressions->mapTo( - 'string', + $expanded = $this->expressions->map( static fn($expression) => $expression->expand($variables), ); - return join(',', $expanded) + return Str::of(',') + ->join($expanded) ->prepend('#') ->toString(); } public function regex(): string { - return $this->regex ?? $this->regex = join( - ',', - $this->expressions->mapTo( - 'string', + return Str::of(',') + ->join($this->expressions->map( static fn($expression) => $expression->regex(), - ), - ) + )) ->prepend('\#') ->toString(); } public function toString(): string { - return $this->string ?? $this->string = join( - ',', - $this->names->mapTo( - 'string', + /** @psalm-suppress InvalidArgument */ + return Str::of(',') + ->join($this->names->map( static fn($element) => $element->toString(), - ), - ) + )) ->prepend('{#') ->append('}') ->toString(); diff --git a/src/Expression/Level3/Label.php b/src/Expression/Level3/Label.php index 73d89a6..2effe4c 100644 --- a/src/Expression/Level3/Label.php +++ b/src/Expression/Level3/Label.php @@ -6,78 +6,70 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level1, - Exception\DomainException, }; use Innmind\Immutable\{ Map, Sequence, Str, -}; -use function Innmind\Immutable\{ - join, - unwrap, + Maybe, }; +/** + * @psalm-immutable + */ final class Label implements Expression { /** @var Sequence */ private Sequence $names; /** @var Sequence */ private Sequence $expressions; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name ...$names) + /** + * @param Sequence $names + */ + private function __construct(Sequence $names) { - /** @var Sequence */ - $this->names = Sequence::of(Name::class, ...$names); + $this->names = $names; /** @var Sequence */ - $this->expressions = $this->names->mapTo( - Expression::class, - static fn(Name $name) => new Level1($name), - ); + $this->expressions = $this->names->map(Level1::named(...)); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{\.[a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)+\}$~')) { - throw new DomainException($string->toString()); - } - - /** @var Sequence $names */ - $names = $string - ->trim('{.}') - ->split(',') - ->reduce( - Sequence::of(Name::class), - static fn(Sequence $names, Str $name): Sequence => ($names)(new Name($name->toString())), - ); + /** @var Maybe */ + return Name::many($string, Expansion::label)->map( + static fn($names) => new self($names), + ); + } - return new self(...unwrap($names)); + public function expansion(): Expansion + { + return Expansion::label; } public function expand(Map $variables): string { - $expanded = $this->expressions->mapTo( - 'string', + $expanded = $this->expressions->map( static fn($expression) => $expression->expand($variables), ); - return join('.', $expanded) + return Str::of('.') + ->join($expanded) ->prepend('.') ->toString(); } public function regex(): string { - return $this->regex ?? $this->regex = join( - '.', - $this->expressions->mapTo( - 'string', + return Str::of('.') + ->join($this->expressions->map( static fn($expression) => $expression->regex(), - ), - ) + )) ->replace('\.', '') ->prepend('\.') ->toString(); @@ -85,13 +77,11 @@ public function regex(): string public function toString(): string { - return $this->string ?? $this->string = join( - ',', - $this->names->mapTo( - 'string', + /** @psalm-suppress InvalidArgument */ + return Str::of(',') + ->join($this->names->map( static fn($element) => $element->toString(), - ), - ) + )) ->prepend('{.') ->append('}') ->toString(); diff --git a/src/Expression/Level3/NamedValues.php b/src/Expression/Level3/NamedValues.php index 530bd37..79c60d1 100644 --- a/src/Expression/Level3/NamedValues.php +++ b/src/Expression/Level3/NamedValues.php @@ -6,59 +6,78 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level1, }; use Innmind\Immutable\{ Map, Sequence, Str, + Maybe, }; -use function Innmind\Immutable\join; +/** + * @psalm-immutable + */ final class NamedValues implements Expression { - private string $lead; - private string $separator; + private Expansion $expansion; /** @var Sequence */ private Sequence $names; /** @var Map */ private Map $expressions; private bool $keyOnlyWhenEmpty = false; - private ?string $regex = null; - private ?string $string = null; - public function __construct(string $lead, string $separator, Name ...$names) + /** + * @param Sequence $names + */ + public function __construct(Expansion $expansion, Sequence $names) { - $this->lead = $lead; - $this->separator = $separator; - /** @var Sequence */ - $this->names = Sequence::of(Name::class, ...$names); + $this->expansion = $expansion; + $this->names = $names; /** @var Map */ - $this->expressions = $this->names->toMapOf( - 'string', - Expression::class, - static fn(Name $name): \Generator => yield $name->toString() => new Level1($name), + $this->expressions = Map::of( + ...$this + ->names + ->map(static fn($name) => [ + $name->toString(), + Level1::named($name), + ]) + ->toList(), ); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { throw new \LogicException('should not be used directly'); } - public static function keyOnlyWhenEmpty(string $lead, string $separator, Name ...$names): self + /** + * @psalm-pure + * + * @param Sequence $names + */ + public static function keyOnlyWhenEmpty(Expansion $expansion, Sequence $names): self { - $self = new self($lead, $separator, ...$names); + $self = new self($expansion, $names); $self->keyOnlyWhenEmpty = true; return $self; } + public function expansion(): Expansion + { + return $this->expansion; + } + public function expand(Map $variables): string { /** @var Sequence */ $expanded = $this->expressions->reduce( - Sequence::of('string'), + Sequence::strings(), function(Sequence $expanded, string $name, Expression $expression) use ($variables): Sequence { $value = Str::of($expression->expand($variables)); @@ -70,39 +89,35 @@ function(Sequence $expanded, string $name, Expression $expression) use ($variabl }, ); - return join($this->separator, $expanded) - ->prepend($this->lead) + return Str::of($this->expansion->continuation()->toString()) + ->join($expanded) + ->prepend($this->expansion->toString()) ->toString(); } public function regex(): string { - return $this->regex ?? $this->regex = join( - '\\'.$this->separator, - $this->names->mapTo( - 'string', + return Str::of($this->expansion->continuation()->regex()) + ->join($this->names->map( fn($name) => \sprintf( '%s=%s%s', $name->toString(), $this->keyOnlyWhenEmpty ? '?' : '', - (new Level1($name))->regex(), + Level1::named($name)->regex(), ), - ), - ) - ->prepend('\\'.$this->lead) + )) + ->prepend($this->expansion->regex()) ->toString(); } public function toString(): string { - return $this->string ?? $this->string = join( - ',', - $this->names->mapTo( - 'string', + /** @psalm-suppress InvalidArgument */ + return Str::of(',') + ->join($this->names->map( static fn($element) => $element->toString(), - ), - ) - ->prepend('{'.$this->lead) + )) + ->prepend('{'.$this->expansion->toString()) ->append('}') ->toString(); } diff --git a/src/Expression/Level3/Parameters.php b/src/Expression/Level3/Parameters.php index 247915d..140a7b8 100644 --- a/src/Expression/Level3/Parameters.php +++ b/src/Expression/Level3/Parameters.php @@ -6,40 +6,52 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, - Exception\DomainException, + Expression\Expansion, }; use Innmind\Immutable\{ Map, Sequence, Str, + Maybe, }; -use function Innmind\Immutable\unwrap; +/** + * @psalm-immutable + */ final class Parameters implements Expression { private Expression $expression; - public function __construct(Name ...$names) + /** + * @param Sequence $names + */ + private function __construct(Sequence $names) { - $this->expression = NamedValues::keyOnlyWhenEmpty(';', ';', ...$names); + $this->expression = NamedValues::keyOnlyWhenEmpty(Expansion::parameter, $names); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{;[a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)+\}$~')) { - throw new DomainException($string->toString()); - } - - /** @var Sequence $names */ - $names = $string - ->trim('{;}') - ->split(',') - ->reduce( - Sequence::of(Name::class), - static fn(Sequence $names, Str $name): Sequence => ($names)(new Name($name->toString())), - ); - - return new self(...unwrap($names)); + /** @var Maybe */ + return Name::many($string, Expansion::parameter)->map( + static fn($names) => new self($names), + ); + } + + /** + * @psalm-pure + */ + public static function named(Name $name): self + { + return new self(Sequence::of($name)); + } + + public function expansion(): Expansion + { + return Expansion::parameter; } public function expand(Map $variables): string diff --git a/src/Expression/Level3/Path.php b/src/Expression/Level3/Path.php index 0e97a90..c4b815d 100644 --- a/src/Expression/Level3/Path.php +++ b/src/Expression/Level3/Path.php @@ -6,92 +6,79 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level1, - Exception\DomainException, }; use Innmind\Immutable\{ Map, Sequence, Str, -}; -use function Innmind\Immutable\{ - join, - unwrap, + Maybe, }; +/** + * @psalm-immutable + */ final class Path implements Expression { /** @var Sequence */ private Sequence $names; /** @var Sequence */ private Sequence $expressions; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name ...$names) + /** + * @param Sequence $names + */ + private function __construct(Sequence $names) { - /** @var Sequence */ - $this->names = Sequence::of(Name::class, ...$names); + $this->names = $names; /** @var Sequence */ - $this->expressions = $this->names->mapTo( - Expression::class, - static fn(Name $name) => new Level1($name), - ); + $this->expressions = $this->names->map(Level1::named(...)); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{/[a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)+\}$~')) { - throw new DomainException($string->toString()); - } - - /** @var Sequence $names */ - $names = $string - ->trim('{/}') - ->split(',') - ->reduce( - Sequence::of(Name::class), - static fn(Sequence $names, Str $name): Sequence => ($names)(new Name($name->toString())), - ); + /** @var Maybe */ + return Name::many($string, Expansion::path)->map( + static fn($names) => new self($names), + ); + } - return new self(...unwrap($names)); + public function expansion(): Expansion + { + return Expansion::path; } public function expand(Map $variables): string { - return join( - '/', - $this->expressions->mapTo( - 'string', + return Str::of('/') + ->join($this->expressions->map( static fn($expression) => $expression->expand($variables), - ), - ) + )) ->prepend('/') ->toString(); } public function regex(): string { - return $this->regex ?? $this->regex = join( - '/', - $this->expressions->mapTo( - 'string', + return Str::of('/') + ->join($this->expressions->map( static fn($expression) => $expression->regex(), - ), - ) + )) ->prepend('/') ->toString(); } public function toString(): string { - return $this->string ?? $this->string = join( - ',', - $this->names->mapTo( - 'string', + /** @psalm-suppress InvalidArgument */ + return Str::of(',') + ->join($this->names->map( static fn($element) => $element->toString(), - ), - ) + )) ->prepend('{/') ->append('}') ->toString(); diff --git a/src/Expression/Level3/Query.php b/src/Expression/Level3/Query.php index a442c23..d821a3e 100644 --- a/src/Expression/Level3/Query.php +++ b/src/Expression/Level3/Query.php @@ -6,40 +6,52 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, - Exception\DomainException, + Expression\Expansion, }; use Innmind\Immutable\{ Map, Sequence, Str, + Maybe, }; -use function Innmind\Immutable\unwrap; +/** + * @psalm-immutable + */ final class Query implements Expression { private Expression $expression; - public function __construct(Name ...$names) + /** + * @param Sequence $names + */ + private function __construct(Sequence $names) { - $this->expression = new NamedValues('?', '&', ...$names); + $this->expression = new NamedValues(Expansion::query, $names); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{\?[a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)+\}$~')) { - throw new DomainException($string->toString()); - } - - /** @var Sequence $names */ - $names = $string - ->trim('{?}') - ->split(',') - ->reduce( - Sequence::of(Name::class), - static fn(Sequence $names, Str $name): Sequence => ($names)(new Name($name->toString())), - ); - - return new self(...unwrap($names)); + /** @var Maybe */ + return Name::many($string, Expansion::query)->map( + static fn($names) => new self($names), + ); + } + + /** + * @psalm-pure + */ + public static function named(Name $name): self + { + return new self(Sequence::of($name)); + } + + public function expansion(): Expansion + { + return Expansion::query; } public function expand(Map $variables): string diff --git a/src/Expression/Level3/QueryContinuation.php b/src/Expression/Level3/QueryContinuation.php index fdb5ee6..6bc6ac0 100644 --- a/src/Expression/Level3/QueryContinuation.php +++ b/src/Expression/Level3/QueryContinuation.php @@ -6,40 +6,49 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, - Exception\DomainException, + Expression\Expansion, }; use Innmind\Immutable\{ Map, Sequence, Str, + Maybe, }; -use function Innmind\Immutable\unwrap; +/** + * @psalm-immutable + */ final class QueryContinuation implements Expression { private Expression $expression; - public function __construct(Name ...$names) + /** + * @param Sequence $names + */ + private function __construct(Sequence $names) { - $this->expression = new NamedValues('&', '&', ...$names); + $this->expression = new NamedValues(Expansion::queryContinuation, $names); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{\&[a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)+\}$~')) { - throw new DomainException($string->toString()); - } - - /** @var Sequence $names */ - $names = $string - ->trim('{&}') - ->split(',') - ->reduce( - Sequence::of(Name::class), - static fn(Sequence $names, Str $name): Sequence => ($names)(new Name($name->toString())), - ); - - return new self(...unwrap($names)); + /** @var Maybe */ + return Name::many($string, Expansion::queryContinuation)->map( + static fn($names) => new self($names), + ); + } + + public static function named(Name $name): self + { + return new self(Sequence::of($name)); + } + + public function expansion(): Expansion + { + return Expansion::queryContinuation; } public function expand(Map $variables): string diff --git a/src/Expression/Level3/Reserved.php b/src/Expression/Level3/Reserved.php index 3412069..f9a401e 100644 --- a/src/Expression/Level3/Reserved.php +++ b/src/Expression/Level3/Reserved.php @@ -6,87 +6,77 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level2, - Exception\DomainException, }; use Innmind\Immutable\{ Map, Sequence, Str, -}; -use function Innmind\Immutable\{ - join, - unwrap, + Maybe, }; +/** + * @psalm-immutable + */ final class Reserved implements Expression { /** @var Sequence */ private Sequence $names; /** @var Sequence */ private Sequence $expressions; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name ...$names) + /** + * @param Sequence $names + */ + private function __construct(Sequence $names) { - /** @var Sequence */ - $this->names = Sequence::of(Name::class, ...$names); + $this->names = $names; /** @var Sequence */ - $this->expressions = $this->names->mapTo( - Expression::class, - static fn(Name $name) => new Level2\Reserved($name), - ); + $this->expressions = $this->names->map(Level2\Reserved::named(...)); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{\+[a-zA-Z0-9_]+(,[a-zA-Z0-9_]+)+\}$~')) { - throw new DomainException($string->toString()); - } - - /** @var Sequence $names */ - $names = $string - ->trim('{+}') - ->split(',') - ->reduce( - Sequence::of(Name::class), - static fn(Sequence $names, Str $name): Sequence => ($names)(new Name($name->toString())), - ); + /** @var Maybe */ + return Name::many($string, Expansion::reserved)->map( + static fn($names) => new self($names), + ); + } - return new self(...unwrap($names)); + public function expansion(): Expansion + { + return Expansion::reserved; } public function expand(Map $variables): string { - $expanded = $this->expressions->mapTo( - 'string', + $expanded = $this->expressions->map( static fn($expression) => $expression->expand($variables), ); - return join(',', $expanded)->toString(); + return Str::of(',')->join($expanded)->toString(); } public function regex(): string { - return $this->regex ?? $this->regex = join( - ',', - $this->expressions->mapTo( - 'string', + return Str::of(',') + ->join($this->expressions->map( static fn($expression) => $expression->regex(), - ), - )->toString(); + )) + ->toString(); } public function toString(): string { - return $this->string ?? $this->string = join( - ',', - $this->names->mapTo( - 'string', + /** @psalm-suppress InvalidArgument */ + return Str::of(',') + ->join($this->names->map( static fn($element) => $element->toString(), - ), - ) + )) ->prepend('{+') ->append('}') ->toString(); diff --git a/src/Expression/Level4.php b/src/Expression/Level4.php index 4a511f1..f1dbf76 100644 --- a/src/Expression/Level4.php +++ b/src/Expression/Level4.php @@ -8,70 +8,63 @@ Expression\Level4\Composite, Exception\DomainException, Exception\ExplodeExpressionCantBeMatched, - Exception\ExpressionLimitCantBeNegative, }; use Innmind\Immutable\{ Map, Str, Sequence, -}; -use function Innmind\Immutable\{ - join, - unwrap, + Maybe, }; +/** + * @psalm-immutable + */ final class Level4 implements Expression { private Name $name; private Expression $expression; + /** @var ?positive-int */ private ?int $limit = null; private bool $explode = false; - private string $lead = ''; - private string $separator = ','; - private ?string $regex = null; - private ?string $string = null; + private Expansion $expansion; - public function __construct(Name $name) + private function __construct(Name $name) { $this->name = $name; - $this->expression = new Level1($name); + $this->expression = Level1::named($name); + $this->expansion = Expansion::simple; } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if ($string->matches('~^\{[a-zA-Z0-9_]+\}$~')) { - return new self(new Name($string->trim('{}')->toString())); - } - - if ($string->matches('~^\{[a-zA-Z0-9_]+\*\}$~')) { - return self::explode(new Name($string->trim('{*}')->toString())); - } - - if ($string->matches('~^\{[a-zA-Z0-9_]+:\d+\}$~')) { - $string = $string->trim('{}'); - [$name, $limit] = unwrap($string->split(':')); - - return self::limit( - new Name($name->toString()), - (int) $limit->toString(), - ); - } - - throw new DomainException($string->toString()); + return Level4\Parse::of( + $string, + static fn(Name $name) => new self($name), + self::explode(...), + self::limit(...), + Expansion::simple, + ); } + /** + * @psalm-pure + * + * @param positive-int $limit + */ public static function limit(Name $name, int $limit): self { - if ($limit < 0) { - throw new ExpressionLimitCantBeNegative($limit); - } - $self = new self($name); $self->limit = $limit; return $self; } + /** + * @psalm-pure + */ public static function explode(Name $name): self { $self = new self($name); @@ -80,27 +73,23 @@ public static function explode(Name $name): self return $self; } - public function add(Str $pattern): Composite + /** + * @psalm-pure + */ + public static function named(Name $name): self { - return new Composite( - ',', - $this, - self::of($pattern->prepend('{')->append('}')), - ); + return new self($name); } - public function withLead(string $lead): self + public function expansion(): Expansion { - $self = clone $this; - $self->lead = $lead; - - return $self; + return Expansion::simple; } - public function withSeparator(string $separator): self + public function withExpansion(Expansion $expansion): self { $self = clone $this; - $self->separator = $separator; + $self->expansion = $expansion; return $self; } @@ -109,31 +98,29 @@ public function withSeparator(string $separator): self * Not ideal technic but didn't find a better to reduce duplicated code * @internal * - * @param class-string $expression + * @param pure-callable(Name): Expression $expression */ - public function withExpression(string $expression): self + public function withExpression(callable $expression): self { - if (!\is_a($expression, Expression::class, true)) { - throw new DomainException($expression); - } - $self = clone $this; - $self->expression = new $expression($self->name); + $self->expression = $expression($self->name); return $self; } public function expand(Map $variables): string { - if (!$variables->contains($this->name->toString())) { + $variable = $variables->get($this->name->toString())->match( + static fn($variable) => $variable, + static fn() => null, + ); + + if (\is_null($variable)) { return ''; } - /** @var scalar|array{0:string, 1:scalar} */ - $variable = $variables->get($this->name->toString()); - if (\is_array($variable)) { - return $this->expandList($variables, ...$variable); + return $this->expandList($variables, $variable); } if ($this->explode) { @@ -141,7 +128,7 @@ public function expand(Map $variables): string } if ($this->mustLimit()) { - $value = Str::of((string) $variable)->substring(0, $this->limit); + $value = Str::of($variable)->take($this->limit); $value = $this->expression->expand( ($variables)($this->name->toString(), $value->toString()), ); @@ -149,15 +136,11 @@ public function expand(Map $variables): string $value = $this->expression->expand($variables); } - return "{$this->lead}$value"; + return "{$this->expansion->toString()}$value"; } public function regex(): string { - if (\is_string($this->regex)) { - return $this->regex; - } - if ($this->explode) { throw new ExplodeExpressionCantBeMatched; } @@ -165,106 +148,99 @@ public function regex(): string if ($this->mustLimit()) { // replace '*' match by the actual limit $regex = Str::of($this->expression->regex()) - ->substring(0, -2) + ->dropEnd(2) ->append("{{$this->limit}})") ->toString(); } else { $regex = $this->expression->regex(); } - return $this->regex = \sprintf( + return \sprintf( '%s%s', - $this->lead ? '\\'.$this->lead : '', + $this->expansion->regex(), $regex, ); } public function toString(): string { - if (\is_string($this->string)) { - return $this->string; - } - if ($this->mustLimit()) { - return $this->string = "{{$this->lead}{$this->name->toString()}:{$this->limit}}"; + return "{{$this->expansion->toString()}{$this->name->toString()}:{$this->limit}}"; } if ($this->explode) { - return $this->string = "{{$this->lead}{$this->name->toString()}*}"; + return "{{$this->expansion->toString()}{$this->name->toString()}*}"; } - return $this->string = "{{$this->lead}{$this->name->toString()}}"; + return "{{$this->expansion->toString()}{$this->name->toString()}}"; } + /** + * @psalm-assert-if-true positive-int $this->limit + */ private function mustLimit(): bool { return \is_int($this->limit); } /** - * @param Map $variables - * @param array $variablesToExpand + * @param Map|list> $variables + * @param list|list $variablesToExpand */ - private function expandList(Map $variables, ...$variablesToExpand): string + private function expandList(Map $variables, array $variablesToExpand): string { if ($this->explode) { return $this->explodeList($variables, $variablesToExpand); } - /** @var Sequence */ - $flattenedVariables = Sequence::of('scalar|array', ...$variablesToExpand)->reduce( - Sequence::of('scalar'), - static function(Sequence $values, $variableToExpand): Sequence { + $flattenedVariables = Sequence::of(...$variablesToExpand)->flatMap( + static function($variableToExpand): Sequence { if (\is_array($variableToExpand)) { [$name, $variableToExpand] = $variableToExpand; - return ($values)($name)($variableToExpand); + return Sequence::of($name, $variableToExpand); } - return ($values)($variableToExpand); + return Sequence::of($variableToExpand); }, ); - $expanded = $flattenedVariables - ->map(function($variableToExpand) use ($variables): string { + $expanded = $flattenedVariables->map( + function($variableToExpand) use ($variables): string { // here we use the level1 expression to transform the variable to // be expanded to its string representation return $this->expression->expand( ($variables)($this->name->toString(), $variableToExpand), ); - }) - ->toSequenceOf('string'); + }, + ); - return join($this->separator, $expanded) - ->prepend($this->lead) + return $this->separator() + ->join($expanded) + ->prepend($this->expansion->toString()) ->toString(); } /** - * @param Map $variables - * @param array $variablesToExpand + * @param Map|list> $variables + * @param list|list $variablesToExpand */ private function explodeList(Map $variables, array $variablesToExpand): string { - $expanded = Sequence::of('scalar|array', ...$variablesToExpand) - ->map(function($variableToExpand) use ($variables): string { + $expanded = Sequence::of(...$variablesToExpand)->map( + function($variableToExpand) use ($variables): string { if (\is_array($variableToExpand)) { - /** - * @var string $name - * @var scalar $value - */ [$name, $value] = $variableToExpand; $variableToExpand = $value; } - /** @psalm-suppress MixedArgument */ $variables = ($variables)($this->name->toString(), $variableToExpand); $value = $this->expression->expand($variables); if (isset($name)) { /** @psalm-suppress MixedArgument */ - $name = new Name($name); + $name = Name::of($name); $value = \sprintf( '%s=%s', $name->toString(), @@ -273,11 +249,21 @@ private function explodeList(Map $variables, array $variablesToExpand): string } return $value; - }) - ->toSequenceOf('string'); + }, + ); - return join($this->separator, $expanded) - ->prepend($this->lead) + return $this->separator() + ->join($expanded) + ->prepend($this->expansion->toString()) ->toString(); } + + private function separator(): Str + { + if (!$this->explode) { + return Str::of(','); + } + + return Str::of($this->expansion->explodeSeparator()); + } } diff --git a/src/Expression/Level4/Composite.php b/src/Expression/Level4/Composite.php index 9e7a277..d5bdc8c 100644 --- a/src/Expression/Level4/Composite.php +++ b/src/Expression/Level4/Composite.php @@ -6,77 +6,67 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expressions, - Exception\DomainException, }; use Innmind\Immutable\{ Map, Sequence, Str, + Maybe, }; -use function Innmind\Immutable\join; +/** + * @psalm-immutable + */ final class Composite implements Expression { - private string $separator; - private string $type; + private Expansion $expansion; /** @var Sequence */ private Sequence $expressions; - private bool $removeLead = false; - private ?string $regex = null; - private ?string $string = null; - - public function __construct( - string $separator, - Expression $level4, - Expression ...$expressions - ) { - $this->separator = $separator; - $this->type = \get_class($level4); - /** @var Sequence */ - $this->expressions = Sequence::of(Expression::class, $level4, ...$expressions); - } - - public static function removeLead( - string $separator, - Expression $level4, - Expression ...$expressions - ): self { - $self = new self($separator, $level4, ...$expressions); - $self->removeLead = true; - return $self; + /** + * @param Sequence $expressions + */ + private function __construct(Expansion $expansion, Sequence $expressions) + { + $this->expansion = $expansion; + $this->expressions = $expressions; } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if (!$string->matches('~^\{[\+#\./;\?&]?[a-zA-Z0-9_]+(\*|:\d*)?(,[a-zA-Z0-9_]+(\*|:\d*)?)+\}$~')) { - throw new DomainException($string->toString()); - } - - $pieces = $string - ->trim('{}') - ->split(','); - - /** - * @psalm-suppress UndefinedInterfaceMethod - * @psalm-suppress MixedInferredReturnType - */ - return $pieces - ->drop(1) - ->reduce( - Expressions::of($pieces->first()->prepend('{')->append('}')), - static function(Expression $level4, Str $expression): Expression { - /** @psalm-suppress MixedReturnStatement */ - return $level4->add($expression); - }, + /** @var Maybe */ + return Maybe::just($string) + ->filter(Expansion::matchesLevel4(...)) + ->map(Expansion::simple->clean(...)) + ->map(static fn($string) => $string->split(',')) + ->flatMap( + static fn($expressions) => $expressions + ->first() + ->map(static fn($first) => $first->prepend('{')->append('}')) + ->flatMap(Expressions::of(...)) + ->flatMap( + static fn($first) => self::parse($first, $expressions->drop(1)) + ->map(static fn($expressions) => new self( + $first->expansion(), + $expressions, + )), + ), ); } + public function expansion(): Expansion + { + return $this->expansion; + } + public function expand(Map $variables): string { - $expanded = $this->expressions->mapTo( - 'string', + $expanded = $this->expressions->map( static fn($expression) => $expression->expand($variables), ); @@ -87,58 +77,44 @@ public function expand(Map $variables): string ->take(1) ->append( $expanded->drop(1)->map(function(string $value): string { - if ($this->removeLead) { - return Str::of($value)->substring(1)->toString(); + if ($this->removeLead()) { + return Str::of($value)->drop(1)->toString(); } return $value; }), ); - return join($this->separator, $expanded)->toString(); + return Str::of($this->expansion()->separator())->join($expanded)->toString(); } public function regex(): string { - if (\is_string($this->regex)) { - return $this->regex; - } - $remaining = $this ->expressions ->drop(1) - ->mapTo( - 'string', - function(Expression $expression): string { - if ($this->removeLead) { - return Str::of($expression->regex())->substring(2)->toString(); - } - - return $expression->regex(); - }, - ); - - return $this->regex = join( - $this->separator ? '\\'.$this->separator : '', - $this - ->expressions - ->take(1) - ->mapTo( - 'string', - static fn($expression) => $expression->regex(), - ) - ->append($remaining) - )->toString(); + ->map(function(Expression $expression): string { + if ($this->removeLead()) { + return Str::of($expression->regex())->drop(2)->toString(); + } + + return $expression->regex(); + }); + + return Str::of($this->expansion()->separatorRegex()) + ->join( + $this + ->expressions + ->take(1) + ->map(static fn($expression) => $expression->regex()) + ->append($remaining), + ) + ->toString(); } public function toString(): string { - if (\is_string($this->string)) { - return $this->string; - } - - $expressions = $this->expressions->mapTo( - Str::class, + $expressions = $this->expressions->map( static fn($expression) => Str::of($expression->toString())->trim('{}'), ); @@ -154,14 +130,38 @@ public function toString(): string return $expression->leftTrim('+#/.;?&'); }), ) - ->mapTo( - 'string', - static fn($element) => $element->toString(), - ); + ->map(static fn($element) => $element->toString()); - return $this->string = join(',', $expressions) + return Str::of(',') + ->join($expressions) ->prepend('{') ->append('}') ->toString(); } + + /** + * @psalm-pure + * + * @param Sequence $expressions + * + * @return Maybe> + */ + private static function parse(Expression $first, Sequence $expressions): Maybe + { + /** @var Maybe> */ + return Maybe::all( + Maybe::just($first), + ...$expressions + ->map(static fn($expression) => $expression->prepend($first->expansion()->continuation()->toString())) + ->map(static fn($expression) => $expression->prepend('{')->append('}')) + ->map(Expressions::of(...)) + ->toList(), + ) + ->map(Sequence::of(...)); + } + + private function removeLead(): bool + { + return $this->expansion() === Expansion::fragment; + } } diff --git a/src/Expression/Level4/Fragment.php b/src/Expression/Level4/Fragment.php index 42fa2a9..e83e61b 100644 --- a/src/Expression/Level4/Fragment.php +++ b/src/Expression/Level4/Fragment.php @@ -6,85 +6,79 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level2, Expression\Level4, Exception\DomainException, Exception\LogicException, - Exception\ExpressionLimitCantBeNegative, }; use Innmind\Immutable\{ Map, Str, + Maybe, }; -use function Innmind\Immutable\unwrap; +/** + * @psalm-immutable + */ final class Fragment implements Expression { private Expression $expression; - public function __construct(Name $name) + private function __construct(Name $name) { - $this->expression = (new Level4($name)) - ->withLead('#') - ->withExpression(Level2\Reserved::class); + $this->expression = Level4::named($name) + ->withExpansion(Expansion::fragment) + ->withExpression(Level2\Reserved::named(...)); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if ($string->matches('~^\{#[a-zA-Z0-9_]+\}$~')) { - return new self(new Name($string->trim('{#}')->toString())); - } - - if ($string->matches('~^\{#[a-zA-Z0-9_]+\*\}$~')) { - return self::explode(new Name($string->trim('{#*}')->toString())); - } - - if ($string->matches('~^\{#[a-zA-Z0-9_]+:\d+\}$~')) { - $string = $string->trim('{#}'); - [$name, $limit] = unwrap($string->split(':')); - - return self::limit( - new Name($name->toString()), - (int) $limit->toString(), - ); - } - - throw new DomainException($string->toString()); + return Parse::of( + $string, + static fn(Name $name) => new self($name), + self::explode(...), + self::limit(...), + Expansion::fragment, + ); } + /** + * @psalm-pure + * + * @param positive-int $limit + */ public static function limit(Name $name, int $limit): self { - if ($limit < 0) { - throw new ExpressionLimitCantBeNegative($limit); - } - $self = new self($name); $self->expression = Level4::limit($name, $limit) - ->withLead('#') - ->withExpression(Level2\Reserved::class); + ->withExpansion(Expansion::fragment) + ->withExpression(Level2\Reserved::named(...)); return $self; } - public function add(Str $pattern): Composite - { - return Composite::removeLead( - ',', - $this, - self::of($pattern->prepend('{#')->append('}')), - ); - } - + /** + * @psalm-pure + */ public static function explode(Name $name): self { $self = new self($name); $self->expression = Level4::explode($name) - ->withLead('#') - ->withExpression(Level2\Reserved::class); + ->withExpansion(Expansion::fragment) + ->withExpression(Level2\Reserved::named(...)); return $self; } + public function expansion(): Expansion + { + return Expansion::fragment; + } + public function regex(): string { return $this->expression->regex(); diff --git a/src/Expression/Level4/Label.php b/src/Expression/Level4/Label.php index 84721c9..b8280c9 100644 --- a/src/Expression/Level4/Label.php +++ b/src/Expression/Level4/Label.php @@ -6,78 +6,71 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level1, Expression\Level4, Exception\DomainException, - Exception\ExpressionLimitCantBeNegative, }; use Innmind\Immutable\{ Map, Str, + Maybe, }; -use function Innmind\Immutable\unwrap; +/** + * @psalm-immutable + */ final class Label implements Expression { private Expression $expression; - public function __construct(Name $name) + private function __construct(Name $name) { - $this->expression = (new Level4($name))->withLead('.'); + $this->expression = Level4::named($name)->withExpansion(Expansion::label); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if ($string->matches('~^\{\.[a-zA-Z0-9_]+\}$~')) { - return new self(new Name($string->trim('{.}')->toString())); - } - - if ($string->matches('~^\{\.[a-zA-Z0-9_]+\*\}$~')) { - return self::explode(new Name($string->trim('{.*}')->toString())); - } - - if ($string->matches('~^\{\.[a-zA-Z0-9_]+:\d+\}$~')) { - $string = $string->trim('{.}'); - [$name, $limit] = unwrap($string->split(':')); - - return self::limit( - new Name($name->toString()), - (int) $limit->toString(), - ); - } - - throw new DomainException($string->toString()); + return Parse::of( + $string, + static fn(Name $name) => new self($name), + self::explode(...), + self::limit(...), + Expansion::label, + ); } + /** + * @psalm-pure + * + * @param positive-int $limit + */ public static function limit(Name $name, int $limit): self { - if ($limit < 0) { - throw new ExpressionLimitCantBeNegative($limit); - } - $self = new self($name); - $self->expression = Level4::limit($name, $limit)->withLead('.'); + $self->expression = Level4::limit($name, $limit)->withExpansion(Expansion::label); return $self; } + /** + * @psalm-pure + */ public static function explode(Name $name): self { $self = new self($name); $self->expression = Level4::explode($name) - ->withLead('.') - ->withSeparator('.'); + ->withExpansion(Expansion::label); return $self; } - public function add(Str $pattern): Composite + public function expansion(): Expansion { - return new Composite( - '', - $this, - self::of($pattern->prepend('{.')->append('}')), - ); + return Expansion::label; } public function expand(Map $variables): string diff --git a/src/Expression/Level4/Parameters.php b/src/Expression/Level4/Parameters.php index 45cab79..b4fcbba 100644 --- a/src/Expression/Level4/Parameters.php +++ b/src/Expression/Level4/Parameters.php @@ -6,73 +6,67 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level1, Expression\Level3, Expression\Level4, Exception\DomainException, Exception\ExplodeExpressionCantBeMatched, - Exception\ExpressionLimitCantBeNegative, }; use Innmind\Immutable\{ Map, Str, Sequence, -}; -use function Innmind\Immutable\{ - join, - unwrap, + Maybe, }; +/** + * @psalm-immutable + */ final class Parameters implements Expression { private Name $name; + /** @var ?positive-int */ private ?int $limit = null; private bool $explode = false; private Expression $expression; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name $name) + private function __construct(Name $name) { $this->name = $name; - $this->expression = new Level1($name); + $this->expression = Level1::named($name); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if ($string->matches('~^\{;[a-zA-Z0-9_]+\}$~')) { - return new self(new Name($string->trim('{;}')->toString())); - } - - if ($string->matches('~^\{;[a-zA-Z0-9_]+\*\}$~')) { - return self::explode(new Name($string->trim('{;*}')->toString())); - } - - if ($string->matches('~^\{;[a-zA-Z0-9_]+:\d+\}$~')) { - $string = $string->trim('{;}'); - [$name, $limit] = unwrap($string->split(':')); - - return self::limit( - new Name($name->toString()), - (int) $limit->toString(), - ); - } - - throw new DomainException($string->toString()); + return Parse::of( + $string, + static fn(Name $name) => new self($name), + self::explode(...), + self::limit(...), + Expansion::parameter, + ); } + /** + * @psalm-pure + * + * @param positive-int $limit + */ public static function limit(Name $name, int $limit): self { - if ($limit < 0) { - throw new ExpressionLimitCantBeNegative($limit); - } - $self = new self($name); $self->limit = $limit; return $self; } + /** + * @psalm-pure + */ public static function explode(Name $name): self { $self = new self($name); @@ -81,39 +75,34 @@ public static function explode(Name $name): self return $self; } - public function add(Str $pattern): Composite + public function expansion(): Expansion { - return new Composite( - '', - $this, - self::of($pattern->prepend('{;')->append('}')), - ); + return Expansion::parameter; } - /** - * @param Map $variables - */ public function expand(Map $variables): string { - if (!$variables->contains($this->name->toString())) { + $variable = $variables->get($this->name->toString())->match( + static fn($variable) => $variable, + static fn() => null, + ); + + if (\is_null($variable)) { return ''; } - /** @var scalar|array{0:string, 1:scalar} */ - $variable = $variables->get($this->name->toString()); - if (\is_array($variable)) { - return $this->expandList($variables, ...$variable); + return $this->expandList($variables, $variable); } if ($this->explode) { - return $this->expandList($variables, $variable); + return $this->explodeList($variables, [$variable]); } $value = Str::of($this->expression->expand($variables)); if ($this->mustLimit()) { - return ";{$this->name->toString()}={$value->substring(0, $this->limit)->toString()}"; + return ";{$this->name->toString()}={$value->take($this->limit)->toString()}"; } return ";{$this->name->toString()}={$value->toString()}"; @@ -121,10 +110,6 @@ public function expand(Map $variables): string public function regex(): string { - if (\is_string($this->regex)) { - return $this->regex; - } - if ($this->explode) { throw new ExplodeExpressionCantBeMatched; } @@ -132,14 +117,14 @@ public function regex(): string if ($this->mustLimit()) { // replace '*' match by the actual limit $regex = Str::of($this->expression->regex()) - ->substring(0, -2) + ->dropEnd(2) ->append("{{$this->limit}})") ->toString(); } else { $regex = $this->expression->regex(); } - return $this->regex = \sprintf( + return \sprintf( '\;%s=%s', $this->name->toString(), $regex, @@ -148,92 +133,84 @@ public function regex(): string public function toString(): string { - if (\is_string($this->string)) { - return $this->string; - } - if ($this->mustLimit()) { - return $this->string = "{;{$this->name->toString()}:{$this->limit}}"; + return "{;{$this->name->toString()}:{$this->limit}}"; } if ($this->explode) { - return $this->string = "{;{$this->name->toString()}*}"; + return "{;{$this->name->toString()}*}"; } - return $this->string = "{;{$this->name->toString()}}"; + return "{;{$this->name->toString()}}"; } + /** + * @psalm-assert-if-true positive-int $this->limit + */ private function mustLimit(): bool { return \is_int($this->limit); } /** - * @param Map $variables - * @param array $variablesToExpand + * @param Map|list> $variables + * @param list|list $variablesToExpand */ - private function expandList(Map $variables, ...$variablesToExpand): string + private function expandList(Map $variables, array $variablesToExpand): string { if ($this->explode) { return $this->explodeList($variables, $variablesToExpand); } - /** @var Sequence */ - $flattenedVariables = Sequence::of('scalar|array', ...$variablesToExpand)->reduce( - Sequence::of('scalar'), - static function(Sequence $values, $variableToExpand): Sequence { + $flattenedVariables = Sequence::of(...$variablesToExpand)->flatMap( + static function($variableToExpand): Sequence { if (\is_array($variableToExpand)) { [$name, $variableToExpand] = $variableToExpand; - return ($values)($name)($variableToExpand); + return Sequence::of($name, $variableToExpand); } - return ($values)($variableToExpand); + return Sequence::of($variableToExpand); }, ); - $expanded = $flattenedVariables - ->map(function($variableToExpand) use ($variables): string { + $expanded = $flattenedVariables->map( + function($variableToExpand) use ($variables): string { // here we use the level1 expression to transform the variable to // be expanded to its string representation - /** @psalm-suppress MixedArgument */ $variables = ($variables)($this->name->toString(), $variableToExpand); return $this->expression->expand($variables); - }) - ->toSequenceOf('string'); + }, + ); - return join(',', $expanded) + return Str::of(',') + ->join($expanded) ->prepend(";{$this->name->toString()}=") ->toString(); } /** - * @param Map $variables - * @param array $variablesToExpand + * @param Map|list> $variables + * @param list|list $variablesToExpand */ private function explodeList(Map $variables, array $variablesToExpand): string { - $expanded = Sequence::of('scalar|array', ...$variablesToExpand) - ->map(function($variableToExpand) use ($variables): string { + $expanded = Sequence::of(...$variablesToExpand)->map( + function($variableToExpand) use ($variables): string { $name = $this->name; if (\is_array($variableToExpand)) { - /** - * @var string $name - * @var scalar $value - */ [$name, $value] = $variableToExpand; - $name = new Name($name); + $name = Name::of($name); $variableToExpand = $value; } - /** @psalm-suppress MixedArgument */ $variables = ($variables)($name->toString(), $variableToExpand); - return (new Level3\Parameters($name))->expand($variables); - }) - ->toSequenceOf('string'); + return Level3\Parameters::named($name)->expand($variables); + }, + ); - return join('', $expanded)->toString(); + return Str::of('')->join($expanded)->toString(); } } diff --git a/src/Expression/Level4/Parse.php b/src/Expression/Level4/Parse.php new file mode 100644 index 0000000..bbe0948 --- /dev/null +++ b/src/Expression/Level4/Parse.php @@ -0,0 +1,71 @@ + + */ + public static function of( + Str $string, + callable $standard, + callable $explode, + callable $limit, + Expansion $expansion, + ): Maybe { + return Name::one($string, $expansion) + ->map($standard) + ->otherwise(static fn() => self::explode($string, $explode, $expansion)) + ->otherwise(static fn() => self::limit($string, $limit, $expansion)); + } + + /** + * @psalm-pure + * + * @param pure-callable(Name): Expression $explode + * + * @return Maybe + */ + private static function explode( + Str $string, + callable $explode, + Expansion $expansion, + ): Maybe { + return Name::explode($string, $expansion)->map($explode); + } + + /** + * @psalm-pure + * + * @param pure-callable(Name, positive-int): Expression $limit + * + * @return Maybe + */ + private static function limit( + Str $string, + callable $limit, + Expansion $expansion, + ): Maybe { + return Name::limit($string, $expansion)->map( + static fn($tuple) => $limit($tuple[0], $tuple[1]), + ); + } +} diff --git a/src/Expression/Level4/Path.php b/src/Expression/Level4/Path.php index eb40380..096daea 100644 --- a/src/Expression/Level4/Path.php +++ b/src/Expression/Level4/Path.php @@ -6,78 +6,71 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level1, Expression\Level4, Exception\DomainException, - Exception\ExpressionLimitCantBeNegative, }; use Innmind\Immutable\{ Map, Str, + Maybe, }; -use function Innmind\Immutable\unwrap; +/** + * @psalm-immutable + */ final class Path implements Expression { private Expression $expression; - public function __construct(Name $name) + private function __construct(Name $name) { - $this->expression = (new Level4($name))->withLead('/'); + $this->expression = Level4::named($name)->withExpansion(Expansion::path); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if ($string->matches('~^\{/[a-zA-Z0-9_]+\}$~')) { - return new self(new Name($string->trim('{/}')->toString())); - } - - if ($string->matches('~^\{/[a-zA-Z0-9_]+\*\}$~')) { - return self::explode(new Name($string->trim('{/*}')->toString())); - } - - if ($string->matches('~^\{/[a-zA-Z0-9_]+:\d+\}$~')) { - $string = $string->trim('{/}'); - [$name, $limit] = unwrap($string->split(':')); - - return self::limit( - new Name($name->toString()), - (int) $limit->toString(), - ); - } - - throw new DomainException($string->toString()); + return Parse::of( + $string, + static fn(Name $name) => new self($name), + self::explode(...), + self::limit(...), + Expansion::path, + ); } + /** + * @psalm-pure + * + * @param positive-int $limit + */ public static function limit(Name $name, int $limit): self { - if ($limit < 0) { - throw new ExpressionLimitCantBeNegative($limit); - } - $self = new self($name); - $self->expression = Level4::limit($name, $limit)->withLead('/'); + $self->expression = Level4::limit($name, $limit)->withExpansion(Expansion::path); return $self; } + /** + * @psalm-pure + */ public static function explode(Name $name): self { $self = new self($name); $self->expression = Level4::explode($name) - ->withLead('/') - ->withSeparator('/'); + ->withExpansion(Expansion::path); return $self; } - public function add(Str $pattern): Composite + public function expansion(): Expansion { - return new Composite( - '', - $this, - self::of($pattern->prepend('{/')->append('}')), - ); + return Expansion::path; } public function expand(Map $variables): string diff --git a/src/Expression/Level4/Query.php b/src/Expression/Level4/Query.php index dfd61cd..275579a 100644 --- a/src/Expression/Level4/Query.php +++ b/src/Expression/Level4/Query.php @@ -6,73 +6,67 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level1, Expression\Level3, Expression\Level4, Exception\DomainException, Exception\ExplodeExpressionCantBeMatched, - Exception\ExpressionLimitCantBeNegative, }; use Innmind\Immutable\{ Map, Str, Sequence, -}; -use function Innmind\Immutable\{ - join, - unwrap, + Maybe, }; +/** + * @psalm-immutable + */ final class Query implements Expression { private Name $name; + /** @var ?positive-int */ private ?int $limit = null; private bool $explode = false; private Expression $expression; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name $name) + private function __construct(Name $name) { $this->name = $name; - $this->expression = new Level1($name); + $this->expression = Level1::named($name); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if ($string->matches('~^\{\?[a-zA-Z0-9_]+\}$~')) { - return new self(new Name($string->trim('{?}')->toString())); - } - - if ($string->matches('~^\{\?[a-zA-Z0-9_]+\*\}$~')) { - return self::explode(new Name($string->trim('{?*}')->toString())); - } - - if ($string->matches('~^\{\?[a-zA-Z0-9_]+:\d+\}$~')) { - $string = $string->trim('{?}'); - [$name, $limit] = unwrap($string->split(':')); - - return self::limit( - new Name($name->toString()), - (int) $limit->toString(), - ); - } - - throw new DomainException($string->toString()); + return Parse::of( + $string, + static fn(Name $name) => new self($name), + self::explode(...), + self::limit(...), + Expansion::query, + ); } + /** + * @psalm-pure + * + * @param positive-int $limit + */ public static function limit(Name $name, int $limit): self { - if ($limit < 0) { - throw new ExpressionLimitCantBeNegative($limit); - } - $self = new self($name); $self->limit = $limit; return $self; } + /** + * @psalm-pure + */ public static function explode(Name $name): self { $self = new self($name); @@ -81,39 +75,34 @@ public static function explode(Name $name): self return $self; } - public function add(Str $pattern): Composite + public function expansion(): Expansion { - return new Composite( - '', - $this, - QueryContinuation::of($pattern->prepend('{&')->append('}')), - ); + return Expansion::query; } - /** - * @param Map $variables - */ public function expand(Map $variables): string { - if (!$variables->contains($this->name->toString())) { + $variable = $variables->get($this->name->toString())->match( + static fn($variable) => $variable, + static fn() => null, + ); + + if (\is_null($variable)) { return ''; } - /** @var scalar|array{0:string, 1:scalar} */ - $variable = $variables->get($this->name->toString()); - if (\is_array($variable)) { - return $this->expandList($variables, ...$variable); + return $this->expandList($variables, $variable); } if ($this->explode) { - return $this->expandList($variables, $variable); + return $this->explodeList($variables, [$variable]); } $value = Str::of($this->expression->expand($variables)); if ($this->mustLimit()) { - return "?{$this->name->toString()}={$value->substring(0, $this->limit)->toString()}"; + return "?{$this->name->toString()}={$value->take($this->limit)->toString()}"; } return "?{$this->name->toString()}={$value->toString()}"; @@ -121,10 +110,6 @@ public function expand(Map $variables): string public function regex(): string { - if (\is_string($this->regex)) { - return $this->regex; - } - if ($this->explode) { throw new ExplodeExpressionCantBeMatched; } @@ -132,14 +117,14 @@ public function regex(): string if ($this->mustLimit()) { // replace '*' match by the actual limit $regex = Str::of($this->expression->regex()) - ->substring(0, -2) + ->dropEnd(2) ->append("{{$this->limit}})") ->toString(); } else { $regex = $this->expression->regex(); } - return $this->regex = \sprintf( + return \sprintf( '\?%s=%s', $this->name->toString(), $regex, @@ -148,98 +133,87 @@ public function regex(): string public function toString(): string { - if (\is_string($this->string)) { - return $this->string; - } - if ($this->mustLimit()) { - return $this->string = "{?{$this->name->toString()}:{$this->limit}}"; + return "{?{$this->name->toString()}:{$this->limit}}"; } if ($this->explode) { - return $this->string = "{?{$this->name->toString()}*}"; + return "{?{$this->name->toString()}*}"; } - return $this->string = "{?{$this->name->toString()}}"; + return "{?{$this->name->toString()}}"; } + /** + * @psalm-assert-if-true positive-int $this->limit + */ private function mustLimit(): bool { return \is_int($this->limit); } /** - * @param Map $variables - * @param array $variablesToExpand + * @param Map|list> $variables + * @param list|list $variablesToExpand */ - private function expandList(Map $variables, ...$variablesToExpand): string + private function expandList(Map $variables, array $variablesToExpand): string { if ($this->explode) { return $this->explodeList($variables, $variablesToExpand); } - /** @var Sequence */ - $flattenedVariables = Sequence::of('scalar|array', ...$variablesToExpand)->reduce( - Sequence::of('scalar'), - static function(Sequence $values, $variableToExpand): Sequence { + $flattenedVariables = Sequence::of(...$variablesToExpand)->flatMap( + static function($variableToExpand): Sequence { if (\is_array($variableToExpand)) { [$name, $variableToExpand] = $variableToExpand; - return ($values)($name)($variableToExpand); + return Sequence::of($name, $variableToExpand); } - return ($values)($variableToExpand); + return Sequence::of($variableToExpand); }, ); - $expanded = $flattenedVariables - ->map(function($variableToExpand) use ($variables): string { - // here we use the level1 expression to transform the variable to - // be expanded to its string representation + $expanded = $flattenedVariables->map(function($variableToExpand) use ($variables): string { + // here we use the level1 expression to transform the variable to + // be expanded to its string representation - /** @psalm-suppress MixedArgument */ - $variables = ($variables)($this->name->toString(), $variableToExpand); + $variables = ($variables)($this->name->toString(), $variableToExpand); - return $this->expression->expand($variables); - }) - ->toSequenceOf('string'); + return $this->expression->expand($variables); + }); - return join(',', $expanded) + return Str::of(',') + ->join($expanded) ->prepend("?{$this->name->toString()}=") ->toString(); } /** - * @param Map $variables - * @param array $variablesToExpand + * @param Map|list> $variables + * @param list|list $variablesToExpand */ private function explodeList(Map $variables, array $variablesToExpand): string { - $expanded = Sequence::of('scalar|array', ...$variablesToExpand) - ->map(function($variableToExpand) use ($variables): string { - $name = $this->name; + $expanded = Sequence::of(...$variablesToExpand)->map(function($variableToExpand) use ($variables): string { + $name = $this->name; - if (\is_array($variableToExpand)) { - /** - * @var string $name - * @var scalar $value - */ - [$name, $value] = $variableToExpand; - $name = new Name($name); - $variableToExpand = $value; - } + if (\is_array($variableToExpand)) { + [$name, $value] = $variableToExpand; + $name = Name::of($name); + $variableToExpand = $value; + } - /** @psalm-suppress MixedArgument */ - $variables = ($variables)($name->toString(), $variableToExpand); + $variables = ($variables)($name->toString(), $variableToExpand); - $value = (new Level3\Query($name))->expand($variables); + $value = Level3\Query::named($name)->expand($variables); - // the substring is here to remove the '?' as it should be a '&' - // done below in the join - return Str::of($value)->substring(1)->toString(); - }) - ->toSequenceOf('string'); + // the substring is here to remove the '?' as it should be a '&' + // done below in the join + return Str::of($value)->drop(1)->toString(); + }); - return join('&', $expanded) + return Str::of('&') + ->join($expanded) ->prepend('?') ->toString(); } diff --git a/src/Expression/Level4/QueryContinuation.php b/src/Expression/Level4/QueryContinuation.php index a2b05bb..c73fb12 100644 --- a/src/Expression/Level4/QueryContinuation.php +++ b/src/Expression/Level4/QueryContinuation.php @@ -6,73 +6,67 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level1, Expression\Level3, Expression\Level4, Exception\DomainException, Exception\ExplodeExpressionCantBeMatched, - Exception\ExpressionLimitCantBeNegative, }; use Innmind\Immutable\{ Map, Str, Sequence, -}; -use function Innmind\Immutable\{ - join, - unwrap, + Maybe, }; +/** + * @psalm-immutable + */ final class QueryContinuation implements Expression { private Name $name; + /** @var ?positive-int */ private ?int $limit = null; private bool $explode = false; private Expression $expression; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name $name) + private function __construct(Name $name) { $this->name = $name; - $this->expression = new Level1($name); + $this->expression = Level1::named($name); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if ($string->matches('~^\{\&[a-zA-Z0-9_]+\}$~')) { - return new self(new Name($string->trim('{&}')->toString())); - } - - if ($string->matches('~^\{\&[a-zA-Z0-9_]+\*\}$~')) { - return self::explode(new Name($string->trim('{&*}')->toString())); - } - - if ($string->matches('~^\{\&[a-zA-Z0-9_]+:\d+\}$~')) { - $string = $string->trim('{&}'); - [$name, $limit] = unwrap($string->split(':')); - - return self::limit( - new Name($name->toString()), - (int) $limit->toString(), - ); - } - - throw new DomainException($string->toString()); + return Parse::of( + $string, + static fn(Name $name) => new self($name), + self::explode(...), + self::limit(...), + Expansion::queryContinuation, + ); } + /** + * @psalm-pure + * + * @param positive-int $limit + */ public static function limit(Name $name, int $limit): self { - if ($limit < 0) { - throw new ExpressionLimitCantBeNegative($limit); - } - $self = new self($name); $self->limit = $limit; return $self; } + /** + * @psalm-pure + */ public static function explode(Name $name): self { $self = new self($name); @@ -81,39 +75,34 @@ public static function explode(Name $name): self return $self; } - public function add(Str $pattern): Composite + public function expansion(): Expansion { - return new Composite( - '', - $this, - self::of($pattern->prepend('{&')->append('}')), - ); + return Expansion::queryContinuation; } - /** - * @param Map $variables - */ public function expand(Map $variables): string { - if (!$variables->contains($this->name->toString())) { + $variable = $variables->get($this->name->toString())->match( + static fn($variable) => $variable, + static fn() => null, + ); + + if (\is_null($variable)) { return ''; } - /** @var scalar|array{0:string, 1:scalar} */ - $variable = $variables->get($this->name->toString()); - if (\is_array($variable)) { - return $this->expandList($variables, ...$variable); + return $this->expandList($variables, $variable); } if ($this->explode) { - return $this->expandList($variables, $variable); + return $this->explodeList($variables, [$variable]); } $value = Str::of($this->expression->expand($variables)); if ($this->mustLimit()) { - return "&{$this->name->toString()}={$value->substring(0, $this->limit)->toString()}"; + return "&{$this->name->toString()}={$value->take($this->limit)->toString()}"; } return "&{$this->name->toString()}={$value->toString()}"; @@ -121,10 +110,6 @@ public function expand(Map $variables): string public function regex(): string { - if (\is_string($this->regex)) { - return $this->regex; - } - if ($this->explode) { throw new ExplodeExpressionCantBeMatched; } @@ -132,14 +117,14 @@ public function regex(): string if ($this->mustLimit()) { // replace '*' match by the actual limit $regex = Str::of($this->expression->regex()) - ->substring(0, -2) + ->dropEnd(2) ->append("{{$this->limit}})") ->toString(); } else { $regex = $this->expression->regex(); } - return $this->regex = \sprintf( + return \sprintf( '\&%s=%s', $this->name->toString(), $regex, @@ -148,92 +133,80 @@ public function regex(): string public function toString(): string { - if (\is_string($this->string)) { - return $this->string; - } - if ($this->mustLimit()) { - return $this->string = "{&{$this->name->toString()}:{$this->limit}}"; + return "{&{$this->name->toString()}:{$this->limit}}"; } if ($this->explode) { - return $this->string = "{&{$this->name->toString()}*}"; + return "{&{$this->name->toString()}*}"; } - return $this->string = "{&{$this->name->toString()}}"; + return "{&{$this->name->toString()}}"; } + /** + * @psalm-assert-if-true positive-int $this->limit + */ private function mustLimit(): bool { return \is_int($this->limit); } /** - * @param Map $variables - * @param array $variablesToExpand + * @param Map|list> $variables + * @param list|list $variablesToExpand */ - private function expandList(Map $variables, ...$variablesToExpand): string + private function expandList(Map $variables, array $variablesToExpand): string { if ($this->explode) { return $this->explodeList($variables, $variablesToExpand); } - /** @var Sequence */ - $flattenedVariables = Sequence::of('scalar|array', ...$variablesToExpand)->reduce( - Sequence::of('scalar'), - static function(Sequence $values, $variableToExpand): Sequence { + $flattenedVariables = Sequence::of(...$variablesToExpand)->flatMap( + static function($variableToExpand): Sequence { if (\is_array($variableToExpand)) { [$name, $variableToExpand] = $variableToExpand; - return ($values)($name)($variableToExpand); + return Sequence::of($name, $variableToExpand); } - return ($values)($variableToExpand); + return Sequence::of($variableToExpand); }, ); - $expanded = $flattenedVariables - ->map(function($variableToExpand) use ($variables): string { - // here we use the level1 expression to transform the variable to - // be expanded to its string representation - /** @psalm-suppress MixedArgument */ - $variables = ($variables)($this->name->toString(), $variableToExpand); - - return $this->expression->expand($variables); - }) - ->toSequenceOf('string'); - - return join(',', $expanded) + $expanded = $flattenedVariables->map(function($variableToExpand) use ($variables): string { + // here we use the level1 expression to transform the variable to + // be expanded to its string representation + $variables = ($variables)($this->name->toString(), $variableToExpand); + + return $this->expression->expand($variables); + }); + + return Str::of(',') + ->join($expanded) ->prepend("&{$this->name->toString()}=") ->toString(); } /** - * @param Map $variables - * @param array $variablesToExpand + * @param Map|list> $variables + * @param list|list $variablesToExpand */ private function explodeList(Map $variables, array $variablesToExpand): string { - $expanded = Sequence::of('scalar|array', ...$variablesToExpand) - ->map(function($variableToExpand) use ($variables): string { - $name = $this->name; + $expanded = Sequence::of(...$variablesToExpand)->map(function($variableToExpand) use ($variables): string { + $name = $this->name; - if (\is_array($variableToExpand)) { - /** - * @var string $name - * @var scalar $value - */ - [$name, $value] = $variableToExpand; - $name = new Name($name); - $variableToExpand = $value; - } + if (\is_array($variableToExpand)) { + [$name, $value] = $variableToExpand; + $name = Name::of($name); + $variableToExpand = $value; + } - /** @psalm-suppress MixedArgument */ - $variables = ($variables)($name->toString(), $variableToExpand); + $variables = ($variables)($name->toString(), $variableToExpand); - return (new Level3\QueryContinuation($name))->expand($variables); - }) - ->toSequenceOf('string'); + return Level3\QueryContinuation::named($name)->expand($variables); + }); - return join('', $expanded)->toString(); + return Str::of('')->join($expanded)->toString(); } } diff --git a/src/Expression/Level4/Reserved.php b/src/Expression/Level4/Reserved.php index b74d033..70c1142 100644 --- a/src/Expression/Level4/Reserved.php +++ b/src/Expression/Level4/Reserved.php @@ -6,91 +6,84 @@ use Innmind\UrlTemplate\{ Expression, Expression\Name, + Expression\Expansion, Expression\Level2, Expression\Level4, Exception\DomainException, Exception\ExplodeExpressionCantBeMatched, - Exception\ExpressionLimitCantBeNegative, }; use Innmind\Immutable\{ Map, Str, + Maybe, }; -use function Innmind\Immutable\unwrap; +/** + * @psalm-immutable + */ final class Reserved implements Expression { private Name $name; + /** @var ?positive-int */ private ?int $limit = null; private bool $explode = false; private Expression $expression; - private ?string $regex = null; - private ?string $string = null; - public function __construct(Name $name) + private function __construct(Name $name) { $this->name = $name; - $this->expression = (new Level4($name))->withExpression( - Level2\Reserved::class, + $this->expression = Level4::named($name)->withExpression( + Level2\Reserved::named(...), ); } - public static function of(Str $string): Expression + /** + * @psalm-pure + */ + public static function of(Str $string): Maybe { - if ($string->matches('~^\{\+[a-zA-Z0-9_]+\}$~')) { - return new self(new Name($string->trim('{+}')->toString())); - } - - if ($string->matches('~^\{\+[a-zA-Z0-9_]+\*\}$~')) { - return self::explode(new Name($string->trim('{+*}')->toString())); - } - - if ($string->matches('~^\{\+[a-zA-Z0-9_]+:\d+\}$~')) { - $string = $string->trim('{+}'); - [$name, $limit] = unwrap($string->split(':')); - - return self::limit( - new Name($name->toString()), - (int) $limit->toString(), - ); - } - - throw new DomainException($string->toString()); + return Parse::of( + $string, + static fn(Name $name) => new self($name), + self::explode(...), + self::limit(...), + Expansion::reserved, + ); } + /** + * @psalm-pure + * + * @param positive-int $limit + */ public static function limit(Name $name, int $limit): self { - if ($limit < 0) { - throw new ExpressionLimitCantBeNegative($limit); - } - $self = new self($name); $self->limit = $limit; $self->expression = Level4::limit($name, $limit)->withExpression( - Level2\Reserved::class, + Level2\Reserved::named(...), ); return $self; } + /** + * @psalm-pure + */ public static function explode(Name $name): self { $self = new self($name); $self->explode = true; $self->expression = Level4::explode($name)->withExpression( - Level2\Reserved::class, + Level2\Reserved::named(...), ); return $self; } - public function add(Str $pattern): Composite + public function expansion(): Expansion { - return new Composite( - ',', - $this, - self::of($pattern->prepend('{+')->append('}')), - ); + return Expansion::reserved; } public function expand(Map $variables): string @@ -100,35 +93,27 @@ public function expand(Map $variables): string public function regex(): string { - if (\is_string($this->regex)) { - return $this->regex; - } - if ($this->explode) { throw new ExplodeExpressionCantBeMatched; } if (\is_int($this->limit)) { - return $this->regex = "(?<{$this->name->toString()}>[a-zA-Z0-9\%:/\?#\[\]@!\$&'\(\)\*\+,;=\-\.\_\~]{{$this->limit}})"; + return "(?<{$this->name->toString()}>[a-zA-Z0-9\%:/\?#\[\]@!\$&'\(\)\*\+,;=\-\.\_\~]{{$this->limit}})"; } - return $this->regex = $this->expression->regex(); + return $this->expression->regex(); } public function toString(): string { - if (\is_string($this->string)) { - return $this->string; - } - if (\is_int($this->limit)) { - return $this->string = "{+{$this->name->toString()}:{$this->limit}}"; + return "{+{$this->name->toString()}:{$this->limit}}"; } if ($this->explode) { - return $this->string = "{+{$this->name->toString()}*}"; + return "{+{$this->name->toString()}*}"; } - return $this->string = "{+{$this->name->toString()}}"; + return "{+{$this->name->toString()}}"; } } diff --git a/src/Expression/Name.php b/src/Expression/Name.php index cc906ff..289dda4 100644 --- a/src/Expression/Name.php +++ b/src/Expression/Name.php @@ -4,21 +4,140 @@ namespace Innmind\UrlTemplate\Expression; use Innmind\UrlTemplate\Exception\DomainException; -use Innmind\Immutable\Str; +use Innmind\Immutable\{ + Str, + Maybe, + Sequence, +}; +/** + * @psalm-immutable + * @internal + */ final class Name { + /** @var non-empty-string */ private string $value; - public function __construct(string $value) + /** + * @param non-empty-string $value + */ + private function __construct(string $value) { - if (!Str::of($value)->matches('~[a-zA-Z0-9_]+~')) { + $this->value = $value; + } + + /** + * @psalm-pure + */ + public static function of(string $value): self + { + $characters = self::characters(); + + if (!Str::of($value)->matches("~^{$characters}\$~")) { throw new DomainException($value); } - $this->value = $value; + /** @psalm-suppress ArgumentTypeCoercion Because of the non-empty-string */ + return new self($value); + } + + /** + * @psalm-pure + * + * @return Maybe + */ + public static function one( + Str $value, + Expansion $expansion, + ): Maybe { + /** @psalm-suppress ArgumentTypeCoercion Because of the non-empty-string */ + return Maybe::just($value) + ->filter($expansion->matches(...)) + ->map($expansion->clean(...)) + ->map(static fn($value) => $value->toString()) + ->map(static fn($value) => new self($value)); + } + + /** + * @psalm-pure + * + * @return Maybe + */ + public static function explode( + Str $value, + Expansion $expansion, + ): Maybe { + /** @psalm-suppress ArgumentTypeCoercion Because of the non-empty-string */ + return Maybe::just($value) + ->filter($expansion->matchesExplode(...)) + ->map($expansion->cleanExplode(...)) + ->map(static fn($value) => $value->toString()) + ->map(static fn($value) => new self($value)); + } + + /** + * @psalm-pure + * + * @return Maybe + */ + public static function limit( + Str $value, + Expansion $expansion, + ): Maybe { + /** @psalm-suppress ArgumentTypeCoercion Because of the non-empty-string */ + return Maybe::just($value) + ->filter($expansion->matchesLimit(...)) + ->map($expansion->clean(...)) + ->map(static fn($value) => $value->split(':')) + ->map(static fn($pieces) => $pieces->map(static fn($piece) => $piece->toString())) + ->flatMap( + static fn($pieces) => $pieces + ->first() + ->map(static fn($value) => new self($value)) + ->flatMap( + static fn($name) => $pieces + ->last() + ->filter(\is_numeric(...)) + ->map(static fn($limit) => (int) $limit) + ->filter(static fn(int $limit) => $limit > 0) + ->map(static fn($int) => [$name, $int]), + ), + ); + } + + /** + * @psalm-pure + * + * @return Maybe> + */ + public static function many( + Str $value, + Expansion $expansion, + ): Maybe { + /** @psalm-suppress ArgumentTypeCoercion Because of the non-empty-string */ + return Maybe::just($value) + ->filter($expansion->matchesMany(...)) + ->map($expansion->clean(...)) + ->map( + static fn($value) => $value + ->split(',') + ->map(static fn($value) => $value->toString()) + ->map(static fn($value) => new self($value)), + ); + } + + /** + * @psalm-pure + */ + public static function characters(): string + { + return '[a-zA-Z0-9_]+'; } + /** + * @return non-empty-string + */ public function toString(): string { return $this->value; diff --git a/src/Expressions.php b/src/Expressions.php index 13f556d..59c186f 100644 --- a/src/Expressions.php +++ b/src/Expressions.php @@ -3,54 +3,63 @@ namespace Innmind\UrlTemplate; -use Innmind\UrlTemplate\Exception\DomainException; use Innmind\Immutable\{ Sequence, Str, + Maybe, }; +/** + * @psalm-immutable + * @internal + */ final class Expressions { - /** @var list */ - private static ?array $expressions = null; - - public static function of(Str $string): Expression + /** + * @psalm-pure + * + * @return Maybe + */ + public static function of(Str $string): Maybe { - foreach (self::expressions() as $expression) { - try { - /** @var Expression */ - return [$expression, 'of']($string); - } catch (DomainException $e) { - //pass - } - } - - throw new DomainException($string->toString()); + /** + * @psalm-suppress MixedReturnTypeCoercion + * @var Maybe + */ + return self::expressions()->reduce( + Maybe::nothing(), + static fn(Maybe $expression, $attempt) => $expression->otherwise( + static fn() => $attempt($string), + ), + ); } /** - * @return list + * @psalm-pure + * + * @return Sequence> */ - private static function expressions(): array + private static function expressions(): Sequence { - return self::$expressions ?? self::$expressions = [ - Expression\Level4::class, - Expression\Level4\Reserved::class, - Expression\Level4\Fragment::class, - Expression\Level4\Label::class, - Expression\Level4\Path::class, - Expression\Level4\Parameters::class, - Expression\Level4\Query::class, - Expression\Level4\QueryContinuation::class, - Expression\Level3::class, - Expression\Level3\Reserved::class, - Expression\Level3\Fragment::class, - Expression\Level3\Label::class, - Expression\Level3\Path::class, - Expression\Level3\Parameters::class, - Expression\Level3\Query::class, - Expression\Level3\QueryContinuation::class, - Expression\Level4\Composite::class, - ]; + /** @var Sequence> */ + return Sequence::of( + Expression\Level4::of(...), + Expression\Level4\Reserved::of(...), + Expression\Level4\Fragment::of(...), + Expression\Level4\Label::of(...), + Expression\Level4\Path::of(...), + Expression\Level4\Parameters::of(...), + Expression\Level4\Query::of(...), + Expression\Level4\QueryContinuation::of(...), + Expression\Level3::of(...), + Expression\Level3\Reserved::of(...), + Expression\Level3\Fragment::of(...), + Expression\Level3\Label::of(...), + Expression\Level3\Path::of(...), + Expression\Level3\Parameters::of(...), + Expression\Level3\Query::of(...), + Expression\Level3\QueryContinuation::of(...), + Expression\Level4\Composite::of(...), + ); } } diff --git a/src/Template.php b/src/Template.php index 7e41fd0..4bbe08f 100644 --- a/src/Template.php +++ b/src/Template.php @@ -4,50 +4,69 @@ namespace Innmind\UrlTemplate; use Innmind\UrlTemplate\Exception\{ - UrlDoesntMatchTemplate, - ExtractionNotSupported, - LogicException, + ExplodeExpressionCantBeMatched, + DomainException, }; use Innmind\Url\Url; use Innmind\Immutable\{ Map, Sequence, Str, + Maybe, }; -use function Innmind\Immutable\assertMap; +/** + * @psalm-immutable + */ final class Template { private Str $template; /** @var Sequence */ private Sequence $expressions; - private function __construct(string $template) + /** + * @param Sequence $expressions + */ + private function __construct(Str $template, Sequence $expressions) { - $this->template = Str::of($template); - $this->expressions = $this - ->extractExpressions( - Sequence::of('string'), - $this->template, - ) - ->mapTo( - Expression::class, - static fn(string $expression) => Expressions::of(Str::of($expression)), - ); + $this->template = $template; + $this->expressions = $expressions; } + /** + * @psalm-pure + * + * @param literal-string $template + * + * @throws DomainException + */ public static function of(string $template): self { - return new self($template); + return self::maybe($template)->match( + static fn($self) => $self, + static fn() => throw new DomainException($template), + ); } /** - * @param Map $variables + * @psalm-pure + * + * @return Maybe */ - public function expand(Map $variables): Url + public static function maybe(string $template): Maybe { - assertMap('string', 'scalar|array', $variables, 1); + $template = Str::of($template); + return self::parse($template)->map( + static fn($expressions) => new self($template, $expressions), + ); + } + + /** + * @param Map|list> $variables + */ + public function expand(Map $variables): Url + { $url = $this->expressions->reduce( $this->template, static function(Str $template, Expression $expression) use ($variables): Str { @@ -55,47 +74,32 @@ static function(Str $template, Expression $expression) use ($variables): Str { $expression->toString(), $expression->expand($variables), ); - } + }, ); return Url::of($url->toString()); } /** + * @throws ExplodeExpressionCantBeMatched + * * @return Map */ public function extract(Url $url): Map { - $regex = $this->regex(); - $url = Str::of($url->toString()); - - if (!$url->matches($regex)) { - throw new UrlDoesntMatchTemplate($url->toString()); - } - /** @var Map */ - return $url - ->capture($regex) - ->filter(static function($key): bool { - return \is_string($key); - }) - ->reduce( - Map::of('string', 'string'), - static function(Map $variables, $name, Str $variable): Map { - return ($variables)( - (string) $name, - \rawurldecode($variable->toString()), - ); - }, - ); + return Str::of($url->toString()) + ->capture($this->regex()) + ->filter(static fn($key) => \is_string($key)) + ->map(static fn($_, $variable) => \rawurldecode($variable->toString())); } + /** + * @throws ExplodeExpressionCantBeMatched + */ public function matches(Url $url): bool { - $regex = $this->regex(); - $url = Str::of($url->toString()); - - return $url->matches($regex); + return Str::of($url->toString())->matches($this->regex()); } public function toString(): string @@ -104,70 +108,72 @@ public function toString(): string } /** - * Recursively find the expressions as Str::capture doesnt capture all of - * them at the same time - * @param Sequence $expressions - * - * @return Sequence + * @throws ExplodeExpressionCantBeMatched */ - private function extractExpressions( - Sequence $expressions, - Str $template - ): Sequence { - $captured = $template->capture('~(\{[\+#\./;\?&]?[a-zA-Z0-9_]+(\*|:\d+)?(,[a-zA-Z0-9_]+(\*|:\d+)?)*\})~'); - - if ($captured->size() === 0) { - return $expressions; - } - - return $this->extractExpressions( - $expressions->add($captured->values()->first()->toString()), - $template->replace($captured->values()->first()->toString(), ''), + private function regex(): string + { + $template = $this + ->expressions + ->reduce( + $this->template->replace('~', '\~'), + static fn(Str $template, $expression) => $template->replace( + $expression->toString(), + \sprintf( + '__innmind_expression_%s__', + \spl_object_hash($expression), + ), + ), + ) + ->pregQuote(); + $template = $this->expressions->reduce( + $template, + static fn(Str $template, $expression) => $template->replace( + \sprintf( + '__innmind_expression_%s__', + \spl_object_hash($expression), + ), + $expression->regex(), + ), ); + + return $template->prepend('~^')->append('$~')->toString(); } - private function regex(): string + /** + * @psalm-pure + * + * Recursively find the expressions as Str::capture doesnt capture all of + * them at the same time + * + * @return Maybe> + */ + private static function parse(Str $template): Maybe { - try { - $i = 0; - $j = 0; - $template = $this - ->expressions - ->reduce( - $this->template->replace('~', '\~'), - static function(Str $template, Expression $expression) use (&$i): Str { - /** - * @psalm-suppress MixedOperand - * @psalm-suppress MixedAssignment - */ - ++$i; - - return $template->replace( - $expression->toString(), - "__innmind_expression_{$i}__", - ); - }, - ) - ->pregQuote(); - $template = $this->expressions->reduce( - $template, - static function(Str $template, Expression $expression) use (&$j): Str { - /** - * @psalm-suppress MixedOperand - * @psalm-suppress MixedAssignment - */ - ++$j; - - return $template->replace( - "__innmind_expression_{$j}__", - $expression->regex(), - ); - }, + /** @var Sequence */ + $expressions = Sequence::of(); + + do { + $captured = $template->capture('~(\{[\+#\./;\?&]?[a-zA-Z0-9_]+(\*|:\d+)?(,[a-zA-Z0-9_]+(\*|:\d+)?)*\})~'); + + [$expressions, $template] = $captured + ->get(0) + ->match( + static fn($value) => [ + ($expressions)($value), + $template->replace($value->toString(), ''), + ], + static fn() => [$expressions, $template], + ); + } while (!$captured->empty()); + + /** @var Maybe> */ + return $expressions + ->map(Expressions::of(...)) + ->match( + static fn($first, $rest) => Maybe::all($first, ...$rest->toList())->map( + Sequence::of(...), + ), + static fn() => Maybe::just(Sequence::of()), ); - } catch (LogicException $e) { - throw new ExtractionNotSupported('', 0, $e); - } - - return $template->prepend('~^')->append('$~')->toString(); } } diff --git a/src/UrlEncode.php b/src/UrlEncode.php index b75df39..3ed78f7 100644 --- a/src/UrlEncode.php +++ b/src/UrlEncode.php @@ -3,16 +3,24 @@ namespace Innmind\UrlTemplate; -use Innmind\Immutable\Str; -use function Innmind\Immutable\join; +use Innmind\Immutable\{ + Str, + Sequence, + Monoid\Concat, +}; +/** + * @psalm-immutable + * @internal + */ final class UrlEncode { - private Str $safeCharacters; + /** @var Sequence */ + private Sequence $safeCharacters; public function __construct() { - $this->safeCharacters = Str::of(''); + $this->safeCharacters = Sequence::strings(); } public function __invoke(string $string): string @@ -21,38 +29,50 @@ public function __invoke(string $string): string return \rawurlencode($string); } - $string = Str::of($string); - - if ($string->length() > 1) { - $characters = $string - ->split() - ->map(function(Str $character): Str { - return Str::of($this($character->toString())); - }) - ->mapTo( - 'string', - static fn(Str $character): string => $character->toString(), - ); - - return join('', $characters)->toString(); - } - - if ($string->empty()) { - return ''; - } - - if ($this->safeCharacters->contains($string->toString())) { - return $string->toString(); - } - - return \rawurlencode($string->toString()); + return Str::of($string) + ->split() + ->map(static fn($char) => $char->toString()) + ->map($this->encode(...)) + ->map(Str::of(...)) + ->fold(new Concat) + ->toString(); } + /** + * @psalm-pure + */ public static function allowReservedCharacters(): self { $self = new self; - $self->safeCharacters = Str::of(':/?#[]@!$&\'()*+,;='); + $self->safeCharacters = Sequence::strings( + ':', + '/', + '?', + '#', + '[', + ']', + '@', + '!', + '$', + '&', + "'", + '(', + ')', + '*', + '+', + ',', + ';', + '=', + ); return $self; } + + private function encode(string $char): string + { + return match ($this->safeCharacters->contains($char)) { + true => $char, + false => \rawurlencode($char), + }; + } } diff --git a/tests/Expression/Level1Test.php b/tests/Expression/Level1Test.php index 772d639..25cf00d 100644 --- a/tests/Expression/Level1Test.php +++ b/tests/Expression/Level1Test.php @@ -5,10 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level1, - Expression\Name, Expression, - Exception\DomainException, - Exception\OnlyScalarCanBeExpandedForExpression, }; use Innmind\Immutable\{ Map, @@ -22,27 +19,39 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Level1(new Name('foo')) + Level1::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{foo}', (new Level1(new Name('foo')))->toString()); + $this->assertSame( + '{foo}', + Level1::of(Str::of('{foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } public function testExpand() { - $expression = new Level1(new Name('foo')); + $expression = Level1::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ); $this->assertSame('value', $expression->expand( - Map::of('string', 'variable')('foo', 'value') + Map::of(['foo', 'value']), )); $this->assertSame('Hello%20World%21', $expression->expand( - Map::of('string', 'variable')('foo', 'Hello World!') + Map::of(['foo', 'Hello World!']), )); $this->assertSame('', $expression->expand( - Map::of('string', 'variable') + Map::of(), )); } @@ -50,36 +59,42 @@ public function testOf() { $this->assertInstanceOf( Level1::class, - $expression = Level1::of(Str::of('{foo}')) + $expression = Level1::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{foo}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('foo'); - - Level1::of(Str::of('foo')); + $this->assertNull(Level1::of(Str::of('foo'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '(?[a-zA-Z0-9\%\-\.\_\~]*)', - Level1::of(Str::of('{foo}'))->regex() + Level1::of(Str::of('{foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } - public function testThrowWhenTryingToExpandWithAnArray() + public function testReturnEmptyStringWhenTryingToExpandWithAnArray() { - $expression = new Level1(new Name('foo')); - - $this->expectException(OnlyScalarCanBeExpandedForExpression::class); - $this->expectExceptionMessage('foo'); - - $expression->expand( - Map::of('string', 'variable')('foo', ['value']) + $expression = Level1::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, ); + + $this->assertSame('', $expression->expand( + Map::of(['foo', ['value']]), + )); } } diff --git a/tests/Expression/Level2/FragmentTest.php b/tests/Expression/Level2/FragmentTest.php index 4dd427b..cd89ebe 100644 --- a/tests/Expression/Level2/FragmentTest.php +++ b/tests/Expression/Level2/FragmentTest.php @@ -5,10 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level2\Fragment, - Expression\Name, Expression, - Exception\DomainException, - Exception\OnlyScalarCanBeExpandedForExpression, }; use Innmind\Immutable\{ Map, @@ -22,30 +19,42 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Fragment(new Name('foo')) + Fragment::of(Str::of('{#foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{#foo}', (new Fragment(new Name('foo')))->toString()); + $this->assertSame( + '{#foo}', + Fragment::of(Str::of('{#foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } public function testExpand() { - $expression = new Fragment(new Name('foo')); + $expression = Fragment::of(Str::of('{#foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ); $this->assertSame('#value', $expression->expand( - Map::of('string', 'variable')('foo', 'value') + Map::of(['foo', 'value']), )); $this->assertSame('#Hello%20World!', $expression->expand( - Map::of('string', 'variable')('foo', 'Hello World!') + Map::of(['foo', 'Hello World!']), )); $this->assertSame('#/foo/bar', $expression->expand( - Map::of('string', 'variable')('foo', '/foo/bar') + Map::of(['foo', '/foo/bar']), )); $this->assertSame('', $expression->expand( - Map::of('string', 'variable') + Map::of(), )); } @@ -53,36 +62,42 @@ public function testOf() { $this->assertInstanceOf( Fragment::class, - $expression = Fragment::of(Str::of('{#foo}')) + $expression = Fragment::of(Str::of('{#foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{#foo}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('foo'); - - Fragment::of(Str::of('foo')); + $this->assertNull(Fragment::of(Str::of('foo'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '\#(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]*)', - Fragment::of(Str::of('{#foo}'))->regex() + Fragment::of(Str::of('{#foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } - public function testThrowWhenTryingToExpandWithAnArray() + public function testReturnEmptyStringWhenTryingToExpandWithAnArray() { - $expression = new Fragment(new Name('foo')); - - $this->expectException(OnlyScalarCanBeExpandedForExpression::class); - $this->expectExceptionMessage('foo'); - - $expression->expand( - Map::of('string', 'variable')('foo', ['value']) + $expression = Fragment::of(Str::of('{#foo}'))->match( + static fn($expression) => $expression, + static fn() => null, ); + + $this->assertSame('', $expression->expand( + Map::of(['foo', ['value']]), + )); } } diff --git a/tests/Expression/Level2/ReservedTest.php b/tests/Expression/Level2/ReservedTest.php index 2adaae5..1b805f2 100644 --- a/tests/Expression/Level2/ReservedTest.php +++ b/tests/Expression/Level2/ReservedTest.php @@ -5,10 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level2\Reserved, - Expression\Name, Expression, - Exception\DomainException, - Exception\OnlyScalarCanBeExpandedForExpression, }; use Innmind\Immutable\{ Map, @@ -22,30 +19,42 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Reserved(new Name('foo')) + Reserved::of(Str::of('{+foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{+foo}', (new Reserved(new Name('foo')))->toString()); + $this->assertSame( + '{+foo}', + Reserved::of(Str::of('{+foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } public function testExpand() { - $expression = new Reserved(new Name('foo')); + $expression = Reserved::of(Str::of('{+foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ); $this->assertSame('value', $expression->expand( - Map::of('string', 'variable')('foo', 'value') + Map::of(['foo', 'value']), )); $this->assertSame('Hello%20World!', $expression->expand( - Map::of('string', 'variable')('foo', 'Hello World!') + Map::of(['foo', 'Hello World!']), )); $this->assertSame('/foo/bar', $expression->expand( - Map::of('string', 'variable')('foo', '/foo/bar') + Map::of(['foo', '/foo/bar']), )); $this->assertSame('', $expression->expand( - Map::of('string', 'variable') + Map::of(), )); } @@ -53,36 +62,42 @@ public function testOf() { $this->assertInstanceOf( Reserved::class, - $expression = Reserved::of(Str::of('{+foo}')) + $expression = Reserved::of(Str::of('{+foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{+foo}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('foo'); - - Reserved::of(Str::of('foo')); + $this->assertNull(Reserved::of(Str::of('foo'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]*)', - Reserved::of(Str::of('{+foo}'))->regex() + Reserved::of(Str::of('{+foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } - public function testThrowWhenTryingToExpandWithAnArray() + public function testReturnEmptyStringWhenTryingToExpandWithAnArray() { - $expression = new Reserved(new Name('foo')); - - $this->expectException(OnlyScalarCanBeExpandedForExpression::class); - $this->expectExceptionMessage('foo'); - - $expression->expand( - Map::of('string', 'variable')('foo', ['value']) + $expression = Reserved::of(Str::of('{+foo}'))->match( + static fn($expression) => $expression, + static fn() => null, ); + + $this->assertSame('', $expression->expand( + Map::of(['foo', ['value']]), + )); } } diff --git a/tests/Expression/Level3/FragmentTest.php b/tests/Expression/Level3/FragmentTest.php index 91a1a94..caa9ec8 100644 --- a/tests/Expression/Level3/FragmentTest.php +++ b/tests/Expression/Level3/FragmentTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level3\Fragment, - Expression\Name, Expression, - Exception\DomainException, }; use Innmind\Immutable\{ Map, @@ -21,7 +19,10 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Fragment(new Name('foo'), new Name('bar')) + Fragment::of(Str::of('{#foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } @@ -29,13 +30,16 @@ public function testStringCast() { $this->assertSame( '{#foo,bar}', - (new Fragment(new Name('foo'), new Name('bar')))->toString(), + Fragment::of(Str::of('{#foo,bar}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('empty', '') @@ -45,11 +49,17 @@ public function testExpand() $this->assertSame( '#1024,Hello%20World!,768', - (new Fragment(new Name('x'), new Name('hello'), new Name('y')))->expand($variables) + Fragment::of(Str::of('{#x,hello,y}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '#/foo/bar,1024', - (new Fragment(new Name('path'), new Name('x')))->expand($variables) + Fragment::of(Str::of('{#path,x}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -57,24 +67,30 @@ public function testOf() { $this->assertInstanceOf( Fragment::class, - $expression = Fragment::of(Str::of('{#foo,bar}')) + $expression = Fragment::of(Str::of('{#foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{#foo,bar}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{#foo}'); - - Fragment::of(Str::of('{#foo}')); + $this->assertNull(Fragment::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '\#(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]*),(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]*)', - Fragment::of(Str::of('{#foo,bar}'))->regex() + Fragment::of(Str::of('{#foo,bar}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level3/LabelTest.php b/tests/Expression/Level3/LabelTest.php index ad1b4f1..4379970 100644 --- a/tests/Expression/Level3/LabelTest.php +++ b/tests/Expression/Level3/LabelTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level3\Label, - Expression\Name, Expression, - Exception\DomainException, }; use Innmind\Immutable\{ Map, @@ -21,7 +19,10 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Label(new Name('foo'), new Name('bar')) + Label::of(Str::of('{.foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } @@ -29,13 +30,16 @@ public function testStringCast() { $this->assertSame( '{.foo,bar}', - (new Label(new Name('foo'), new Name('bar')))->toString(), + Label::of(Str::of('{.foo,bar}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('empty', '') @@ -45,11 +49,17 @@ public function testExpand() $this->assertSame( '.1024.768', - (new Label(new Name('x'), new Name('y')))->expand($variables) + Label::of(Str::of('{.x,y}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '.value', - (new Label(new Name('var')))->expand($variables) + Label::of(Str::of('{.var}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -57,24 +67,30 @@ public function testOf() { $this->assertInstanceOf( Label::class, - $expression = Label::of(Str::of('{.foo,bar}')) + $expression = Label::of(Str::of('{.foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{.foo,bar}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{.foo}'); - - Label::of(Str::of('{.foo}')); + $this->assertNull(Label::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '\.(?[a-zA-Z0-9\%\-\_\~]*).(?[a-zA-Z0-9\%\-\_\~]*)', - Label::of(Str::of('{.foo,bar}'))->regex() + Label::of(Str::of('{.foo,bar}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level3/ParametersTest.php b/tests/Expression/Level3/ParametersTest.php index 1c8db48..2d8f9d0 100644 --- a/tests/Expression/Level3/ParametersTest.php +++ b/tests/Expression/Level3/ParametersTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level3\Parameters, - Expression\Name, Expression, - Exception\DomainException, }; use Innmind\Immutable\{ Map, @@ -21,7 +19,10 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Parameters(new Name('foo'), new Name('bar')) + Parameters::of(Str::of('{;foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } @@ -29,13 +30,16 @@ public function testStringCast() { $this->assertSame( '{;foo,bar}', - (new Parameters(new Name('foo'), new Name('bar')))->toString(), + Parameters::of(Str::of('{;foo,bar}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('empty', '') @@ -45,11 +49,17 @@ public function testExpand() $this->assertSame( ';x=1024;y=768', - (new Parameters(new Name('x'), new Name('y')))->expand($variables) + Parameters::of(Str::of('{;x,y}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( ';x=1024;y=768;empty', - (new Parameters(new Name('x'), new Name('y'), new Name('empty')))->expand($variables) + Parameters::of(Str::of('{;x,y,empty}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -57,24 +67,30 @@ public function testOf() { $this->assertInstanceOf( Parameters::class, - $expression = Parameters::of(Str::of('{;foo,bar}')) + $expression = Parameters::of(Str::of('{;foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{;foo,bar}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{;foo}'); - - Parameters::of(Str::of('{;foo}')); + $this->assertNull(Parameters::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '\;foo=?(?[a-zA-Z0-9\%\-\.\_\~]*)\;bar=?(?[a-zA-Z0-9\%\-\.\_\~]*)', - Parameters::of(Str::of('{;foo,bar}'))->regex() + Parameters::of(Str::of('{;foo,bar}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level3/PathTest.php b/tests/Expression/Level3/PathTest.php index d07f3c5..71bae63 100644 --- a/tests/Expression/Level3/PathTest.php +++ b/tests/Expression/Level3/PathTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level3\Path, - Expression\Name, Expression, - Exception\DomainException, }; use Innmind\Immutable\{ Map, @@ -21,7 +19,10 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Path(new Name('foo'), new Name('bar')) + Path::of(Str::of('{/foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } @@ -29,13 +30,16 @@ public function testStringCast() { $this->assertSame( '{/foo,bar}', - (new Path(new Name('foo'), new Name('bar')))->toString(), + Path::of(Str::of('{/foo,bar}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('empty', '') @@ -45,11 +49,17 @@ public function testExpand() $this->assertSame( '/value', - (new Path(new Name('var')))->expand($variables) + Path::of(Str::of('{/var}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '/value/1024', - (new Path(new Name('var'), new Name('x')))->expand($variables) + Path::of(Str::of('{/var,x}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -57,24 +67,30 @@ public function testOf() { $this->assertInstanceOf( Path::class, - $expression = Path::of(Str::of('{/foo,bar}')) + $expression = Path::of(Str::of('{/foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{/foo,bar}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{/foo}'); - - Path::of(Str::of('{/foo}')); + $this->assertNull(Path::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '/(?[a-zA-Z0-9\%\-\.\_\~]*)/(?[a-zA-Z0-9\%\-\.\_\~]*)', - Path::of(Str::of('{/foo,bar}'))->regex() + Path::of(Str::of('{/foo,bar}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level3/QueryContinuationTest.php b/tests/Expression/Level3/QueryContinuationTest.php index 4729b12..56d3315 100644 --- a/tests/Expression/Level3/QueryContinuationTest.php +++ b/tests/Expression/Level3/QueryContinuationTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level3\QueryContinuation, - Expression\Name, Expression, - Exception\DomainException, }; use Innmind\Immutable\{ Map, @@ -21,7 +19,10 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new QueryContinuation(new Name('foo'), new Name('bar')) + QueryContinuation::of(Str::of('{&foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } @@ -29,13 +30,16 @@ public function testStringCast() { $this->assertSame( '{&foo,bar}', - (new QueryContinuation(new Name('foo'), new Name('bar')))->toString(), + QueryContinuation::of(Str::of('{&foo,bar}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('empty', '') @@ -45,11 +49,17 @@ public function testExpand() $this->assertSame( '&x=1024&y=768', - (new QueryContinuation(new Name('x'), new Name('y')))->expand($variables) + QueryContinuation::of(Str::of('{&x,y}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '&x=1024&y=768&empty=', - (new QueryContinuation(new Name('x'), new Name('y'), new Name('empty')))->expand($variables) + QueryContinuation::of(Str::of('{&x,y,empty}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -57,24 +67,30 @@ public function testOf() { $this->assertInstanceOf( QueryContinuation::class, - $expression = QueryContinuation::of(Str::of('{&foo,bar}')) + $expression = QueryContinuation::of(Str::of('{&foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{&foo,bar}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{&foo}'); - - QueryContinuation::of(Str::of('{&foo}')); + $this->assertNull(QueryContinuation::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '\&foo=(?[a-zA-Z0-9\%\-\.\_\~]*)\&bar=(?[a-zA-Z0-9\%\-\.\_\~]*)', - QueryContinuation::of(Str::of('{&foo,bar}'))->regex() + QueryContinuation::of(Str::of('{&foo,bar}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level3/QueryTest.php b/tests/Expression/Level3/QueryTest.php index e6bc1ef..ddc4dae 100644 --- a/tests/Expression/Level3/QueryTest.php +++ b/tests/Expression/Level3/QueryTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level3\Query, - Expression\Name, Expression, - Exception\DomainException, }; use Innmind\Immutable\{ Map, @@ -21,7 +19,10 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Query(new Name('foo'), new Name('bar')) + Query::of(Str::of('{?foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } @@ -29,13 +30,16 @@ public function testStringCast() { $this->assertSame( '{?foo,bar}', - (new Query(new Name('foo'), new Name('bar')))->toString(), + Query::of(Str::of('{?foo,bar}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('empty', '') @@ -45,11 +49,17 @@ public function testExpand() $this->assertSame( '?x=1024&y=768', - (new Query(new Name('x'), new Name('y')))->expand($variables) + Query::of(Str::of('{?x,y}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '?x=1024&y=768&empty=', - (new Query(new Name('x'), new Name('y'), new Name('empty')))->expand($variables) + Query::of(Str::of('{?x,y,empty}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -57,24 +67,30 @@ public function testOf() { $this->assertInstanceOf( Query::class, - $expression = Query::of(Str::of('{?foo,bar}')) + $expression = Query::of(Str::of('{?foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{?foo,bar}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{?foo}'); - - Query::of(Str::of('{?foo}')); + $this->assertNull(Query::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '\?foo=(?[a-zA-Z0-9\%\-\.\_\~]*)\&bar=(?[a-zA-Z0-9\%\-\.\_\~]*)', - Query::of(Str::of('{?foo,bar}'))->regex() + Query::of(Str::of('{?foo,bar}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level3/ReservedTest.php b/tests/Expression/Level3/ReservedTest.php index ae6e6ae..b89b3bd 100644 --- a/tests/Expression/Level3/ReservedTest.php +++ b/tests/Expression/Level3/ReservedTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level3\Reserved, - Expression\Name, Expression, - Exception\DomainException, }; use Innmind\Immutable\{ Map, @@ -21,7 +19,10 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Reserved(new Name('foo'), new Name('bar')) + Reserved::of(Str::of('{+foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } @@ -29,13 +30,16 @@ public function testStringCast() { $this->assertSame( '{+foo,bar}', - (new Reserved(new Name('foo'), new Name('bar')))->toString(), + Reserved::of(Str::of('{+foo,bar}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('empty', '') @@ -45,11 +49,17 @@ public function testExpand() $this->assertSame( '1024,Hello%20World!,768', - (new Reserved(new Name('x'), new Name('hello'), new Name('y')))->expand($variables) + Reserved::of(Str::of('{+x,hello,y}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '/foo/bar,1024', - (new Reserved(new Name('path'), new Name('x')))->expand($variables) + Reserved::of(Str::of('{+path,x}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -57,24 +67,30 @@ public function testOf() { $this->assertInstanceOf( Reserved::class, - $expression = Reserved::of(Str::of('{+foo,bar}')) + $expression = Reserved::of(Str::of('{+foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{+foo,bar}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{+foo}'); - - Reserved::of(Str::of('{+foo}')); + $this->assertNull(Reserved::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]*),(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]*)', - Reserved::of(Str::of('{+foo,bar}'))->regex() + Reserved::of(Str::of('{+foo,bar}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level3Test.php b/tests/Expression/Level3Test.php index da16ecc..5cd1194 100644 --- a/tests/Expression/Level3Test.php +++ b/tests/Expression/Level3Test.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level3, - Expression\Name, Expression, - Exception\DomainException, }; use Innmind\Immutable\{ Map, @@ -21,7 +19,10 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Level3(new Name('foo'), new Name('bar')) + Level3::of(Str::of('{foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } @@ -29,13 +30,16 @@ public function testStringCast() { $this->assertSame( '{foo,bar}', - (new Level3(new Name('foo'), new Name('bar')))->toString(), + Level3::of(Str::of('{foo,bar}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('empty', '') @@ -45,11 +49,17 @@ public function testExpand() $this->assertSame( '1024,768', - (new Level3(new Name('x'), new Name('y')))->expand($variables) + Level3::of(Str::of('{x,y}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '1024,Hello%20World%21,768', - (new Level3(new Name('x'), new Name('hello'), new Name('y')))->expand($variables) + Level3::of(Str::of('{x,hello,y}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -57,24 +67,30 @@ public function testOf() { $this->assertInstanceOf( Level3::class, - $expression = Level3::of(Str::of('{foo,bar}')) + $expression = Level3::of(Str::of('{foo,bar}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{foo,bar}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{foo}'); - - Level3::of(Str::of('{foo}')); + $this->assertNull(Level3::of(Str::of('{foo'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '(?[a-zA-Z0-9\%\-\.\_\~]*),(?[a-zA-Z0-9\%\-\.\_\~]*)', - Level3::of(Str::of('{foo,bar}'))->regex() + Level3::of(Str::of('{foo,bar}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level4/CompositeTest.php b/tests/Expression/Level4/CompositeTest.php index 6f23767..a57b5d9 100644 --- a/tests/Expression/Level4/CompositeTest.php +++ b/tests/Expression/Level4/CompositeTest.php @@ -7,9 +7,7 @@ Expression\Level4\Composite, Expression\Level4\Path, Expression\Level4, - Expression\Name, Expression, - Exception\DomainException, }; use Innmind\Immutable\{ Map, @@ -23,10 +21,10 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Composite( - '/', - $this->createMock(Expression::class) - ) + Composite::of(Str::of('{var}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } @@ -34,25 +32,23 @@ public function testStringCast() { $this->assertSame( '{/var:1,var}', - (new Composite( - '/', - Path::limit(new Name('var'), 1), - new Level4(new Name('var')) - ))->toString() + Composite::of(Str::of('{/var:1,var}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); $this->assertSame( '{/list*,path:4}', - (new Composite( - '/', - Path::explode(new Name('list')), - Level4::limit(new Name('path'), 4) - ))->toString() + Composite::of(Str::of('{/list*,path:4}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') @@ -61,19 +57,17 @@ public function testExpand() $this->assertSame( '/v/value', - (new Composite( - '/', - Path::limit(new Name('var'), 1), - new Level4(new Name('var')) - ))->expand($variables) + Composite::of(Str::of('{/var:1,var}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '/red/green/blue/%2Ffoo', - (new Composite( - '/', - Path::explode(new Name('list')), - Level4::limit(new Name('path'), 4) - ))->expand($variables) + Composite::of(Str::of('{/list*,path:4}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -82,64 +76,94 @@ public function testExpand() */ public function testOf($pattern, $expected) { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') ('list', ['red', 'green', 'blue']) ('keys', [['semi', ';'], ['dot', '.'], ['comma', ',']]); - $expression = Composite::of(Str::of($pattern)); + $expression = Composite::of(Str::of($pattern))->match( + static fn($expression) => $expression, + static fn() => null, + ); $this->assertSame($pattern, $expression->toString()); $this->assertSame($expected, $expression->expand($variables)); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('foo'); - - Composite::of(Str::of('foo')); + $this->assertNull(Composite::of(Str::of('foo'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testRegex() { $this->assertSame( '(?[a-zA-Z0-9\%\-\.\_\~]*)\,(?[a-zA-Z0-9\%\-\.\_\~]*)', - Composite::of(Str::of('{var,hello}'))->regex() + Composite::of(Str::of('{var,hello}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '(?[a-zA-Z0-9\%\-\.\_\~]*)\,(?[a-zA-Z0-9\%\-\.\_\~]{5})', - Composite::of(Str::of('{var,hello:5}'))->regex() + Composite::of(Str::of('{var,hello:5}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]*)\,(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]{5})', - Composite::of(Str::of('{+var,hello:5}'))->regex() + Composite::of(Str::of('{+var,hello:5}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\#(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]*)\,(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]{5})', - Composite::of(Str::of('{#var,hello:5}'))->regex() + Composite::of(Str::of('{#var,hello:5}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\.(?[a-zA-Z0-9\%\-\.\_\~]*)\.(?[a-zA-Z0-9\%\-\.\_\~]{5})', - Composite::of(Str::of('{.var,hello:5}'))->regex() + Composite::of(Str::of('{.var,hello:5}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\/(?[a-zA-Z0-9\%\-\.\_\~]*)\/(?[a-zA-Z0-9\%\-\.\_\~]{5})', - Composite::of(Str::of('{/var,hello:5}'))->regex() + Composite::of(Str::of('{/var,hello:5}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\;var=(?[a-zA-Z0-9\%\-\.\_\~]*)\;hello=(?[a-zA-Z0-9\%\-\.\_\~]{5})', - Composite::of(Str::of('{;var,hello:5}'))->regex() + Composite::of(Str::of('{;var,hello:5}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\?var=(?[a-zA-Z0-9\%\-\.\_\~]*)\&hello=(?[a-zA-Z0-9\%\-\.\_\~]{5})', - Composite::of(Str::of('{?var,hello:5}'))->regex() + Composite::of(Str::of('{?var,hello:5}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\&var=(?[a-zA-Z0-9\%\-\.\_\~]*)\&hello=(?[a-zA-Z0-9\%\-\.\_\~]{5})', - Composite::of(Str::of('{&var,hello:5}'))->regex() + Composite::of(Str::of('{&var,hello:5}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } diff --git a/tests/Expression/Level4/FragmentTest.php b/tests/Expression/Level4/FragmentTest.php index a475f52..9a18c9f 100644 --- a/tests/Expression/Level4/FragmentTest.php +++ b/tests/Expression/Level4/FragmentTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level4\Fragment, - Expression\Name, Expression, - Exception\DomainException, Exception\LogicException, }; use Innmind\Immutable\{ @@ -28,39 +26,67 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Fragment(new Name('foo')) + Fragment::of(Str::of('{#foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Fragment::explode(new Name('foo')) + Fragment::of(Str::of('{#foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Fragment::limit(new Name('foo'), 42) + Fragment::of(Str::of('{#foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{#foo}', (new Fragment(new Name('foo')))->toString()); - $this->assertSame('{#foo*}', Fragment::explode(new Name('foo'))->toString()); - $this->assertSame('{#foo:42}', Fragment::limit(new Name('foo'), 42)->toString()); + $this->assertSame( + '{#foo}', + Fragment::of(Str::of('{#foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{#foo*}', + Fragment::of(Str::of('{#foo*}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{#foo:42}', + Fragment::of(Str::of('{#foo:42}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } - public function testThrowWhenNegativeLimit() + public function testReturnNothingWhenNegativeLimit() { $this ->forAll(Set\Integers::below(1)) ->then(function(int $int): void { - $this->expectException(DomainException::class); - - Fragment::limit(new Name('foo'), $int); + $this->assertNull(Fragment::of(Str::of("{#list:$int}"))->match( + static fn($expression) => $expression, + static fn() => null, + )); }); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') @@ -69,23 +95,38 @@ public function testExpand() $this->assertSame( '#/foo/b', - Fragment::limit(new Name('path'), 6)->expand($variables) + Fragment::of(Str::of('{#path:6}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '#red,green,blue', - (new Fragment(new Name('list')))->expand($variables) + Fragment::of(Str::of('{#list}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '#red,green,blue', - Fragment::explode(new Name('list'))->expand($variables) + Fragment::of(Str::of('{#list*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '#semi,;,dot,.,comma,,', - (new Fragment(new Name('keys')))->expand($variables) + Fragment::of(Str::of('{#keys}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '#semi=;,dot=.,comma=,', - Fragment::explode(new Name('keys'))->expand($variables) + Fragment::of(Str::of('{#keys*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -93,45 +134,63 @@ public function testOf() { $this->assertInstanceOf( Fragment::class, - $expression = Fragment::of(Str::of('{#foo}')) + $expression = Fragment::of(Str::of('{#foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{#foo}', $expression->toString()); $this->assertInstanceOf( Fragment::class, - $expression = Fragment::of(Str::of('{#foo*}')) + $expression = Fragment::of(Str::of('{#foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{#foo*}', $expression->toString()); $this->assertInstanceOf( Fragment::class, - $expression = Fragment::of(Str::of('{#foo:42}')) + $expression = Fragment::of(Str::of('{#foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{#foo:42}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{foo}'); - - Fragment::of(Str::of('{foo}')); + $this->assertNull(Fragment::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testThrowExplodeRegex() { $this->expectException(LogicException::class); - Fragment::of(Str::of('{#foo*}'))->regex(); + Fragment::of(Str::of('{#foo*}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ); } public function testRegex() { $this->assertSame( '\#(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]*)', - Fragment::of(Str::of('{#foo}'))->regex() + Fragment::of(Str::of('{#foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\#(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]{2})', - Fragment::of(Str::of('{#foo:2}'))->regex() + Fragment::of(Str::of('{#foo:2}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level4/LabelTest.php b/tests/Expression/Level4/LabelTest.php index 2775ecb..099843b 100644 --- a/tests/Expression/Level4/LabelTest.php +++ b/tests/Expression/Level4/LabelTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level4\Label, - Expression\Name, Expression, - Exception\DomainException, Exception\LogicException, }; use Innmind\Immutable\{ @@ -28,39 +26,67 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Label(new Name('foo')) + Label::of(Str::of('{.foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Label::explode(new Name('foo')) + Label::of(Str::of('{.foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Label::limit(new Name('foo'), 42) + Label::of(Str::of('{.foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{.foo}', (new Label(new Name('foo')))->toString()); - $this->assertSame('{.foo*}', Label::explode(new Name('foo'))->toString()); - $this->assertSame('{.foo:42}', Label::limit(new Name('foo'), 42)->toString()); + $this->assertSame( + '{.foo}', + Label::of(Str::of('{.foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{.foo*}', + Label::of(Str::of('{.foo*}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{.foo:42}', + Label::of(Str::of('{.foo:42}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } - public function testThrowWhenNegativeLimit() + public function testReturnNothingWhenNegativeLimit() { $this ->forAll(Set\Integers::below(1)) ->then(function(int $int): void { - $this->expectException(DomainException::class); - - Label::limit(new Name('foo'), $int); + $this->assertNull(Label::of(Str::of("{.foo:$int}"))->match( + static fn($expression) => $expression, + static fn() => null, + )); }); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') @@ -69,23 +95,38 @@ public function testExpand() $this->assertSame( '.val', - Label::limit(new Name('var'), 3)->expand($variables) + Label::of(Str::of('{.var:3}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '.red,green,blue', - (new Label(new Name('list')))->expand($variables) + Label::of(Str::of('{.list}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '.red.green.blue', - Label::explode(new Name('list'))->expand($variables) + Label::of(Str::of('{.list*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '.semi,%3B,dot,.,comma,%2C', - (new Label(new Name('keys')))->expand($variables) + Label::of(Str::of('{.keys}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '.semi=%3B.dot=..comma=%2C', - Label::explode(new Name('keys'))->expand($variables) + Label::of(Str::of('{.keys*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -93,45 +134,63 @@ public function testOf() { $this->assertInstanceOf( Label::class, - $expression = Label::of(Str::of('{.foo}')) + $expression = Label::of(Str::of('{.foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{.foo}', $expression->toString()); $this->assertInstanceOf( Label::class, - $expression = Label::of(Str::of('{.foo*}')) + $expression = Label::of(Str::of('{.foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{.foo*}', $expression->toString()); $this->assertInstanceOf( Label::class, - $expression = Label::of(Str::of('{.foo:42}')) + $expression = Label::of(Str::of('{.foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{.foo:42}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{foo}'); - - Label::of(Str::of('{foo}')); + $this->assertNull(Label::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testThrowExplodeRegex() { $this->expectException(LogicException::class); - Label::of(Str::of('{.foo*}'))->regex(); + Label::of(Str::of('{.foo*}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ); } public function testRegex() { $this->assertSame( '\.(?[a-zA-Z0-9\%\-\.\_\~]*)', - Label::of(Str::of('{.foo}'))->regex() + Label::of(Str::of('{.foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\.(?[a-zA-Z0-9\%\-\.\_\~]{2})', - Label::of(Str::of('{.foo:2}'))->regex() + Label::of(Str::of('{.foo:2}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level4/ParametersTest.php b/tests/Expression/Level4/ParametersTest.php index ae6d2a4..a58d9ec 100644 --- a/tests/Expression/Level4/ParametersTest.php +++ b/tests/Expression/Level4/ParametersTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level4\Parameters, - Expression\Name, Expression, - Exception\DomainException, Exception\LogicException, }; use Innmind\Immutable\{ @@ -28,39 +26,67 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Parameters(new Name('foo')) + Parameters::of(Str::of('{;foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Parameters::explode(new Name('foo')) + Parameters::of(Str::of('{;foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Parameters::limit(new Name('foo'), 42) + Parameters::of(Str::of('{;foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{;foo}', (new Parameters(new Name('foo')))->toString()); - $this->assertSame('{;foo*}', Parameters::explode(new Name('foo'))->toString()); - $this->assertSame('{;foo:42}', Parameters::limit(new Name('foo'), 42)->toString()); + $this->assertSame( + '{;foo}', + Parameters::of(Str::of('{;foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{;foo*}', + Parameters::of(Str::of('{;foo*}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{;foo:42}', + Parameters::of(Str::of('{;foo:42}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } - public function testThrowWhenNegativeLimit() + public function testReturnNothingWhenNegativeLimit() { $this ->forAll(Set\Integers::below(1)) ->then(function(int $int): void { - $this->expectException(DomainException::class); - - Parameters::limit(new Name('foo'), $int); + $this->assertNull(Parameters::of(Str::of("{;foo:$int}"))->match( + static fn($expression) => $expression, + static fn() => null, + )); }); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') @@ -69,23 +95,38 @@ public function testExpand() $this->assertSame( ';hello=Hello', - Parameters::limit(new Name('hello'), 5)->expand($variables) + Parameters::of(Str::of('{;hello:5}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( ';list=red,green,blue', - (new Parameters(new Name('list')))->expand($variables) + Parameters::of(Str::of('{;list}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( ';list=red;list=green;list=blue', - Parameters::explode(new Name('list'))->expand($variables) + Parameters::of(Str::of('{;list*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( ';keys=semi,%3B,dot,.,comma,%2C', - (new Parameters(new Name('keys')))->expand($variables) + Parameters::of(Str::of('{;keys}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( ';semi=%3B;dot=.;comma=%2C', - Parameters::explode(new Name('keys'))->expand($variables) + Parameters::of(Str::of('{;keys*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -93,45 +134,63 @@ public function testOf() { $this->assertInstanceOf( Parameters::class, - $expression = Parameters::of(Str::of('{;foo}')) + $expression = Parameters::of(Str::of('{;foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{;foo}', $expression->toString()); $this->assertInstanceOf( Parameters::class, - $expression = Parameters::of(Str::of('{;foo*}')) + $expression = Parameters::of(Str::of('{;foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{;foo*}', $expression->toString()); $this->assertInstanceOf( Parameters::class, - $expression = Parameters::of(Str::of('{;foo:42}')) + $expression = Parameters::of(Str::of('{;foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{;foo:42}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{foo}'); - - Parameters::of(Str::of('{foo}')); + $this->assertNull(Parameters::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testThrowExplodeRegex() { $this->expectException(LogicException::class); - Parameters::of(Str::of('{;foo*}'))->regex(); + Parameters::of(Str::of('{;foo*}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ); } public function testRegex() { $this->assertSame( '\;foo=(?[a-zA-Z0-9\%\-\.\_\~]*)', - Parameters::of(Str::of('{;foo}'))->regex() + Parameters::of(Str::of('{;foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\;foo=(?[a-zA-Z0-9\%\-\.\_\~]{2})', - Parameters::of(Str::of('{;foo:2}'))->regex() + Parameters::of(Str::of('{;foo:2}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level4/PathTest.php b/tests/Expression/Level4/PathTest.php index b0e7bf3..0cd2dac 100644 --- a/tests/Expression/Level4/PathTest.php +++ b/tests/Expression/Level4/PathTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level4\Path, - Expression\Name, Expression, - Exception\DomainException, Exception\LogicException, }; use Innmind\Immutable\{ @@ -28,39 +26,67 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Path(new Name('foo')) + Path::of(Str::of('{/foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Path::explode(new Name('foo')) + Path::of(Str::of('{/foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Path::limit(new Name('foo'), 42) + Path::of(Str::of('{/foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{/foo}', (new Path(new Name('foo')))->toString()); - $this->assertSame('{/foo*}', Path::explode(new Name('foo'))->toString()); - $this->assertSame('{/foo:42}', Path::limit(new Name('foo'), 42)->toString()); + $this->assertSame( + '{/foo}', + Path::of(Str::of('{/foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{/foo*}', + Path::of(Str::of('{/foo*}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{/foo:42}', + Path::of(Str::of('{/foo:42}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } - public function testThrowWhenNegativeLimit() + public function testReturnNothingWhenNegativeLimit() { $this ->forAll(Set\Integers::below(1)) ->then(function(int $int): void { - $this->expectException(DomainException::class); - - Path::limit(new Name('foo'), $int); + $this->assertNull(Path::of(Str::of("{/foo:$int}"))->match( + static fn($expression) => $expression, + static fn() => null, + )); }); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') @@ -69,19 +95,31 @@ public function testExpand() $this->assertSame( '/red,green,blue', - (new Path(new Name('list')))->expand($variables) + Path::of(Str::of('{/list}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '/red/green/blue', - Path::explode(new Name('list'))->expand($variables) + Path::of(Str::of('{/list*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '/semi,%3B,dot,.,comma,%2C', - (new Path(new Name('keys')))->expand($variables) + Path::of(Str::of('{/keys}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '/semi=%3B/dot=./comma=%2C', - Path::explode(new Name('keys'))->expand($variables) + Path::of(Str::of('{/keys*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -89,45 +127,63 @@ public function testOf() { $this->assertInstanceOf( Path::class, - $expression = Path::of(Str::of('{/foo}')) + $expression = Path::of(Str::of('{/foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{/foo}', $expression->toString()); $this->assertInstanceOf( Path::class, - $expression = Path::of(Str::of('{/foo*}')) + $expression = Path::of(Str::of('{/foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{/foo*}', $expression->toString()); $this->assertInstanceOf( Path::class, - $expression = Path::of(Str::of('{/foo:42}')) + $expression = Path::of(Str::of('{/foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{/foo:42}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{foo}'); - - Path::of(Str::of('{foo}')); + $this->assertNull(Path::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testThrowExplodeRegex() { $this->expectException(LogicException::class); - Path::of(Str::of('{/foo*}'))->regex(); + Path::of(Str::of('{/foo*}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ); } public function testRegex() { $this->assertSame( '\/(?[a-zA-Z0-9\%\-\.\_\~]*)', - Path::of(Str::of('{/foo}'))->regex() + Path::of(Str::of('{/foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\/(?[a-zA-Z0-9\%\-\.\_\~]{2})', - Path::of(Str::of('{/foo:2}'))->regex() + Path::of(Str::of('{/foo:2}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level4/QueryContinuationTest.php b/tests/Expression/Level4/QueryContinuationTest.php index 4755e51..b2f807b 100644 --- a/tests/Expression/Level4/QueryContinuationTest.php +++ b/tests/Expression/Level4/QueryContinuationTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level4\QueryContinuation, - Expression\Name, Expression, - Exception\DomainException, Exception\LogicException, }; use Innmind\Immutable\{ @@ -28,39 +26,67 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new QueryContinuation(new Name('foo')) + QueryContinuation::of(Str::of('{&foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - QueryContinuation::explode(new Name('foo')) + QueryContinuation::of(Str::of('{&foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - QueryContinuation::limit(new Name('foo'), 42) + QueryContinuation::of(Str::of('{&foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{&foo}', (new QueryContinuation(new Name('foo')))->toString()); - $this->assertSame('{&foo*}', QueryContinuation::explode(new Name('foo'))->toString()); - $this->assertSame('{&foo:42}', QueryContinuation::limit(new Name('foo'), 42)->toString()); + $this->assertSame( + '{&foo}', + QueryContinuation::of(Str::of('{&foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{&foo*}', + QueryContinuation::of(Str::of('{&foo*}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{&foo:42}', + QueryContinuation::of(Str::of('{&foo:42}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } - public function testThrowWhenNegativeLimit() + public function testReturnNothingWhenNegativeLimit() { $this ->forAll(Set\Integers::below(1)) ->then(function(int $int): void { - $this->expectException(DomainException::class); - - QueryContinuation::limit(new Name('foo'), $int); + $this->assertNull(QueryContinuation::of(Str::of("{&foo:$int}"))->match( + static fn($expression) => $expression, + static fn() => null, + )); }); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') @@ -69,23 +95,38 @@ public function testExpand() $this->assertSame( '&var=val', - QueryContinuation::limit(new Name('var'), 3)->expand($variables) + QueryContinuation::of(Str::of('{&var:3}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '&list=red,green,blue', - (new QueryContinuation(new Name('list')))->expand($variables) + QueryContinuation::of(Str::of('{&list}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '&list=red&list=green&list=blue', - QueryContinuation::explode(new Name('list'))->expand($variables) + QueryContinuation::of(Str::of('{&list*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '&keys=semi,%3B,dot,.,comma,%2C', - (new QueryContinuation(new Name('keys')))->expand($variables) + QueryContinuation::of(Str::of('{&keys}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '&semi=%3B&dot=.&comma=%2C', - QueryContinuation::explode(new Name('keys'))->expand($variables) + QueryContinuation::of(Str::of('{&keys*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -93,45 +134,63 @@ public function testOf() { $this->assertInstanceOf( QueryContinuation::class, - $expression = QueryContinuation::of(Str::of('{&foo}')) + $expression = QueryContinuation::of(Str::of('{&foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{&foo}', $expression->toString()); $this->assertInstanceOf( QueryContinuation::class, - $expression = QueryContinuation::of(Str::of('{&foo*}')) + $expression = QueryContinuation::of(Str::of('{&foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{&foo*}', $expression->toString()); $this->assertInstanceOf( QueryContinuation::class, - $expression = QueryContinuation::of(Str::of('{&foo:42}')) + $expression = QueryContinuation::of(Str::of('{&foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{&foo:42}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{foo}'); - - QueryContinuation::of(Str::of('{foo}')); + $this->assertNull(QueryContinuation::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testThrowExplodeRegex() { $this->expectException(LogicException::class); - QueryContinuation::of(Str::of('{&foo*}'))->regex(); + QueryContinuation::of(Str::of('{&foo*}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ); } public function testRegex() { $this->assertSame( '\&foo=(?[a-zA-Z0-9\%\-\.\_\~]*)', - QueryContinuation::of(Str::of('{&foo}'))->regex() + QueryContinuation::of(Str::of('{&foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\&foo=(?[a-zA-Z0-9\%\-\.\_\~]{2})', - QueryContinuation::of(Str::of('{&foo:2}'))->regex() + QueryContinuation::of(Str::of('{&foo:2}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level4/QueryTest.php b/tests/Expression/Level4/QueryTest.php index b7471e2..0c4d636 100644 --- a/tests/Expression/Level4/QueryTest.php +++ b/tests/Expression/Level4/QueryTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level4\Query, - Expression\Name, Expression, - Exception\DomainException, Exception\LogicException, }; use Innmind\Immutable\{ @@ -28,39 +26,67 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Query(new Name('foo')) + Query::of(Str::of('{?foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Query::explode(new Name('foo')) + Query::of(Str::of('{?foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Query::limit(new Name('foo'), 42) + Query::of(Str::of('{?foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{?foo}', (new Query(new Name('foo')))->toString()); - $this->assertSame('{?foo*}', Query::explode(new Name('foo'))->toString()); - $this->assertSame('{?foo:42}', Query::limit(new Name('foo'), 42)->toString()); + $this->assertSame( + '{?foo}', + Query::of(Str::of('{?foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{?foo*}', + Query::of(Str::of('{?foo*}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{?foo:42}', + Query::of(Str::of('{?foo:42}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } - public function testThrowWhenNegativeLimit() + public function testReturnNothingWhenNegativeLimit() { $this ->forAll(Set\Integers::below(1)) ->then(function(int $int): void { - $this->expectException(DomainException::class); - - Query::limit(new Name('foo'), $int); + $this->assertNull(Query::of(Str::of("{?foo:$int}"))->match( + static fn($expression) => $expression, + static fn() => null, + )); }); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') @@ -69,23 +95,38 @@ public function testExpand() $this->assertSame( '?var=val', - Query::limit(new Name('var'), 3)->expand($variables) + Query::of(Str::of('{?var:3}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '?list=red,green,blue', - (new Query(new Name('list')))->expand($variables) + Query::of(Str::of('{?list}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '?list=red&list=green&list=blue', - Query::explode(new Name('list'))->expand($variables) + Query::of(Str::of('{?list*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '?keys=semi,%3B,dot,.,comma,%2C', - (new Query(new Name('keys')))->expand($variables) + Query::of(Str::of('{?keys}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '?semi=%3B&dot=.&comma=%2C', - Query::explode(new Name('keys'))->expand($variables) + Query::of(Str::of('{?keys*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -93,45 +134,63 @@ public function testOf() { $this->assertInstanceOf( Query::class, - $expression = Query::of(Str::of('{?foo}')) + $expression = Query::of(Str::of('{?foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{?foo}', $expression->toString()); $this->assertInstanceOf( Query::class, - $expression = Query::of(Str::of('{?foo*}')) + $expression = Query::of(Str::of('{?foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{?foo*}', $expression->toString()); $this->assertInstanceOf( Query::class, - $expression = Query::of(Str::of('{?foo:42}')) + $expression = Query::of(Str::of('{?foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{?foo:42}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{foo}'); - - Query::of(Str::of('{foo}')); + $this->assertNull(Query::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testThrowExplodeRegex() { $this->expectException(LogicException::class); - Query::of(Str::of('{?foo*}'))->regex(); + Query::of(Str::of('{?foo*}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ); } public function testRegex() { $this->assertSame( '\?foo=(?[a-zA-Z0-9\%\-\.\_\~]*)', - Query::of(Str::of('{?foo}'))->regex() + Query::of(Str::of('{?foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '\?foo=(?[a-zA-Z0-9\%\-\.\_\~]{2})', - Query::of(Str::of('{?foo:2}'))->regex() + Query::of(Str::of('{?foo:2}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level4/ReservedTest.php b/tests/Expression/Level4/ReservedTest.php index 57f8dee..ef735cc 100644 --- a/tests/Expression/Level4/ReservedTest.php +++ b/tests/Expression/Level4/ReservedTest.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level4\Reserved, - Expression\Name, Expression, - Exception\DomainException, Exception\LogicException, }; use Innmind\Immutable\{ @@ -28,39 +26,67 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Reserved(new Name('foo')) + Reserved::of(Str::of('{+foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Reserved::explode(new Name('foo')) + Reserved::of(Str::of('{+foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Reserved::limit(new Name('foo'), 42) + Reserved::of(Str::of('{+foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{+foo}', (new Reserved(new Name('foo')))->toString()); - $this->assertSame('{+foo*}', Reserved::explode(new Name('foo'))->toString()); - $this->assertSame('{+foo:42}', Reserved::limit(new Name('foo'), 42)->toString()); + $this->assertSame( + '{+foo}', + Reserved::of(Str::of('{+foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{+foo*}', + Reserved::of(Str::of('{+foo*}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{+foo:42}', + Reserved::of(Str::of('{+foo:42}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } - public function testThrowWhenNegativeLimit() + public function testReturnNothingWhenNegativeLimit() { $this ->forAll(Set\Integers::below(1)) ->then(function(int $int): void { - $this->expectException(DomainException::class); - - Reserved::limit(new Name('foo'), $int); + $this->assertNull(Reserved::of(Str::of("{+foo:$int}"))->match( + static fn($expression) => $expression, + static fn() => null, + )); }); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') @@ -69,23 +95,38 @@ public function testExpand() $this->assertSame( '/foo/b', - Reserved::limit(new Name('path'), 6)->expand($variables) + Reserved::of(Str::of('{+path:6}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( 'red,green,blue', - (new Reserved(new Name('list')))->expand($variables) + Reserved::of(Str::of('{+list}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( 'red,green,blue', - Reserved::explode(new Name('list'))->expand($variables) + Reserved::of(Str::of('{+list*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( 'semi,;,dot,.,comma,,', - (new Reserved(new Name('keys')))->expand($variables) + Reserved::of(Str::of('{+keys}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( 'semi=;,dot=.,comma=,', - Reserved::explode(new Name('keys'))->expand($variables) + Reserved::of(Str::of('{+keys*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -93,45 +134,63 @@ public function testOf() { $this->assertInstanceOf( Reserved::class, - $expression = Reserved::of(Str::of('{+foo}')) + $expression = Reserved::of(Str::of('{+foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{+foo}', $expression->toString()); $this->assertInstanceOf( Reserved::class, - $expression = Reserved::of(Str::of('{+foo*}')) + $expression = Reserved::of(Str::of('{+foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{+foo*}', $expression->toString()); $this->assertInstanceOf( Reserved::class, - $expression = Reserved::of(Str::of('{+foo:42}')) + $expression = Reserved::of(Str::of('{+foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{+foo:42}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('{foo}'); - - Reserved::of(Str::of('{foo}')); + $this->assertNull(Reserved::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testThrowExplodeRegex() { $this->expectException(LogicException::class); - Reserved::of(Str::of('{+foo*}'))->regex(); + Reserved::of(Str::of('{+foo*}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ); } public function testRegex() { $this->assertSame( '(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]*)', - Reserved::of(Str::of('{+foo}'))->regex() + Reserved::of(Str::of('{+foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '(?[a-zA-Z0-9\%:/\?#\[\]@!$&\'\(\)\*\+,;=\-\.\_\~]{2})', - Reserved::of(Str::of('{+foo:2}'))->regex() + Reserved::of(Str::of('{+foo:2}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/Level4Test.php b/tests/Expression/Level4Test.php index 19f4e61..46e1a93 100644 --- a/tests/Expression/Level4Test.php +++ b/tests/Expression/Level4Test.php @@ -5,9 +5,7 @@ use Innmind\UrlTemplate\{ Expression\Level4, - Expression\Name, Expression, - Exception\DomainException, Exception\LogicException, }; use Innmind\Immutable\{ @@ -28,39 +26,67 @@ public function testInterface() { $this->assertInstanceOf( Expression::class, - new Level4(new Name('foo')) + Level4::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Level4::explode(new Name('foo')) + Level4::of(Str::of('{foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertInstanceOf( Expression::class, - Level4::limit(new Name('foo'), 42) + Level4::of(Str::of('{foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); } public function testStringCast() { - $this->assertSame('{foo}', (new Level4(new Name('foo')))->toString()); - $this->assertSame('{foo*}', Level4::explode(new Name('foo'))->toString()); - $this->assertSame('{foo:42}', Level4::limit(new Name('foo'), 42)->toString()); + $this->assertSame( + '{foo}', + Level4::of(Str::of('{foo}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{foo*}', + Level4::of(Str::of('{foo*}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); + $this->assertSame( + '{foo:42}', + Level4::of(Str::of('{foo:42}'))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), + ); } - public function testThrowWhenNegativeLimit() + public function testReturnNothingWhenNegativeLimit() { $this ->forAll(Set\Integers::below(1)) ->then(function(int $int): void { - $this->expectException(DomainException::class); - - Level4::limit(new Name('foo'), $int); + $this->assertNull(Level4::of(Str::of("{foo:$int}"))->match( + static fn($expression) => $expression, + static fn() => null, + )); }); } public function testExpand() { - $variables = Map::of('string', 'variable') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') @@ -69,31 +95,52 @@ public function testExpand() $this->assertSame( 'val', - Level4::limit(new Name('var'), 3)->expand($variables) + Level4::of(Str::of('{var:3}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( 'value', - Level4::limit(new Name('var'), 30)->expand($variables) + Level4::of(Str::of('{var:30}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( '%2Ffoo', - Level4::limit(new Name('path'), 4)->expand($variables) + Level4::of(Str::of('{path:4}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( 'red,green,blue', - (new Level4(new Name('list')))->expand($variables) + Level4::of(Str::of('{list}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( 'red,green,blue', - Level4::explode(new Name('list'))->expand($variables) + Level4::of(Str::of('{list*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( 'semi,%3B,dot,.,comma,%2C', - (new Level4(new Name('keys')))->expand($variables) + Level4::of(Str::of('{keys}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); $this->assertSame( 'semi=%3B,dot=.,comma=%2C', - Level4::explode(new Name('keys'))->expand($variables) + Level4::of(Str::of('{keys*}'))->match( + static fn($expression) => $expression->expand($variables), + static fn() => null, + ), ); } @@ -101,45 +148,63 @@ public function testOf() { $this->assertInstanceOf( Level4::class, - $expression = Level4::of(Str::of('{foo}')) + $expression = Level4::of(Str::of('{foo}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{foo}', $expression->toString()); $this->assertInstanceOf( Level4::class, - $expression = Level4::of(Str::of('{foo*}')) + $expression = Level4::of(Str::of('{foo*}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{foo*}', $expression->toString()); $this->assertInstanceOf( Level4::class, - $expression = Level4::of(Str::of('{foo:42}')) + $expression = Level4::of(Str::of('{foo:42}'))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame('{foo:42}', $expression->toString()); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('foo'); - - Level4::of(Str::of('foo')); + $this->assertNull(Level4::of(Str::of('foo'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function testThrowExplodeRegex() { $this->expectException(LogicException::class); - Level4::of(Str::of('{foo*}'))->regex(); + Level4::of(Str::of('{foo*}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ); } public function testRegex() { $this->assertSame( '(?[a-zA-Z0-9\%\-\.\_\~]*)', - Level4::of(Str::of('{foo}'))->regex() + Level4::of(Str::of('{foo}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); $this->assertSame( '(?[a-zA-Z0-9\%\-\.\_\~]{2})', - Level4::of(Str::of('{foo:2}'))->regex() + Level4::of(Str::of('{foo:2}'))->match( + static fn($expression) => $expression->regex(), + static fn() => null, + ), ); } } diff --git a/tests/Expression/NameTest.php b/tests/Expression/NameTest.php index 5e6c3c2..76a1cef 100644 --- a/tests/Expression/NameTest.php +++ b/tests/Expression/NameTest.php @@ -21,12 +21,15 @@ public function testInterface() { $this ->forAll( - Set\Strings::atLeast(1)->filter(static function(string $string): bool { - return (bool) \preg_match('~[a-zA-Z0-9_]+~', $string); - }), + Set\Strings::madeOf( + Set\Chars::lowercaseLetter(), + Set\Chars::uppercaseLetter(), + Set\Chars::number(), + Set\Elements::of('_'), + )->atLeast(1), ) ->then(function(string $string): void { - $this->assertSame($string, (new Name($string))->toString()); + $this->assertSame($string, Name::of($string)->toString()); }); } @@ -35,14 +38,14 @@ public function testThrowWhenInvalidName() $this ->forAll( Set\Strings::any()->filter(static function(string $string): bool { - return (bool) !\preg_match('~[a-zA-Z0-9_]+~', $string); + return (bool) !\preg_match('~^[a-zA-Z0-9_]+$~', $string); }), ) ->then(function(string $string): void { $this->expectException(DomainException::class); $this->expectExceptionMessage($string); - new Name($string); + Name::of($string); }); } } diff --git a/tests/ExpressionsTest.php b/tests/ExpressionsTest.php index c79e399..0ffb298 100644 --- a/tests/ExpressionsTest.php +++ b/tests/ExpressionsTest.php @@ -7,7 +7,6 @@ Expressions, Expression\Level4, Expression\Level3, - Exception\DomainException, }; use Innmind\Immutable\Str; use PHPUnit\Framework\TestCase; @@ -21,20 +20,26 @@ public function testOf($string, $expected) { $this->assertInstanceOf( $expected, - Expressions::of(Str::of($string)) + Expressions::of(Str::of($string))->match( + static fn($expression) => $expression, + static fn() => null, + ), ); $this->assertSame( $string, - Expressions::of(Str::of($string))->toString(), + Expressions::of(Str::of($string))->match( + static fn($expression) => $expression->toString(), + static fn() => null, + ), ); } - public function testThrowWhenInvalidPattern() + public function testReturnNothingWhenInvalidPattern() { - $this->expectException(DomainException::class); - $this->expectExceptionMessage('foo'); - - Expressions::of(Str::of('foo')); + $this->assertNull(Expressions::of(Str::of('foo'))->match( + static fn($expression) => $expression, + static fn() => null, + )); } public function cases(): array diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 0927047..c70004b 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -5,8 +5,7 @@ use Innmind\UrlTemplate\{ Template, - Exception\UrlDoesntMatchTemplate, - Exception\ExtractionNotSupported, + Exception\ExplodeExpressionCantBeMatched, }; use Innmind\Url\Url; use Innmind\Immutable\{ @@ -37,7 +36,7 @@ public function testOf() */ public function testExpand($pattern, $expected) { - $variables = Map::of('string', 'scalar|array') + $variables = Map::of() ('var', 'value') ('hello', 'Hello World!') ('path', '/foo/bar') @@ -58,28 +57,12 @@ public function testExpand($pattern, $expected) $this->assertSame($expected, $url->toString()); } - public function testThrowWhenInvalidVariablesKeyType() + public function testReturnEmptyMapWhenUrlDoesntMatchTemplate() { - $this->expectException(\TypeError::class); - $this->expectExceptionMessage('Argument 1 must be of type Map'); - - Template::of('foo')->expand(Map::of('int', 'scalar|array')); - } - - public function testThrowWhenInvalidVariablesValueType() - { - $this->expectException(\TypeError::class); - $this->expectExceptionMessage('Argument 1 must be of type Map'); - - Template::of('foo')->expand(Map::of('string', 'string')); - } - - public function testThrowWhenUrlDoesntMatchTemplate() - { - $this->expectException(UrlDoesntMatchTemplate::class); - $this->expectExceptionMessage('/hello%20world%21/foo'); - - Template::of('/{foo}')->extract(Url::of('/hello%20world%21/foo')); + $this->assertCount( + 0, + Template::of('/{foo}')->extract(Url::of('/hello%20world%21/foo')), + ); } public function testLevel1Extraction() @@ -87,11 +70,15 @@ public function testLevel1Extraction() $variables = Template::of('/{foo}/{bar}')->extract(Url::of('/hello%20world%21/foo')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(2, $variables); - $this->assertSame('hello world!', $variables->get('foo')); - $this->assertSame('foo', $variables->get('bar')); + $this->assertSame('hello world!', $variables->get('foo')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('foo', $variables->get('bar')->match( + static fn($value) => $value, + static fn() => null, + )); } public function testLevel2Extraction() @@ -99,18 +86,20 @@ public function testLevel2Extraction() $variables = Template::of('{+path}/here')->extract(Url::of('/foo/bar/here')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('/foo/bar', $variables->get('path')); + $this->assertSame('/foo/bar', $variables->get('path')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('X{#hello}')->extract(Url::of('X#Hello%20World!')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('Hello World!', $variables->get('hello')); + $this->assertSame('Hello World!', $variables->get('hello')->match( + static fn($value) => $value, + static fn() => null, + )); } public function testLevel3Extraction() @@ -118,133 +107,204 @@ public function testLevel3Extraction() $variables = Template::of('/map?{x,y}')->extract(Url::of('/map?1024,768')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(2, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('768', $variables->get('y')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('768', $variables->get('y')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('/{x,hello,y}')->extract(Url::of('/1024,Hello%20World%21,768')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(3, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('Hello World!', $variables->get('hello')); - $this->assertSame('768', $variables->get('y')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('Hello World!', $variables->get('hello')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('768', $variables->get('y')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('/{+x,hello,y}')->extract(Url::of('/1024,Hello%20World!,768')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(3, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('Hello World!', $variables->get('hello')); - $this->assertSame('768', $variables->get('y')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('Hello World!', $variables->get('hello')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('768', $variables->get('y')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{+path,x}/here')->extract(Url::of('/foo/bar,1024/here')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(2, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('/foo/bar', $variables->get('path')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('/foo/bar', $variables->get('path')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{#x,hello,y}')->extract(Url::of('#1024,Hello%20World!,768')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(3, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('Hello World!', $variables->get('hello')); - $this->assertSame('768', $variables->get('y')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('Hello World!', $variables->get('hello')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('768', $variables->get('y')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{#path,x}/here')->extract(Url::of('#/foo/bar,1024/here')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(2, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('/foo/bar', $variables->get('path')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('/foo/bar', $variables->get('path')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{.x,y}')->extract(Url::of('.1024.768')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(2, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('768', $variables->get('y')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('768', $variables->get('y')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{/var,x}/here')->extract(Url::of('/value/1024/here')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(2, $variables); - $this->assertSame('value', $variables->get('var')); - $this->assertSame('1024', $variables->get('x')); + $this->assertSame('value', $variables->get('var')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{;x,y}')->extract(Url::of(';x=1024;y=768')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(2, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('768', $variables->get('y')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('768', $variables->get('y')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{;x,y,empty}')->extract(Url::of(';x=1024;y=768;empty')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(3, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('768', $variables->get('y')); - $this->assertSame('', $variables->get('empty')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('768', $variables->get('y')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('', $variables->get('empty')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{?x,y}')->extract(Url::of('?x=1024&y=768')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(2, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('768', $variables->get('y')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('768', $variables->get('y')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{?x,y,empty}')->extract(Url::of('?x=1024&y=768&empty=')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(3, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('768', $variables->get('y')); - $this->assertSame('', $variables->get('empty')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('768', $variables->get('y')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('', $variables->get('empty')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('?fixed=yes{&x}')->extract(Url::of('?fixed=yes&x=1024')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('1024', $variables->get('x')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{&x,y,empty}')->extract(Url::of('&x=1024&y=768&empty=')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(3, $variables); - $this->assertSame('1024', $variables->get('x')); - $this->assertSame('768', $variables->get('y')); - $this->assertSame('', $variables->get('empty')); + $this->assertSame('1024', $variables->get('x')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('768', $variables->get('y')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('', $variables->get('empty')->match( + static fn($value) => $value, + static fn() => null, + )); } public function testLevel4Extraction() @@ -252,66 +312,74 @@ public function testLevel4Extraction() $variables = Template::of('{var:3}')->extract(Url::of('val')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('val', $variables->get('var')); + $this->assertSame('val', $variables->get('var')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{+path:6}/here')->extract(Url::of('/foo/b/here')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('/foo/b', $variables->get('path')); + $this->assertSame('/foo/b', $variables->get('path')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{#path:6}/here')->extract(Url::of('#/foo/b/here')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('/foo/b', $variables->get('path')); + $this->assertSame('/foo/b', $variables->get('path')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{.var:3}')->extract(Url::of('.val')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('val', $variables->get('var')); + $this->assertSame('val', $variables->get('var')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{/var:1}')->extract(Url::of('/v')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('v', $variables->get('var')); + $this->assertSame('v', $variables->get('var')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{;var:5}')->extract(Url::of(';var=hello')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('hello', $variables->get('var')); + $this->assertSame('hello', $variables->get('var')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{?var:3}')->extract(Url::of('?var=hel')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('hel', $variables->get('var')); + $this->assertSame('hel', $variables->get('var')->match( + static fn($value) => $value, + static fn() => null, + )); $variables = Template::of('{&var:3}')->extract(Url::of('&var=hel')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(1, $variables); - $this->assertSame('hel', $variables->get('var')); + $this->assertSame('hel', $variables->get('var')->match( + static fn($value) => $value, + static fn() => null, + )); } public function testExtraction() @@ -320,16 +388,20 @@ public function testExtraction() ->extract(Url::of('http://example.com/search?q=chien&lang=fr')); $this->assertInstanceOf(Map::class, $variables); - $this->assertSame('string', (string) $variables->keyType()); - $this->assertSame('string', (string) $variables->valueType()); $this->assertCount(2, $variables); - $this->assertSame('chien', $variables->get('q')); - $this->assertSame('fr', $variables->get('lang')); + $this->assertSame('chien', $variables->get('q')->match( + static fn($value) => $value, + static fn() => null, + )); + $this->assertSame('fr', $variables->get('lang')->match( + static fn($value) => $value, + static fn() => null, + )); } public function testThrowWhenExtractionNotSupportedForTemplate() { - $this->expectException(ExtractionNotSupported::class); + $this->expectException(ExplodeExpressionCantBeMatched::class); Template::of('{foo*}')->extract(Url::of('foo,bar,baz')); } diff --git a/tests/UrlEncodeTest.php b/tests/UrlEncodeTest.php index 975dd73..90dab26 100644 --- a/tests/UrlEncodeTest.php +++ b/tests/UrlEncodeTest.php @@ -46,7 +46,7 @@ public function testSafeCharactersAreNotEncoded() '+', ',', ';', - '=' + '=', )) ->then(function(string $char): void { $encode = UrlEncode::allowReservedCharacters(); @@ -61,7 +61,7 @@ public function testSafeCharactersAreNotEncodedEvenWhenInMiddleOfString() $this->assertSame( ':%20)', - $encode(': )') + $encode(': )'), ); }