diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 314b8a4..8571406 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,13 +10,28 @@ on:
workflow_dispatch:
jobs:
- tests:
- runs-on: ubuntu-20.04
+ cs-fix:
+ name: CS Fixer
+ runs-on: ubuntu-22.04
- strategy:
- fail-fast: true
- matrix:
- php-versions: ['7.3', '7.4']
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.1'
+
+ - name: Install dependencies
+ run: composer install
+
+ - name: Run CS Fixer
+ run: composer run-script cs
+
+ phpstan:
+ name: PHPStan
+ runs-on: ubuntu-22.04
steps:
- name: Checkout
@@ -25,16 +40,17 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: ${{ matrix.php-versions }}
+ php-version: '8.1'
- name: Install dependencies
- run: composer install --optimize-autoloader --prefer-dist
+ run: composer install
- - name: Run tests
- run: composer run-script test
+ - name: Run CS Fixer
+ run: composer run-script phpstan
- tests-php8:
- runs-on: ubuntu-20.04
+ tests:
+ name: PHPUnit on PHP ${{ matrix.php-versions }}
+ runs-on: ubuntu-22.04
strategy:
fail-fast: true
@@ -51,7 +67,7 @@ jobs:
php-version: ${{ matrix.php-versions }}
- name: Install dependencies
- run: composer require -W phpunit/phpunit
+ run: composer install
- name: Run tests
run: composer run-script test
diff --git a/.gitignore b/.gitignore
index e5294f4..0943256 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+/.idea
vendor
composer.lock
.DS_Store
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..cb2af38
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,19 @@
+setParallelConfig(ParallelConfigFactory::detect())
+ ->setRiskyAllowed(true)
+ ->setUsingCache(false)
+ ->setRules([
+ '@PER-CS2.0' => true,
+ ])
+ ->setFinder(
+ (new Finder())
+ ->in([__DIR__ . '/src', __DIR__ . '/tests'])
+ );
\ No newline at end of file
diff --git a/composer.json b/composer.json
index cec985a..0dbfd2e 100644
--- a/composer.json
+++ b/composer.json
@@ -11,10 +11,12 @@
}
],
"require": {
- "php": ">=5.3.0"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "~4.8.36"
+ "phpunit/phpunit": "~10.0",
+ "phpstan/phpstan": "^1.12",
+ "friendsofphp/php-cs-fixer": "^3.63"
},
"autoload": {
"psr-4": {
@@ -22,6 +24,9 @@
}
},
"scripts": {
- "test": "vendor/bin/phpunit test/"
+ "test": "vendor/bin/phpunit",
+ "cs": "vendor/bin/php-cs-fixer fix --dry-run",
+ "cs-fix": "vendor/bin/php-cs-fixer fix",
+ "phpstan": "vendor/bin/phpstan"
}
}
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..a76a832
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,5 @@
+parameters:
+ level: 5
+ paths:
+ - src
+ - tests
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..530f84f
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
diff --git a/src/Rize/UriTemplate.php b/src/Rize/UriTemplate.php
index 330f39b..51a841d 100644
--- a/src/Rize/UriTemplate.php
+++ b/src/Rize/UriTemplate.php
@@ -5,47 +5,38 @@
use Rize\UriTemplate\Parser;
/**
- * URI Template
+ * URI Template.
*/
class UriTemplate
{
- /**
- * @var Parser
- */
- protected $parser,
- $parsed = array(),
- $base_uri,
- $params = array();
-
- public function __construct($base_uri = '', $params = array(), Parser $parser = null)
+ protected Parser $parser;
+ protected array $parsed = [];
+
+ public function __construct(protected string $base_uri = '', protected array $params = [], ?Parser $parser = null)
{
- $this->base_uri = $base_uri;
- $this->params = $params;
- $this->parser = $parser ?: $this->createNodeParser();
+ $this->parser = $parser ?: $this->createNodeParser();
}
/**
- * Expands URI Template
+ * Expands URI Template.
*
- * @param string $uri URI Template
- * @param array $params URI Template's parameters
- * @return string
+ * @param mixed $params
*/
- public function expand($uri, $params = array())
+ public function expand(string $uri, $params = []): string
{
$params += $this->params;
- $uri = $this->base_uri.$uri;
- $result = array();
+ $uri = $this->base_uri . $uri;
+ $result = [];
// quick check
- if (($start = strpos($uri, '{')) === false) {
+ if (!str_contains($uri, '{')) {
return $uri;
}
$parser = $this->parser;
$nodes = $parser->parse($uri);
- foreach($nodes as $node) {
+ foreach ($nodes as $node) {
$result[] = $node->expand($parser, $params);
}
@@ -53,48 +44,48 @@ public function expand($uri, $params = array())
}
/**
- * Extracts variables from URI
+ * Extracts variables from URI.
*
- * @param string $template
- * @param string $uri
- * @param bool $strict This will perform a full match
* @return null|array params or null if not match and $strict is true
*/
- public function extract($template, $uri, $strict = false)
+ public function extract(string $template, string $uri, bool $strict = false): ?array
{
- $params = array();
+ $params = [];
$nodes = $this->parser->parse($template);
- # PHP 8.1.0RC4-dev still throws deprecation warning for `strlen`.
- # $uri = (string) $uri;
-
- foreach($nodes as $node) {
+ // PHP 8.1.0RC4-dev still throws deprecation warning for `strlen`.
+ // $uri = (string) $uri;
+ foreach ($nodes as $node) {
// if strict is given, and there's no remaining uri just return null
- if ($strict && !strlen((string) $uri)) {
+ if ($strict && (string) $uri === '') {
return null;
}
- // uri'll be truncated from the start when a match is found
+ // URI will be truncated from the start when a match is found
$match = $node->match($this->parser, $uri, $params, $strict);
- list($uri, $params) = $match;
+ if ($match === null) {
+ return null;
+ }
+
+ [$uri, $params] = $match;
}
// if there's remaining $uri, matching is failed
- if ($strict && strlen((string) $uri)) {
+ if ($strict && (string) $uri !== '') {
return null;
}
return $params;
}
- public function getParser()
+ public function getParser(): Parser
{
return $this->parser;
}
- protected function createNodeParser()
+ protected function createNodeParser(): Parser
{
static $parser;
@@ -102,6 +93,6 @@ protected function createNodeParser()
return $parser;
}
- return $parser = new Parser;
+ return $parser = new Parser();
}
}
diff --git a/src/Rize/UriTemplate/Node/Abstraction.php b/src/Rize/UriTemplate/Node/Abstraction.php
index 3f2e350..7332df5 100644
--- a/src/Rize/UriTemplate/Node/Abstraction.php
+++ b/src/Rize/UriTemplate/Node/Abstraction.php
@@ -5,61 +5,45 @@
use Rize\UriTemplate\Parser;
/**
- * Base class for all Nodes
+ * Base class for all Nodes.
*/
abstract class Abstraction
{
- /**
- * @var string
- */
- private $token;
-
- public function __construct($token)
- {
- $this->token = $token;
- }
+ public function __construct(private readonly string $token) {}
/**
* Expands URI template
*
- * @param Parser $parser
- * @param array $params
- * @return null|string
+ * @param array $params
*/
- public function expand(Parser $parser, array $params = array())
+ public function expand(Parser $parser, array $params = []): ?string
{
return $this->token;
}
/**
- * Matches given URI against current node
+ * Matches given URI against current node.
+ *
+ * @param array $params
*
- * @param Parser $parser
- * @param string $uri
- * @param array $params
- * @param bool $strict
- * @return null|array `uri and params` or `null` if not match and $strict is true
+ * @return null|array{0: string, 1: array} `uri and params` or `null` if not match and $strict is true
*/
- public function match(Parser $parser, $uri, $params = array(), $strict = false)
+ public function match(Parser $parser, string $uri, array $params = [], bool $strict = false): ?array
{
// match literal string from start to end
- $length = strlen($this->token);
- if (substr($uri, 0, $length) === $this->token) {
- $uri = substr($uri, $length);
+ if (str_starts_with($uri, $this->token)) {
+ $uri = substr($uri, strlen($this->token));
}
// when there's no match, just return null if strict mode is given
- else if ($strict) {
+ elseif ($strict) {
return null;
}
- return array($uri, $params);
+ return [$uri, $params];
}
- /**
- * @return string
- */
- public function getToken()
+ public function getToken(): string
{
return $this->token;
}
diff --git a/src/Rize/UriTemplate/Node/Expression.php b/src/Rize/UriTemplate/Node/Expression.php
index 892ccc6..a31fa24 100644
--- a/src/Rize/UriTemplate/Node/Expression.php
+++ b/src/Rize/UriTemplate/Node/Expression.php
@@ -2,83 +2,53 @@
namespace Rize\UriTemplate\Node;
-use Rize\UriTemplate\Parser;
use Rize\UriTemplate\Operator;
+use Rize\UriTemplate\Parser;
-/**
- * Description
- */
class Expression extends Abstraction
{
/**
- * @var Operator\Abstraction
- */
- private $operator;
-
- /**
- * @var array
+ * @param string $forwardLookupSeparator
*/
- private $variables = array();
-
- /**
- * Whether to do a forward lookup for a given separator
- * @var string
+ public function __construct(string $token, private readonly Operator\Abstraction $operator, private readonly ?array $variables = null, /**
+ * Whether to do a forward lookup for a given separator.
*/
- private $forwardLookupSeparator;
-
- public function __construct($token, Operator\Abstraction $operator, array $variables = null, $forwardLookupSeparator = null)
+ private $forwardLookupSeparator = null)
{
parent::__construct($token);
- $this->operator = $operator;
- $this->variables = $variables;
- $this->forwardLookupSeparator = $forwardLookupSeparator;
}
- /**
- * @return Operator\Abstraction
- */
- public function getOperator()
+ public function getOperator(): Operator\Abstraction
{
return $this->operator;
}
- /**
- * @return array
- */
- public function getVariables()
+ public function getVariables(): ?array
{
return $this->variables;
}
- /**
- * @return string
- */
- public function getForwardLookupSeparator()
+ public function getForwardLookupSeparator(): string
{
return $this->forwardLookupSeparator;
}
- /**
- * @param string $forwardLookupSeparator
- */
- public function setForwardLookupSeparator($forwardLookupSeparator)
+ public function setForwardLookupSeparator(string $forwardLookupSeparator): void
{
$this->forwardLookupSeparator = $forwardLookupSeparator;
}
- /**
- * @param Parser $parser
- * @param array $params
- * @return null|string
- */
- public function expand(Parser $parser, array $params = array())
+ public function expand(Parser $parser, array $params = []): ?string
{
- $data = array();
- $op = $this->operator;
+ $data = [];
+ $op = $this->operator;
- // check for variable modifiers
- foreach($this->variables as $var) {
+ if ($this->variables === null) {
+ return $op->first;
+ }
+ // check for variable modifiers
+ foreach ($this->variables as $var) {
$val = $op->expand($parser, $var, $params);
// skip null value
@@ -87,25 +57,21 @@ public function expand(Parser $parser, array $params = array())
}
}
- return $data ? $op->first.implode($op->sep, $data) : null;
+ return $data ? $op->first . implode($op->sep, $data) : null;
}
/**
- * Matches given URI against current node
+ * Matches given URI against current node.
*
- * @param Parser $parser
- * @param string $uri
- * @param array $params
- * @param bool $strict
* @return null|array `uri and params` or `null` if not match and $strict is true
*/
- public function match(Parser $parser, $uri, $params = array(), $strict = false)
+ public function match(Parser $parser, string $uri, array $params = [], bool $strict = false): ?array
{
$op = $this->operator;
// check expression operator first
if ($op->id && isset($uri[0]) && $uri[0] !== $op->id) {
- return array($uri, $params);
+ return [$uri, $params];
}
// remove operator from input
@@ -113,9 +79,8 @@ public function match(Parser $parser, $uri, $params = array(), $strict = false)
$uri = substr($uri, 1);
}
- foreach($this->sortVariables($this->variables) as $var) {
- /** @var \Rize\UriTemplate\Node\Variable $regex */
- $regex = '#'.$op->toRegex($parser, $var).'#';
+ foreach ($this->sortVariables($this->variables) as $var) {
+ $regex = '#' . $op->toRegex($parser, $var) . '#';
$val = null;
// do a forward lookup and get just the relevant part
@@ -128,14 +93,13 @@ public function match(Parser $parser, $uri, $params = array(), $strict = false)
}
if (preg_match($regex, $preparedUri, $match)) {
-
// remove matched part from input
- $preparedUri = preg_replace($regex, '', $preparedUri, $limit = 1);
+ $preparedUri = preg_replace($regex, '', $preparedUri, 1);
$val = $op->extract($parser, $var, $match[0]);
}
// if strict is given, we quit immediately when there's no match
- else if ($strict) {
+ elseif ($strict) {
return null;
}
@@ -144,21 +108,16 @@ public function match(Parser $parser, $uri, $params = array(), $strict = false)
$params[$var->getToken()] = $val;
}
- return array($uri, $params);
+ return [$uri, $params];
}
/**
* Sort variables before extracting data from uri.
* We have to sort vars by non-explode to explode.
- *
- * @param array $vars
- * @return array
*/
- protected function sortVariables(array $vars)
+ protected function sortVariables(array $vars): array
{
- usort($vars, function($a, $b) {
- return $a->options['modifier'] >= $b->options['modifier'] ? 1 : -1;
- });
+ usort($vars, static fn($a, $b) => $a->options['modifier'] <=> $b->options['modifier']);
return $vars;
}
diff --git a/src/Rize/UriTemplate/Node/Literal.php b/src/Rize/UriTemplate/Node/Literal.php
index 1c54da1..bc939b6 100644
--- a/src/Rize/UriTemplate/Node/Literal.php
+++ b/src/Rize/UriTemplate/Node/Literal.php
@@ -1,11 +1,5 @@
- null,
- 'value' => null,
- );
+ /**
+ * Variable name without modifier
+ * e.g. 'term:1' becomes 'term'.
+ */
+ public string $name;
+ public array $options = ['modifier' => null, 'value' => null];
- public function __construct($token, array $options = array())
+ public function __construct(string $token, array $options = [])
{
parent::__construct($token);
$this->options = $options + $this->options;
@@ -25,9 +19,9 @@ public function __construct($token, array $options = array())
// normalize var name e.g. from 'term:1' becomes 'term'
$name = $token;
if ($options['modifier'] === ':') {
- $name = substr($name, 0, strpos($name, $options['modifier']));
+ $name = strstr($name, $options['modifier'], true);
}
$this->name = $name;
}
-}
\ No newline at end of file
+}
diff --git a/src/Rize/UriTemplate/Operator/Abstraction.php b/src/Rize/UriTemplate/Operator/Abstraction.php
index 1b76337..856bd1c 100644
--- a/src/Rize/UriTemplate/Operator/Abstraction.php
+++ b/src/Rize/UriTemplate/Operator/Abstraction.php
@@ -14,7 +14,7 @@
* | named | false false false false true true true false |
* | ifemp | "" "" "" "" "" "=" "=" "" |
* | allow | U U+R U U U U U U+R |
- * `------------------------------------------------------------------'
+ * `------------------------------------------------------------------'.
*
* named = false
* | 1 | {/list} /red,green,blue | {$value}*(?:,{$value}+)*
@@ -35,7 +35,7 @@
*
* RESERVED
* --------
- * RFC 1738 ":" | "/" | "?" | | "@" | "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=" | "-" | "_" | "." |
+ * RFC 1738 ":" | "/" | "?" | | "@" | "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=" | "-" | "_" | "." |
* RFC 3986 ":" | "/" | "?" | "#" | "[" | "]" | "@" | "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
* RFC 6570 ":" | "/" | "?" | "#" | "[" | "]" | "@" | "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
*
@@ -47,108 +47,109 @@ abstract class Abstraction
* start - Variable offset position, level-2 operators start at 1
* (exclude operator itself, e.g. {?query})
* first - If variables found, prepend this value to it
- * named - Whether or not the expansion includes the variable or key name
- * reserved - union of (unreserved / reserved / pct-encoded)
+ * named - Whether the expansion includes the variable or key name
+ * reserved - union of (unreserved / reserved / pct-encoded).
*/
- public $id,
- $named,
- $sep,
- $empty,
- $reserved,
- $start,
- $first;
-
- protected static $types = array(
- '' => array(
- 'sep' => ',',
- 'named' => false,
- 'empty' => '',
- 'reserved' => false,
- 'start' => 0,
- 'first' => null,
- ),
- '+' => array(
- 'sep' => ',',
- 'named' => false,
- 'empty' => '',
- 'reserved' => true,
- 'start' => 1,
- 'first' => null,
- ),
- '.' => array(
- 'sep' => '.',
- 'named' => false,
- 'empty' => '',
- 'reserved' => false,
- 'start' => 1,
- 'first' => '.',
- ),
- '/' => array(
- 'sep' => '/',
- 'named' => false,
- 'empty' => '',
- 'reserved' => false,
- 'start' => 1,
- 'first' => '/',
- ),
- ';' => array(
- 'sep' => ';',
- 'named' => true,
- 'empty' => '',
- 'reserved' => false,
- 'start' => 1,
- 'first' => ';',
- ),
- '?' => array(
- 'sep' => '&',
- 'named' => true,
- 'empty' => '=',
- 'reserved' => false,
- 'start' => 1,
- 'first' => '?',
- ),
- '&' => array(
- 'sep' => '&',
- 'named' => true,
- 'empty' => '=',
- 'reserved' => false,
- 'start' => 1,
- 'first' => '&',
- ),
- '#' => array(
- 'sep' => ',',
- 'named' => false,
- 'empty' => '',
- 'reserved' => true,
- 'start' => 1,
- 'first' => '#',
- ),
- ),
- $loaded = array();
-
- /**
- * gen-delims | sub-delims
- */
- public static $reserved_chars = array(
- '%3A' => ':',
- '%2F' => '/',
- '%3F' => '?',
- '%23' => '#',
- '%5B' => '[',
- '%5D' => ']',
- '%40' => '@',
- '%21' => '!',
- '%24' => '$',
- '%26' => '&',
- '%27' => "'",
- '%28' => '(',
- '%29' => ')',
- '%2A' => '*',
- '%2B' => '+',
- '%2C' => ',',
- '%3B' => ';',
- '%3D' => '=',
- );
+ public $id;
+ public $named;
+ public $sep;
+ public $empty;
+ public $reserved;
+ public $start;
+ public $first;
+
+ /**
+ * gen-delims | sub-delims.
+ */
+ public static $reserved_chars = [
+ '%3A' => ':',
+ '%2F' => '/',
+ '%3F' => '?',
+ '%23' => '#',
+ '%5B' => '[',
+ '%5D' => ']',
+ '%40' => '@',
+ '%21' => '!',
+ '%24' => '$',
+ '%26' => '&',
+ '%27' => "'",
+ '%28' => '(',
+ '%29' => ')',
+ '%2A' => '*',
+ '%2B' => '+',
+ '%2C' => ',',
+ '%3B' => ';',
+ '%3D' => '=',
+ ];
+
+ protected static $types = [
+ '' => [
+ 'sep' => ',',
+ 'named' => false,
+ 'empty' => '',
+ 'reserved' => false,
+ 'start' => 0,
+ 'first' => null,
+ ],
+ '+' => [
+ 'sep' => ',',
+ 'named' => false,
+ 'empty' => '',
+ 'reserved' => true,
+ 'start' => 1,
+ 'first' => null,
+ ],
+ '.' => [
+ 'sep' => '.',
+ 'named' => false,
+ 'empty' => '',
+ 'reserved' => false,
+ 'start' => 1,
+ 'first' => '.',
+ ],
+ '/' => [
+ 'sep' => '/',
+ 'named' => false,
+ 'empty' => '',
+ 'reserved' => false,
+ 'start' => 1,
+ 'first' => '/',
+ ],
+ ';' => [
+ 'sep' => ';',
+ 'named' => true,
+ 'empty' => '',
+ 'reserved' => false,
+ 'start' => 1,
+ 'first' => ';',
+ ],
+ '?' => [
+ 'sep' => '&',
+ 'named' => true,
+ 'empty' => '=',
+ 'reserved' => false,
+ 'start' => 1,
+ 'first' => '?',
+ ],
+ '&' => [
+ 'sep' => '&',
+ 'named' => true,
+ 'empty' => '=',
+ 'reserved' => false,
+ 'start' => 1,
+ 'first' => '&',
+ ],
+ '#' => [
+ 'sep' => ',',
+ 'named' => false,
+ 'empty' => '',
+ 'reserved' => true,
+ 'start' => 1,
+ 'first' => '#',
+ ],
+ ];
+
+ protected static $loaded = [];
/**
* RFC 3986 Allowed path characters regex except the path delimiter '/'.
@@ -166,29 +167,29 @@ abstract class Abstraction
public function __construct($id, $named, $sep, $empty, $reserved, $start, $first)
{
- $this->id = $id;
+ $this->id = $id;
$this->named = $named;
- $this->sep = $sep;
+ $this->sep = $sep;
$this->empty = $empty;
$this->start = $start;
$this->first = $first;
$this->reserved = $reserved;
}
- abstract public function toRegex(Parser $parser, Node\Variable $var);
+ abstract public function toRegex(Parser $parser, Node\Variable $var): string;
- public function expand(Parser $parser, Node\Variable $var, array $params = array())
+ public function expand(Parser $parser, Node\Variable $var, array $params = [])
{
$options = $var->options;
$name = $var->name;
- $is_explode = in_array($options['modifier'], array('*', '%'));
+ $is_explode = in_array($options['modifier'], ['*', '%']);
// skip null
if (!isset($params[$name])) {
return null;
}
- $val = $params[$name];
+ $val = $params[$name];
// This algorithm is based on RFC6570 http://tools.ietf.org/html/rfc6570
// non-array, e.g. string
@@ -197,38 +198,32 @@ public function expand(Parser $parser, Node\Variable $var, array $params = array
}
// non-explode ':'
- else if (!$is_explode) {
+ if (!$is_explode) {
return $this->expandNonExplode($parser, $var, $val);
}
// explode '*', '%'
- else {
- return $this->expandExplode($parser, $var, $val);
- }
+
+ return $this->expandExplode($parser, $var, $val);
}
public function expandString(Parser $parser, Node\Variable $var, $val)
{
- $val = (string)$val;
+ $val = (string) $val;
$options = $var->options;
$result = null;
if ($options['modifier'] === ':') {
- $val = substr($val, 0, (int)$options['value']);
+ $val = substr($val, 0, (int) $options['value']);
}
- return $result.$this->encode($parser, $var, $val);
+ return $result . $this->encode($parser, $var, $val);
}
/**
- * Non explode modifier ':'
- *
- * @param Parser $parser
- * @param Node\Variable $var
- * @param array $val
- * @return null|string
+ * Non explode modifier ':'.
*/
- public function expandNonExplode(Parser $parser, Node\Variable $var, array $val)
+ public function expandNonExplode(Parser $parser, Node\Variable $var, array $val): ?string
{
if (empty($val)) {
return null;
@@ -238,14 +233,9 @@ public function expandNonExplode(Parser $parser, Node\Variable $var, array $val)
}
/**
- * Explode modifier '*', '%'
- *
- * @param Parser $parser
- * @param Node\Variable $var
- * @param array $val
- * @return null|string
+ * Explode modifier '*', '%'.
*/
- public function expandExplode(Parser $parser, Node\Variable $var, array $val)
+ public function expandExplode(Parser $parser, Node\Variable $var, array $val): ?string
{
if (empty($val)) {
return null;
@@ -255,17 +245,13 @@ public function expandExplode(Parser $parser, Node\Variable $var, array $val)
}
/**
- * Encodes variable according to spec (reserved or unreserved)
- *
- * @param Parser $parser
- * @param Node\Variable $var
- * @param mixed $values
+ * Encodes variable according to spec (reserved or unreserved).
*
* @return string encoded string
*/
- public function encode(Parser $parser, Node\Variable $var, $values)
+ public function encode(Parser $parser, Node\Variable $var, mixed $values)
{
- $values = (array)$values;
+ $values = (array) $values;
$list = isset($values[0]);
$reserved = $this->reserved;
$maps = static::$reserved_chars;
@@ -277,13 +263,12 @@ public function encode(Parser $parser, Node\Variable $var, $values)
$assoc_sep = $sep = ',';
}
- array_walk($values, function(&$v, $k) use ($assoc_sep, $reserved, $list, $maps) {
-
+ array_walk($values, function (&$v, $k) use ($assoc_sep, $reserved, $list, $maps): void {
$encoded = rawurlencode($v);
// assoc? encode key too
if (!$list) {
- $encoded = rawurlencode($k).$assoc_sep.$encoded;
+ $encoded = rawurlencode($k) . $assoc_sep . $encoded;
}
// rawurlencode is compliant with 'unreserved' set
@@ -293,11 +278,10 @@ public function encode(Parser $parser, Node\Variable $var, $values)
// decode chars in reserved set
else {
-
$v = str_replace(
array_keys($maps),
$maps,
- $encoded
+ $encoded,
);
}
});
@@ -306,71 +290,59 @@ public function encode(Parser $parser, Node\Variable $var, $values)
}
/**
- * Decodes variable
- *
- * @param Parser $parser
- * @param Node\Variable $var
- * @param mixed $values
+ * Decodes variable.
*
* @return string decoded string
*/
- public function decode(Parser $parser, Node\Variable $var, $values)
+ public function decode(Parser $parser, Node\Variable $var, mixed $values)
{
$single = !is_array($values);
- $values = (array)$values;
+ $values = (array) $values;
- array_walk($values, function(&$v, $k) {
+ array_walk($values, function (&$v, $k): void {
$v = rawurldecode($v);
});
return $single ? reset($values) : $values;
}
-
+
/**
- * Extracts value from variable
- *
- * @param Parser $parser
- * @param Node\Variable $var
- * @param string $data
- * @return string
+ * Extracts value from variable.
*/
- public function extract(Parser $parser, Node\Variable $var, $data)
+ public function extract(Parser $parser, Node\Variable $var, string $data): array|string
{
- $value = $data;
- $vals = array_filter(explode($this->sep, $data));
+ $value = $data;
+ $vals = array_filter(explode($this->sep, $data));
$options = $var->options;
switch ($options['modifier']) {
-
case '*':
- $data = array();
- foreach($vals as $val) {
-
- if (strpos($val, '=') !== false) {
- list($k, $v) = explode('=', $val);
- $data[$k] = $v;
- }
-
- else {
- $data[] = $val;
+ $value = [];
+ foreach ($vals as $val) {
+ if (str_contains($val, '=')) {
+ [$k, $v] = explode('=', $val);
+ $value[$k] = $v;
+ } else {
+ $value[] = $val;
}
}
break;
+
case ':':
break;
- default:
- $data = strpos($data, $this->sep) !== false ? $vals : $value;
+ default:
+ $value = str_contains($value, (string) $this->sep) ? $vals : $value;
}
- return $this->decode($parser, $var, $data);
+ return $this->decode($parser, $var, $value);
}
public static function createById($id)
{
if (!isset(static::$types[$id])) {
- throw new \Exception("Invalid operator [$id]");
+ throw new \InvalidArgumentException("Invalid operator [{$id}]");
}
if (isset(static::$loaded[$id])) {
@@ -378,31 +350,24 @@ public static function createById($id)
}
$op = static::$types[$id];
- $class = __NAMESPACE__.'\\'.($op['named'] ? 'Named' : 'UnNamed');
+ $class = __NAMESPACE__ . '\\' . ($op['named'] ? 'Named' : 'UnNamed');
return static::$loaded[$id] = new $class($id, $op['named'], $op['sep'], $op['empty'], $op['reserved'], $op['start'], $op['first']);
}
- public static function isValid($id)
+ public static function isValid($id): bool
{
return isset(static::$types[$id]);
}
/**
- * Returns the correct regex given the variable location in the URI
- *
- * @return string
+ * Returns the correct regex given the variable location in the URI.
*/
- protected function getRegex()
+ protected function getRegex(): string
{
- switch ($this->id) {
- case '?':
- case '&':
- case '#':
- return self::$queryRegex;
- case ';':
- default:
- return self::$pathRegex;
- }
+ return match ($this->id) {
+ '?', '&', '#' => self::$queryRegex,
+ default => self::$pathRegex,
+ };
}
}
diff --git a/src/Rize/UriTemplate/Operator/Named.php b/src/Rize/UriTemplate/Operator/Named.php
index 708710c..2c0a505 100644
--- a/src/Rize/UriTemplate/Operator/Named.php
+++ b/src/Rize/UriTemplate/Operator/Named.php
@@ -10,49 +10,51 @@
* | 2 | {?list*} ?list=red&list=green&list=blue | {name}+=(?:{$value}+(?:{sep}{name}+={$value}*))*
* | 3 | {?keys} ?keys=semi,%3B,dot,.,comma,%2C | (same as 1)
* | 4 | {?keys*} ?semi=%3B&dot=.&comma=%2C | (same as 2)
- * | 5 | {?list*} ?list[]=red&list[]=green&list[]=blue | {name[]}+=(?:{$value}+(?:{sep}{name[]}+={$value}*))*
+ * | 5 | {?list*} ?list[]=red&list[]=green&list[]=blue | {name[]}+=(?:{$value}+(?:{sep}{name[]}+={$value}*))*.
*/
class Named extends Abstraction
{
- public function toRegex(Parser $parser, Node\Variable $var)
+ public function toRegex(Parser $parser, Node\Variable $var): string
{
- $regex = null;
$name = $var->name;
$value = $this->getRegex();
$options = $var->options;
if ($options['modifier']) {
- switch($options['modifier']) {
+ switch ($options['modifier']) {
case '*':
// 2 | 4
$regex = "{$name}+=(?:{$value}+(?:{$this->sep}{$name}+={$value}*)*)"
. "|{$value}+=(?:{$value}+(?:{$this->sep}{$value}+={$value}*)*)";
+
break;
+
case ':':
- $regex = "{$value}\{0,{$options['value']}\}";
+ $regex = "{$value}\\{0,{$options['value']}\\}";
+
break;
case '%':
// 5
- $name = $name.'+(?:%5B|\[)[^=]*=';
+ $name .= '+(?:%5B|\[)[^=]*=';
$regex = "{$name}(?:{$value}+(?:{$this->sep}{$name}{$value}*)*)";
+
break;
+
default:
- throw new \Exception("Unknown modifier `{$options['modifier']}`");
+ throw new \InvalidArgumentException("Unknown modifier `{$options['modifier']}`");
}
- }
-
- else {
+ } else {
// 1, 3
$regex = "{$name}=(?:{$value}+(?:,{$value}+)*)*";
}
- return '(?:&)?'.$regex;
+ return '(?:&)?' . $regex;
}
- public function expandString(Parser $parser, Node\Variable $var, $val)
+ public function expandString(Parser $parser, Node\Variable $var, $val): string
{
- $val = (string)$val;
+ $val = (string) $val;
$options = $var->options;
$result = $this->encode($parser, $var, $var->name);
@@ -61,61 +63,42 @@ public function expandString(Parser $parser, Node\Variable $var, $val)
return $result . $this->empty;
}
- else {
- $result .= '=';
- }
+ $result .= '=';
if ($options['modifier'] === ':') {
- $val = mb_substr($val, 0, (int)$options['value']);
+ $val = mb_substr($val, 0, (int) $options['value']);
}
- return $result.$this->encode($parser, $var, $val);
+ return $result . $this->encode($parser, $var, $val);
}
- public function expandNonExplode(Parser $parser, Node\Variable $var, array $val)
+ public function expandNonExplode(Parser $parser, Node\Variable $var, array $val): ?string
{
if (empty($val)) {
return null;
}
- $result = $this->encode($parser, $var, $var->name);
-
- if (empty($val)) {
- return $result . $this->empty;
- }
+ $result = $this->encode($parser, $var, $var->name);
- else {
- $result .= '=';
- }
+ $result .= '=';
- return $result.$this->encode($parser, $var, $val);
+ return $result . $this->encode($parser, $var, $val);
}
- public function expandExplode(Parser $parser, Node\Variable $var, array $val)
+ public function expandExplode(Parser $parser, Node\Variable $var, array $val): ?string
{
if (empty($val)) {
return null;
}
- $result = $this->encode($parser, $var, $var->name);
-
- // RFC6570 doesn't specify how to handle empty list/assoc array
- // for explode modifier
- if (empty($val)) {
- return $result . $this->empty;
- }
-
- $list = isset($val[0]);
- $data = array();
- foreach($val as $k => $v) {
-
+ $list = isset($val[0]);
+ $data = [];
+ foreach ($val as $k => $v) {
// if value is a list, use `varname` as keyname, otherwise use `key` name
$key = $list ? $var->name : $k;
if ($list) {
$data[$key][] = $v;
- }
-
- else {
+ } else {
$data[$key] = $v;
}
}
@@ -123,14 +106,14 @@ public function expandExplode(Parser $parser, Node\Variable $var, array $val)
// if it's array modifier, we have to use variable name as index
// e.g. if variable name is 'query' and value is ['limit' => 1]
// then we convert it to ['query' => ['limit' => 1]]
- if (!$list and $var->options['modifier'] === '%') {
- $data = array($var->name => $data);
+ if (!$list && $var->options['modifier'] === '%') {
+ $data = [$var->name => $data];
}
- return $this->encodeExplodeVars($parser, $var, $data);
+ return $this->encodeExplodeVars($var, $data);
}
- public function extract(Parser $parser, Node\Variable $var, $data)
+ public function extract(Parser $parser, Node\Variable $var, $data): array|string
{
// get rid of optional `&` at the beginning
if ($data[0] === '&') {
@@ -143,45 +126,47 @@ public function extract(Parser $parser, Node\Variable $var, $data)
switch ($options['modifier']) {
case '%':
- parse_str($data, $query);
+ parse_str($value, $query);
return $query[$var->name];
case '*':
- $data = array();
+ $value = [];
- foreach($vals as $val) {
- list($k, $v) = explode('=', $val);
+ foreach ($vals as $val) {
+ [$k, $v] = explode('=', $val);
// 2
if ($k === $var->getToken()) {
- $data[] = $v;
+ $value[] = $v;
}
// 4
else {
- $data[$k] = $v;
+ $value[$k] = $v;
}
}
break;
+
case ':':
break;
+
default:
// 1, 3
// remove key from value e.g. 'lang=en,th' becomes 'en,th'
- $value = str_replace($var->getToken().'=', '', $value);
- $data = explode(',', $value);
+ $value = str_replace($var->getToken() . '=', '', $value);
+ $value = explode(',', $value);
- if (sizeof($data) === 1) {
- $data = current($data);
+ if (count($value) === 1) {
+ $value = current($value);
}
}
- return $this->decode($parser, $var, $data);
+ return $this->decode($parser, $var, $value);
}
- public function encodeExplodeVars(Parser $parser, Node\Variable $var, $data)
+ public function encodeExplodeVars(Node\Variable $var, $data): null|array|string
{
// http_build_query uses PHP_QUERY_RFC1738 encoding by default
// i.e. spaces are encoded as '+' (plus signs) we need to convert
@@ -198,7 +183,6 @@ public function encodeExplodeVars(Parser $parser, Node\Variable $var, $data)
// `:`, `*` modifiers
else {
-
// by default, http_build_query will convert array values to `a[]=1&a[]=2`
// which is different from the spec. It should be `a=1&a=2`
$query = preg_replace('#%5B\d+%5D#', '', $query);
@@ -206,11 +190,10 @@ public function encodeExplodeVars(Parser $parser, Node\Variable $var, $data)
// handle reserved charset
if ($this->reserved) {
-
$query = str_replace(
array_keys(static::$reserved_chars),
static::$reserved_chars,
- $query
+ $query,
);
}
diff --git a/src/Rize/UriTemplate/Operator/UnNamed.php b/src/Rize/UriTemplate/Operator/UnNamed.php
index 14a49b2..d1e53dd 100644
--- a/src/Rize/UriTemplate/Operator/UnNamed.php
+++ b/src/Rize/UriTemplate/Operator/UnNamed.php
@@ -9,37 +9,39 @@
* | 1 | {/list} /red,green,blue | {$value}*(?:,{$value}+)*
* | 2 | {/list*} /red/green/blue | {$value}+(?:{$sep}{$value}+)*
* | 3 | {/keys} /semi,%3B,dot,.,comma,%2C | /(\w+,?)+
- * | 4 | {/keys*} /semi=%3B/dot=./comma=%2C | /(?:\w+=\w+/?)*
+ * | 4 | {/keys*} /semi=%3B/dot=./comma=%2C | /(?:\w+=\w+/?)*.
*/
class UnNamed extends Abstraction
{
- public function toRegex(Parser $parser, Node\Variable $var)
+ public function toRegex(Parser $parser, Node\Variable $var): string
{
- $regex = null;
$value = $this->getRegex();
$options = $var->options;
if ($options['modifier']) {
- switch($options['modifier']) {
+ switch ($options['modifier']) {
case '*':
// 2 | 4
$regex = "{$value}+(?:{$this->sep}{$value}+)*";
+
break;
+
case ':':
- $regex = $value.'{0,'.$options['value'].'}';
+ $regex = $value . '{0,' . $options['value'] . '}';
+
break;
+
case '%':
- throw new \Exception("% (array) modifier only works with Named type operators e.g. ;,?,&");
+ throw new \InvalidArgumentException('% (array) modifier only works with Named type operators e.g. ;,?,&');
+
default:
- throw new \Exception("Unknown modifier `{$options['modifier']}`");
+ throw new \InvalidArgumentException("Unknown modifier `{$options['modifier']}`");
}
- }
-
- else {
+ } else {
// 1, 3
$regex = "{$value}*(?:,{$value}+)*";
}
return $regex;
}
-}
\ No newline at end of file
+}
diff --git a/src/Rize/UriTemplate/Parser.php b/src/Rize/UriTemplate/Parser.php
index 10ca382..48e8f17 100644
--- a/src/Rize/UriTemplate/Parser.php
+++ b/src/Rize/UriTemplate/Parser.php
@@ -2,34 +2,33 @@
namespace Rize\UriTemplate;
-use Rize\UriTemplate\Node;
+use Rize\UriTemplate\Node\Abstraction;
use Rize\UriTemplate\Node\Expression;
-use Rize\UriTemplate\Operator;
+use Rize\UriTemplate\Node\Variable;
use Rize\UriTemplate\Operator\UnNamed;
class Parser
{
- const REGEX_VARNAME = '(?:[A-z0-9_\.]|%[0-9a-fA-F]{2})';
+ private const REGEX_VARNAME = '[A-z0-9.]|%[0-9a-fA-F]{2}';
/**
- * Parses URI Template and returns nodes
+ * Parses URI Template and returns nodes.
*
- * @param string $template
* @return Node\Abstraction[]
*/
- public function parse($template)
+ public function parse(string $template): array
{
- $parts = preg_split('#(\{[^\}]+\})#', $template, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
- $nodes = array();
+ $parts = preg_split('#(\{[^}]+})#', $template, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+ $nodes = [];
- foreach($parts as $part) {
+ foreach ($parts as $part) {
$node = $this->createNode($part);
// if current node has dot separator that requires a forward lookup
// for the previous node iff previous node's operator is UnNamed
if ($node instanceof Expression && $node->getOperator()->id === '.') {
- if (sizeof($nodes) > 0) {
- $previousNode = $nodes[sizeof($nodes) - 1];
+ if (count($nodes) > 0) {
+ $previousNode = $nodes[count($nodes) - 1];
if ($previousNode instanceof Expression && $previousNode->getOperator() instanceof UnNamed) {
$previousNode->setForwardLookupSeparator($node->getOperator()->id);
}
@@ -42,11 +41,7 @@ public function parse($template)
return $nodes;
}
- /**
- * @param string $token
- * @return Node\Abstraction
- */
- protected function createNode($token)
+ protected function createNode(string $token): Abstraction
{
// literal string
if ($token[0] !== '{') {
@@ -59,17 +54,16 @@ protected function createNode($token)
return $node;
}
- protected function parseExpression($expression)
+ protected function parseExpression(string $expression): Expression
{
$token = $expression;
$prefix = $token[0];
// not a valid operator?
if (!Operator\Abstraction::isValid($prefix)) {
-
// not valid chars?
- if (!preg_match('#'.self::REGEX_VARNAME.'#', $token)) {
- throw new \Exception("Invalid operator [$prefix] found at {$token}");
+ if (!preg_match('#' . self::REGEX_VARNAME . '#', $token)) {
+ throw new \InvalidArgumentException("Invalid operator [{$prefix}] found at {$token}");
}
// default operator
@@ -82,74 +76,71 @@ protected function parseExpression($expression)
}
// parse variables
- $vars = array();
- foreach(explode(',', $token) as $var) {
+ $vars = [];
+ foreach (explode(',', $token) as $var) {
$vars[] = $this->parseVariable($var);
}
return $this->createExpressionNode(
$token,
$this->createOperatorNode($prefix),
- $vars
+ $vars,
);
}
- protected function parseVariable($var)
+ protected function parseVariable(string $var): Variable
{
$var = trim($var);
$val = null;
$modifier = null;
// check for prefix (:) / explode (*) / array (%) modifier
- if (strpos($var, ':') !== false) {
+ if (str_contains($var, ':')) {
$modifier = ':';
- list($varname, $val) = explode(':', $var);
+ [$varname, $val] = explode(':', $var);
// error checking
if (!is_numeric($val)) {
- throw new \Exception("Value for `:` modifier must be numeric value [$varname:$val]");
+ throw new \InvalidArgumentException("Value for `:` modifier must be numeric value [{$varname}:{$val}]");
}
}
- switch($last = substr($var, -1)) {
+ switch ($last = substr($var, -1)) {
case '*':
case '%':
-
// there can be only 1 modifier per var
if ($modifier) {
- throw new \Exception("Multiple modifiers per variable are not allowed [$var]");
+ throw new \InvalidArgumentException("Multiple modifiers per variable are not allowed [{$var}]");
}
$modifier = $last;
- $var = substr($var, 0, -1);
+ $var = substr($var, 0, -1);
+
break;
}
return $this->createVariableNode(
$var,
- array(
- 'modifier' => $modifier,
- 'value' => $val,
- )
+ ['modifier' => $modifier, 'value' => $val],
);
}
- protected function createVariableNode($token, $options = array())
+ protected function createVariableNode($token, $options = []): Variable
{
- return new Node\Variable($token, $options);
+ return new Variable($token, $options);
}
- protected function createExpressionNode($token, Operator\Abstraction $operator = null, array $vars = array())
+ protected function createExpressionNode($token, ?Operator\Abstraction $operator = null, array $vars = []): Expression
{
- return new Node\Expression($token, $operator, $vars);
+ return new Expression($token, $operator, $vars);
}
- protected function createLiteralNode($token)
+ protected function createLiteralNode(string $token): Node\Literal
{
return new Node\Literal($token);
}
- protected function createOperatorNode($token)
+ protected function createOperatorNode($token): Operator\Abstraction
{
return Operator\Abstraction::createById($token);
}
diff --git a/src/Rize/UriTemplate/UriTemplate.php b/src/Rize/UriTemplate/UriTemplate.php
index 983d5b5..2170bfc 100644
--- a/src/Rize/UriTemplate/UriTemplate.php
+++ b/src/Rize/UriTemplate/UriTemplate.php
@@ -5,8 +5,6 @@
use Rize\UriTemplate as Template;
/**
- * Future compatibility
+ * Future compatibility.
*/
-class UriTemplate extends Template
-{
-}
+class UriTemplate extends Template {}
diff --git a/test/Rize/Uri/Node/ParserTest.php b/test/Rize/Uri/Node/ParserTest.php
deleted file mode 100644
index e6d1f1a..0000000
--- a/test/Rize/Uri/Node/ParserTest.php
+++ /dev/null
@@ -1,131 +0,0 @@
- ':',
- 'value' => 1,
- )
- ),
- )
- ),
- new Node\Literal('/'),
- new Node\Expression(
- 'term',
- Operator\Abstraction::createById(''),
- array(
- new Node\Variable(
- 'term',
- array(
- 'modifier' => null,
- 'value' => null,
- )
- ),
- )
- ),
- new Node\Literal('/'),
- new Node\Expression(
- 'test*',
- Operator\Abstraction::createById(''),
- array(
- new Node\Variable(
- 'test',
- array(
- 'modifier' => '*',
- 'value' => null,
- )
- ),
- )
- ),
- new Node\Literal('/foo'),
- new Node\Expression(
- 'query,number',
- Operator\Abstraction::createById('?'),
- array(
- new Node\Variable(
- 'query',
- array(
- 'modifier' => null,
- 'value' => null,
- )
- ),
- new Node\Variable(
- 'number',
- array(
- 'modifier' => null,
- 'value' => null,
- )
- ),
- )
- ),
- );
-
- $service = $this->service();
- $actual = $service->parse($input);
-
- $this->assertEquals($expected, $actual);
- }
-
- public function testParseTemplateWithLiteral()
- {
- // will pass
- $uri = new UriTemplate('http://www.example.com/v1/company/', array());
- $params = $uri->extract('/{countryCode}/{registrationNumber}/test{.format}', '/gb/0123456/test.json');
- static::assertEquals(array('countryCode' => 'gb', 'registrationNumber' => '0123456', 'format' => 'json'), $params);
- }
-
- /**
- * @depends testParseTemplateWithLiteral
- */
- public function testParseTemplateWithTwoVariablesAndDotBetween()
- {
- // will fail
- $uri = new UriTemplate('http://www.example.com/v1/company/', array());
- $params = $uri->extract('/{countryCode}/{registrationNumber}{.format}', '/gb/0123456.json');
- static::assertEquals(array('countryCode' => 'gb', 'registrationNumber' => '0123456', 'format' => 'json'), $params);
- }
-
- /**
- * @depends testParseTemplateWithLiteral
- */
- public function testParseTemplateWithTwoVariablesAndDotBetweenStrict()
- {
- // will fail
- $uri = new UriTemplate('http://www.example.com/v1/company/', array());
- $params = $uri->extract('/{countryCode}/{registrationNumber}{.format}', '/gb/0123456.json', true);
- static::assertEquals(array('countryCode' => 'gb', 'registrationNumber' => '0123456', 'format' => 'json'), $params);
- }
-
- /**
- * @depends testParseTemplateWithLiteral
- */
- public function testParseTemplateWithThreeVariablesAndDotBetweenStrict()
- {
- // will fail
- $uri = new UriTemplate('http://www.example.com/v1/company/', array());
- $params = $uri->extract('/{countryCode}/{registrationNumber}{.namespace}{.format}', '/gb/0123456.company.json');
- static::assertEquals(array('countryCode' => 'gb', 'registrationNumber' => '0123456', 'namespace' => 'company', 'format' => 'json'), $params);
- }
-}
diff --git a/test/Rize/UriTemplateTest.php b/test/Rize/UriTemplateTest.php
deleted file mode 100644
index d57a226..0000000
--- a/test/Rize/UriTemplateTest.php
+++ /dev/null
@@ -1,688 +0,0 @@
- array("one", "two", "three"),
- 'dom' => array("example", "com"),
- 'dub' => "me/too",
- 'hello' => "Hello World!",
- 'half' => "50%",
- 'var' => "value",
- 'who' => "fred",
- 'base' => "http://example.com/home/",
- 'path' => "/foo/bar",
- 'list' => array("red", "green", "blue"),
- 'keys' => array(
- "semi" => ";",
- "dot" => ".",
- "comma" => ",",
- ),
- 'list_with_empty' => array(''),
- 'keys_with_empty' => array('john' => ''),
- 'v' => "6",
- 'x' => "1024",
- 'y' => "768",
- 'empty' => "",
- 'empty_keys' => array(),
- 'undef' => null,
- );
-
- return array(
-
- array(
- 'http://example.com/~john',
- array(
- 'uri' => 'http://example.com/~{username}',
- 'params' => array(
- 'username' => 'john',
- ),
- ),
- ),
-
- array(
- 'http://example.com/dictionary/d/dog',
- array(
- 'uri' => 'http://example.com/dictionary/{term:1}/{term}',
- 'params' => array(
- 'term' => 'dog',
- ),
- 'extract' => array(
- 'term:1' => 'd',
- 'term' => 'dog',
- ),
- ),
- ),
-
- # Form-style parameters expression
- array(
- 'http://example.com/j/john/search?q=mycelium&q=3&lang=th,jp,en',
- array(
- 'uri' => 'http://example.com/{term:1}/{term}/search{?q*,lang}',
- 'params' => array(
- 'q' => array('mycelium', 3),
- 'lang' => array('th', 'jp', 'en'),
- 'term' => 'john',
- ),
- ),
- ),
-
- array(
- 'http://www.example.com/john',
- array(
- 'uri' => 'http://www.example.com/{username}',
- 'params' => array(
- 'username' => 'john',
- ),
- ),
- ),
-
- array(
- 'http://www.example.com/foo?query=mycelium&number=100',
- array(
- 'uri' => 'http://www.example.com/foo{?query,number}',
- 'params' => array(
- 'query' => 'mycelium',
- 'number' => 100,
- ),
- ),
- ),
-
- # 'query' is undefined
- array(
- 'http://www.example.com/foo?number=100',
- array(
- 'uri' => 'http://www.example.com/foo{?query,number}',
- 'params' => array(
- 'number' => 100,
- ),
- # we can't extract undefined values
- 'extract' => false,
- ),
- ),
-
- # undefined variables
- array(
- 'http://www.example.com/foo',
- array(
- 'uri' => 'http://www.example.com/foo{?query,number}',
- 'params' => array(),
- 'extract' => array('query' => null, 'number' => null),
- ),
- ),
-
- array(
- 'http://www.example.com/foo',
- array(
- 'uri' => 'http://www.example.com/foo{?number}',
- 'params' => array(),
- 'extract' => array('number' => null),
- ),
- ),
-
- array(
- 'one,two,three|one,two,three|/one,two,three|/one/two/three|;count=one,two,three|;count=one;count=two;count=three|?count=one,two,three|?count=one&count=two&count=three|&count=one&count=two&count=three',
- array(
- 'uri' => '{count}|{count*}|{/count}|{/count*}|{;count}|{;count*}|{?count}|{?count*}|{&count*}',
- 'params' => array(
- 'count' => array('one', 'two', 'three'),
- ),
- ),
- ),
-
- array(
- 'http://www.host.com/path/to/a/file.x.y',
- array(
- 'uri' => 'http://{host}{/segments*}/{file}{.extensions*}',
- 'params' => array(
- 'host' => 'www.host.com',
- 'segments' => array('path', 'to', 'a'),
- 'file' => 'file',
- 'extensions' => array('x', 'y'),
- ),
- 'extract' => array(
- 'host' => 'www.host.com',
- 'segments' => array('path', 'to', 'a'),
- 'file' => 'file.x.y',
- 'extensions' => null,
- ),
- ),
- ),
-
- # level 1 - Simple String Expansion: {var}
- array(
- 'value|Hello%20World%21|50%25|OX|OX|1024,768|1024,Hello%20World%21,768|?1024,|?1024|?768|val|value|red,green,blue|semi,%3B,dot,.,comma,%2C|semi=%3B,dot=.,comma=%2C',
- array(
- 'uri' => '{var}|{hello}|{half}|O{empty}X|O{undef}X|{x,y}|{x,hello,y}|?{x,empty}|?{x,undef}|?{undef,y}|{var:3}|{var:30}|{list}|{keys}|{keys*}',
- 'params' => $params,
- ),
- ),
-
- # level 2 - Reserved Expansion: {+var}
- array(
- 'value|Hello%20World!|50%25|http%3A%2F%2Fexample.com%2Fhome%2Findex|http://example.com/home/index|OX|OX|/foo/bar/here|here?ref=/foo/bar|up/foo/barvalue/here|1024,Hello%20World!,768|/foo/bar,1024/here|/foo/b/here|red,green,blue|red,green,blue|semi,;,dot,.,comma,,|semi=;,dot=.,comma=,',
- array(
- 'uri' => '{+var}|{+hello}|{+half}|{base}index|{+base}index|O{+empty}X|O{+undef}X|{+path}/here|here?ref={+path}|up{+path}{var}/here|{+x,hello,y}|{+path,x}/here|{+path:6}/here|{+list}|{+list*}|{+keys}|{+keys*}',
- 'params' => $params,
- ),
- ),
-
- # level 2 - Fragment Expansion: {#var}
- array(
- '#value|#Hello%20World!|#50%25|foo#|foo|#1024,Hello%20World!,768|#/foo/bar,1024/here|#/foo/b/here|#red,green,blue|#red,green,blue|#semi,;,dot,.,comma,,|#semi=;,dot=.,comma=,',
- array(
- 'uri' => '{#var}|{#hello}|{#half}|foo{#empty}|foo{#undef}|{#x,hello,y}|{#path,x}/here|{#path:6}/here|{#list}|{#list*}|{#keys}|{#keys*}',
- 'params' => $params,
- ),
- ),
-
- # Label Expansion with Dot-Prefix: {.var}
- array(
- '.fred|.fred.fred|.50%25.fred|www.example.com|X.value|X.|X|X.val|X.red,green,blue|X.red.green.blue|X.semi,%3B,dot,.,comma,%2C|X.semi=%3B.dot=..comma=%2C|X|X',
- array(
- 'uri' => '{.who}|{.who,who}|{.half,who}|www{.dom*}|X{.var}|X{.empty}|X{.undef}|X{.var:3}|X{.list}|X{.list*}|X{.keys}|X{.keys*}|X{.empty_keys}|X{.empty_keys*}',
- 'params' => $params,
- ),
- ),
-
- # Path Segment Expansion: {/var}
- array(
- '/fred|/fred/fred|/50%25/fred|/fred/me%2Ftoo|/value|/value/|/value|/value/1024/here|/v/value|/red,green,blue|/red/green/blue|/red/green/blue/%2Ffoo|/semi,%3B,dot,.,comma,%2C|/semi=%3B/dot=./comma=%2C',
- array(
- 'uri' => '{/who}|{/who,who}|{/half,who}|{/who,dub}|{/var}|{/var,empty}|{/var,undef}|{/var,x}/here|{/var:1,var}|{/list}|{/list*}|{/list*,path:4}|{/keys}|{/keys*}',
- 'params' => $params,
- ),
- ),
-
- # Path-Style Parameter Expansion: {;var}
- array(
- ';who=fred|;half=50%25|;empty|;v=6;empty;who=fred|;v=6;who=fred|;x=1024;y=768|;x=1024;y=768;empty|;x=1024;y=768|;hello=Hello|;list=red,green,blue|;list=red;list=green;list=blue|;keys=semi,%3B,dot,.,comma,%2C|;semi=%3B;dot=.;comma=%2C',
- array(
- 'uri' => '{;who}|{;half}|{;empty}|{;v,empty,who}|{;v,bar,who}|{;x,y}|{;x,y,empty}|{;x,y,undef}|{;hello:5}|{;list}|{;list*}|{;keys}|{;keys*}',
- 'params' => $params,
- ),
- ),
-
- # Form-Style Query Expansion: {?var}
- array(
- '?who=fred|?half=50%25|?x=1024&y=768|?x=1024&y=768&empty=|?x=1024&y=768|?var=val|?list=red,green,blue|?list=red&list=green&list=blue|?keys=semi,%3B,dot,.,comma,%2C|?semi=%3B&dot=.&comma=%2C|?list_with_empty=|?john=',
- array(
- 'uri' => '{?who}|{?half}|{?x,y}|{?x,y,empty}|{?x,y,undef}|{?var:3}|{?list}|{?list*}|{?keys}|{?keys*}|{?list_with_empty*}|{?keys_with_empty*}',
- 'params' => $params,
- ),
- ),
-
- # Form-Style Query Continuation: {&var}
- array(
- '&who=fred|&half=50%25|?fixed=yes&x=1024|&x=1024&y=768&empty=|&x=1024&y=768|&var=val|&list=red,green,blue|&list=red&list=green&list=blue|&keys=semi,%3B,dot,.,comma,%2C|&semi=%3B&dot=.&comma=%2C',
- array(
- 'uri' => '{&who}|{&half}|?fixed=yes{&x}|{&x,y,empty}|{&x,y,undef}|{&var:3}|{&list}|{&list*}|{&keys}|{&keys*}',
- 'params' => $params,
- ),
- ),
-
- # Test empty values
- array(
- '|||',
- array(
- 'uri' => '{empty}|{empty*}|{?empty}|{?empty*}',
- 'params' => array(
- 'empty' => array(),
- ),
- ),
- ),
- );
- }
-
- public static function dataExpandWithArrayModifier()
- {
- return array(
- # List
- array(
- # '?choices[]=a&choices[]=b&choices[]=c',
- '?choices%5B%5D=a&choices%5B%5D=b&choices%5B%5D=c',
- array(
- 'uri' => '{?choices%}',
- 'params' => array(
- 'choices' => array('a', 'b', 'c'),
- ),
- ),
- ),
-
- # Keys
- array(
- # '?choices[a]=1&choices[b]=2&choices[c][test]=3',
- '?choices%5Ba%5D=1&choices%5Bb%5D=2&choices%5Bc%5D%5Btest%5D=3',
- array(
- 'uri' => '{?choices%}',
- 'params' => array(
- 'choices' => array(
- 'a' => 1,
- 'b' => 2,
- 'c' => array(
- 'test' => 3,
- ),
- ),
- ),
- ),
- ),
-
- # Mixed
- array(
- # '?list[]=a&list[]=b&keys[a]=1&keys[b]=2',
- '?list%5B%5D=a&list%5B%5D=b&keys%5Ba%5D=1&keys%5Bb%5D=2',
- array(
- 'uri' => '{?list%,keys%}',
- 'params' => array(
- 'list' => array(
- 'a', 'b',
- ),
- 'keys' => array(
- 'a' => 1,
- 'b' => 2,
- ),
- ),
- ),
- ),
- );
- }
-
- public static function dataBaseTemplate()
- {
- return array(
- array(
- 'http://google.com/api/1/users/1',
- # base uri
- array(
- 'uri' => '{+host}/api/{v}',
- 'params' => array(
- 'host' => 'http://google.com',
- 'v' => 1,
- ),
- ),
- # other uri
- array(
- 'uri' => '/{resource}/{id}',
- 'params' => array(
- 'resource' => 'users',
- 'id' => 1,
- ),
- ),
- ),
-
- # test override base params
- array(
- 'http://github.com/api/1/users/1',
- # base uri
- array(
- 'uri' => '{+host}/api/{v}',
- 'params' => array(
- 'host' => 'http://google.com',
- 'v' => 1,
- ),
- ),
- # other uri
- array(
- 'uri' => '/{resource}/{id}',
- 'params' => array(
- 'host' => 'http://github.com',
- 'resource' => 'users',
- 'id' => 1,
- ),
- ),
- ),
- );
- }
-
- public static function dataExtraction()
- {
- return array(
- array(
- '/no/{term:1}/random/foo{?query,list%,keys%}',
- '/no/j/random/foo?query=1,2,3&list%5B%5D=a&list%5B%5D=b&keys%5Ba%5D=1&keys%5Bb%5D=2&keys%5Bc%5D%5Btest%5D%5Btest%5D=1',
- array(
- 'term:1' => 'j',
- 'query' => array(1, 2, 3),
- 'list' => array(
- 'a', 'b',
- ),
- 'keys' => array(
- 'a' => 1,
- 'b' => 2,
- 'c' => array(
- 'test' => array(
- 'test' => 1,
- ),
- ),
- ),
- ),
- ),
- array(
- '/no/{term:1}/random/{term}/{test*}/foo{?query,number}',
- '/no/j/random/john/a,b,c/foo?query=1,2,3&number=10',
- array(
- 'term:1' => 'j',
- 'term' => 'john',
- 'test' => array('a', 'b', 'c'),
- 'query' => array(1, 2, 3),
- 'number' => 10,
- ),
- ),
- array(
- '/search/{term:1}/{term}/{?q*,limit}',
- '/search/j/john/?a=1&b=2&limit=10',
- array(
- 'term:1' => 'j',
- 'term' => 'john',
- 'q' => array('a' => 1, 'b' => 2),
- 'limit' => 10,
- ),
- ),
- array(
- 'http://www.example.com/foo{?query,number}',
- 'http://www.example.com/foo?query=5',
- array(
- 'query' => 5,
- 'number' => null,
- ),
- ),
- array(
- '{count}|{count*}|{/count}|{/count*}|{;count}|{;count*}|{?count}|{?count*}|{&count*}',
- 'one,two,three|one,two,three|/one,two,three|/one/two/three|;count=one,two,three|;count=one;count=two;count=three|?count=one,two,three|?count=one&count=two&count=three|&count=one&count=two&count=three',
- array(
- 'count' => array('one', 'two', 'three'),
- ),
- ),
- array(
- 'http://example.com/{term:1}/{term}/search{?q*,lang}',
- 'http://example.com/j/john/search?q=Hello%20World%21&q=3&lang=th,jp,en',
- array(
- 'q' => array('Hello World!', 3),
- 'lang' => array('th', 'jp', 'en'),
- 'term' => 'john',
- 'term:1' => 'j',
- ),
- ),
- array(
- '/foo/bar/{number}',
- '/foo/bar/0',
- array(
- 'number' => 0,
- ),
- ),
- array(
- '/some/{path}{?ref}',
- '/some/foo',
- array(
- 'path' => 'foo',
- 'ref' => null,
- ),
- ),
- );
- }
-
- /**
- * @dataProvider dataExpansion
- */
- public function testExpansion($expected, $input)
- {
- $service = $this->service();
- $result = $service->expand($input['uri'], $input['params']);
-
- $this->assertEquals($expected, $result);
- }
-
- /**
- * @dataProvider dataExpandWithArrayModifier
- */
- public function testExpandWithArrayModifier($expected, $input)
- {
- $service = $this->service();
- $result = $service->expand($input['uri'], $input['params']);
-
- $this->assertEquals($expected, $result);
- }
-
- /**
- * @dataProvider dataBaseTemplate
- */
- public function testBaseTemplate($expected, $base, $other)
- {
- $service = $this->service($base['uri'], $base['params']);
- $result = $service->expand($other['uri'], $other['params']);
-
- $this->assertEquals($expected, $result);
- }
-
- /**
- * @dataProvider dataExtraction
- */
- public function testExtract($template, $uri, $expected)
- {
- $service = $this->service();
- $actual = $service->extract($template, $uri);
-
- $this->assertEquals($expected, $actual);
- }
-
- public function testExpandFromFixture()
- {
- $dir = dirname(__DIR__).DIRECTORY_SEPARATOR.'fixtures'.DIRECTORY_SEPARATOR;
- $files = array('spec-examples.json', 'spec-examples-by-section.json', 'extended-tests.json');
- $service = $this->service();
-
- foreach($files as $file) {
- $content = json_decode(file_get_contents($dir.$file), $array = true);
-
- # iterate through each fixture
- foreach($content as $fixture) {
- $vars = $fixture['variables'];
-
- # assert each test cases
- foreach($fixture['testcases'] as $case) {
- list($uri, $expected) = $case;
-
- $actual = $service->expand($uri, $vars);
-
- if (is_array($expected)) {
- $expected = current(array_filter($expected, function($input) use ($actual) {
- return $actual === $input;
- }));
- }
-
- $this->assertEquals($expected, $actual);
- }
- }
- }
- }
-
- public static function dataExtractStrictMode()
- {
- $dataTest = array(
-
- array(
- '/search/{term:1}/{term}/{?q*,limit}',
- '/search/j/john/?a=1&b=2&limit=10',
- array(
- 'term:1' => 'j',
- 'term' => 'john',
- 'limit' => '10',
- 'q' => array(
- 'a' => '1',
- 'b' => '2',
- ),
- ),
- ),
- array(
- 'http://example.com/{term:1}/{term}/search{?q*,lang}',
- 'http://example.com/j/john/search?q=Hello%20World%21&q=3&lang=th,jp,en',
- array(
- 'term:1' => 'j',
- 'term' => 'john',
- 'lang' => array(
- 'th',
- 'jp',
- 'en',
- ),
- 'q' => array(
- 'Hello World!',
- '3',
- ),
- ),
- ),
- array(
- '/foo/bar/{number}',
- '/foo/bar/0',
- array(
- 'number' => 0,
- ),
- ),
- array(
- '/',
- '/',
- array(),
- ),
- );
-
- $rfc3986AllowedPathCharacters = array(
- '-', '.', '_', '~', '!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '=', ':', '@',
- );
-
- foreach ($rfc3986AllowedPathCharacters as $char) {
- $title = "RFC3986 path character ($char)";
- $title = str_replace("'", 'single quote', $title); // PhpStorm workaround
- if ($char === ',') { // , means array on RFC6570
- $params = array(
- 'term' => array(
- 'foo',
- 'baz',
- ),
- );
- } else {
- $params = array(
- 'term' => "foo{$char}baz",
- );
- }
-
- $data = array(
- '/search/{term}',
- "/search/foo{$char}baz",
- $params,
- );
-
- $dataTest[$title] = $data;
- $data = array(
- '/search/{;term}',
- "/search/;term=foo{$char}baz",
- $params,
- );
- $dataTest['Named ' . $title] = $data;
- }
-
- $rfc3986AllowedQueryCharacters = $rfc3986AllowedPathCharacters;
- $rfc3986AllowedQueryCharacters[] = '/';
- $rfc3986AllowedQueryCharacters[] = '?';
- unset($rfc3986AllowedQueryCharacters[array_search('&', $rfc3986AllowedQueryCharacters, true)]);
-
- foreach ($rfc3986AllowedQueryCharacters as $char) {
- $title = "RFC3986 query character ($char)";
- $title = str_replace("'", 'single quote', $title); // PhpStorm workaround
- if ($char === ',') { // , means array on RFC6570
- $params = array(
- 'term' => array(
- 'foo',
- 'baz',
- ),
- );
- } else {
- $params = array(
- 'term' => "foo{$char}baz",
- );
- }
-
- $data = array(
- '/search/{?term}',
- "/search/?term=foo{$char}baz",
- $params,
- );
- $dataTest['Named ' . $title] = $data;
- }
-
- return $dataTest;
- }
-
- public static function extractStrictModeNotMatchProvider()
- {
- return array(
- array(
- '/',
- '/a',
- ),
- array(
- '/{test}',
- '/a/',
- ),
- array(
- '/search/{term:1}/{term}/{?q*,limit}',
- '/search/j/?a=1&b=2&limit=10',
- ),
- array(
- 'http://www.example.com/foo{?query,number}',
- 'http://www.example.com/foo?query=5',
- ),
- array(
- 'http://www.example.com/foo{?query,number}',
- 'http://www.example.com/foo',
- ),
- array(
- 'http://example.com/{term:1}/{term}/search{?q*,lang}',
- 'http://example.com/j/john/search?q=',
- ),
- );
- }
-
- /**
- * @dataProvider dataExtractStrictMode
- *
- * @param string $template
- * @param string $uri
- * @param array $expectedParams
- */
- public function testExtractStrictMode($template, $uri, array $expectedParams)
- {
- $service = $this->service();
- $params = $service->extract($template, $uri, true);
-
- $this->assertTrue(isset($params));
- $this->assertEquals($expectedParams, $params);
- }
-
- /**
- * @dataProvider extractStrictModeNotMatchProvider
- *
- * @param string $template
- * @param string $uri
- */
- public function testExtractStrictModeNotMatch($template, $uri)
- {
- $service = $this->service();
- $actual = $service->extract($template, $uri, true);
-
- $this->assertFalse(isset($actual));
- }
-}
diff --git a/tests/Rize/Uri/Node/ParserTest.php b/tests/Rize/Uri/Node/ParserTest.php
new file mode 100644
index 0000000..203d238
--- /dev/null
+++ b/tests/Rize/Uri/Node/ParserTest.php
@@ -0,0 +1,93 @@
+ ':', 'value' => 1],
+ )],
+ ), new Node\Literal('/'), new Node\Expression(
+ 'term',
+ Operator\Abstraction::createById(''),
+ [new Node\Variable(
+ 'term',
+ ['modifier' => null, 'value' => null],
+ )],
+ ), new Node\Literal('/'), new Node\Expression(
+ 'test*',
+ Operator\Abstraction::createById(''),
+ [new Node\Variable(
+ 'test',
+ ['modifier' => '*', 'value' => null],
+ )],
+ ), new Node\Literal('/foo'), new Node\Expression(
+ 'query,number',
+ Operator\Abstraction::createById('?'),
+ [new Node\Variable(
+ 'query',
+ ['modifier' => null, 'value' => null],
+ ), new Node\Variable(
+ 'number',
+ ['modifier' => null, 'value' => null],
+ )],
+ )];
+
+ $service = $this->service();
+ $actual = $service->parse($input);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testParseTemplateWithLiteral()
+ {
+ // will pass
+ $uri = new UriTemplate('http://www.example.com/v1/company/', []);
+ $params = $uri->extract('/{countryCode}/{registrationNumber}/test{.format}', '/gb/0123456/test.json');
+ static::assertEquals(['countryCode' => 'gb', 'registrationNumber' => '0123456', 'format' => 'json'], $params);
+ }
+
+ #[Depends('testParseTemplateWithLiteral')]
+ public function testParseTemplateWithTwoVariablesAndDotBetween()
+ {
+ // will fail
+ $uri = new UriTemplate('http://www.example.com/v1/company/', []);
+ $params = $uri->extract('/{countryCode}/{registrationNumber}{.format}', '/gb/0123456.json');
+ static::assertEquals(['countryCode' => 'gb', 'registrationNumber' => '0123456', 'format' => 'json'], $params);
+ }
+
+ #[Depends('testParseTemplateWithLiteral')]
+ public function testParseTemplateWithTwoVariablesAndDotBetweenStrict()
+ {
+ // will fail
+ $uri = new UriTemplate('http://www.example.com/v1/company/', []);
+ $params = $uri->extract('/{countryCode}/{registrationNumber}{.format}', '/gb/0123456.json', true);
+ static::assertEquals(['countryCode' => 'gb', 'registrationNumber' => '0123456', 'format' => 'json'], $params);
+ }
+
+ #[Depends('testParseTemplateWithLiteral')]
+ public function testParseTemplateWithThreeVariablesAndDotBetweenStrict()
+ {
+ // will fail
+ $uri = new UriTemplate('http://www.example.com/v1/company/', []);
+ $params = $uri->extract('/{countryCode}/{registrationNumber}{.namespace}{.format}', '/gb/0123456.company.json');
+ static::assertEquals(['countryCode' => 'gb', 'registrationNumber' => '0123456', 'namespace' => 'company', 'format' => 'json'], $params);
+ }
+}
diff --git a/tests/Rize/UriTemplateTest.php b/tests/Rize/UriTemplateTest.php
new file mode 100644
index 0000000..427f93f
--- /dev/null
+++ b/tests/Rize/UriTemplateTest.php
@@ -0,0 +1,253 @@
+ ["one", "two", "three"], 'dom' => ["example", "com"], 'dub' => "me/too", 'hello' => "Hello World!", 'half' => "50%", 'var' => "value", 'who' => "fred", 'base' => "http://example.com/home/", 'path' => "/foo/bar", 'list' => ["red", "green", "blue"], 'keys' => ["semi" => ";", "dot" => ".", "comma" => ","], 'list_with_empty' => [''], 'keys_with_empty' => ['john' => ''], 'v' => "6", 'x' => "1024", 'y' => "768", 'empty' => "", 'empty_keys' => [], 'undef' => null];
+
+ return [
+ ['http://example.com/~john', ['uri' => 'http://example.com/~{username}', 'params' => ['username' => 'john']]],
+ ['http://example.com/dictionary/d/dog', ['uri' => 'http://example.com/dictionary/{term:1}/{term}', 'params' => ['term' => 'dog'], 'extract' => ['term:1' => 'd', 'term' => 'dog']]],
+ # Form-style parameters expression
+ ['http://example.com/j/john/search?q=mycelium&q=3&lang=th,jp,en', ['uri' => 'http://example.com/{term:1}/{term}/search{?q*,lang}', 'params' => ['q' => ['mycelium', 3], 'lang' => ['th', 'jp', 'en'], 'term' => 'john']]],
+ ['http://www.example.com/john', ['uri' => 'http://www.example.com/{username}', 'params' => ['username' => 'john']]],
+ ['http://www.example.com/foo?query=mycelium&number=100', ['uri' => 'http://www.example.com/foo{?query,number}', 'params' => ['query' => 'mycelium', 'number' => 100]]],
+ # 'query' is undefined
+ ['http://www.example.com/foo?number=100', [
+ 'uri' => 'http://www.example.com/foo{?query,number}',
+ 'params' => ['number' => 100],
+ # we can't extract undefined values
+ 'extract' => false,
+ ]],
+ # undefined variables
+ ['http://www.example.com/foo', ['uri' => 'http://www.example.com/foo{?query,number}', 'params' => [], 'extract' => ['query' => null, 'number' => null]]],
+ ['http://www.example.com/foo', ['uri' => 'http://www.example.com/foo{?number}', 'params' => [], 'extract' => ['number' => null]]],
+ ['one,two,three|one,two,three|/one,two,three|/one/two/three|;count=one,two,three|;count=one;count=two;count=three|?count=one,two,three|?count=one&count=two&count=three|&count=one&count=two&count=three', ['uri' => '{count}|{count*}|{/count}|{/count*}|{;count}|{;count*}|{?count}|{?count*}|{&count*}', 'params' => ['count' => ['one', 'two', 'three']]]],
+ ['http://www.host.com/path/to/a/file.x.y', ['uri' => 'http://{host}{/segments*}/{file}{.extensions*}', 'params' => ['host' => 'www.host.com', 'segments' => ['path', 'to', 'a'], 'file' => 'file', 'extensions' => ['x', 'y']], 'extract' => ['host' => 'www.host.com', 'segments' => ['path', 'to', 'a'], 'file' => 'file.x.y', 'extensions' => null]]],
+ # level 1 - Simple String Expansion: {var}
+ ['value|Hello%20World%21|50%25|OX|OX|1024,768|1024,Hello%20World%21,768|?1024,|?1024|?768|val|value|red,green,blue|semi,%3B,dot,.,comma,%2C|semi=%3B,dot=.,comma=%2C', ['uri' => '{var}|{hello}|{half}|O{empty}X|O{undef}X|{x,y}|{x,hello,y}|?{x,empty}|?{x,undef}|?{undef,y}|{var:3}|{var:30}|{list}|{keys}|{keys*}', 'params' => $params]],
+ # level 2 - Reserved Expansion: {+var}
+ ['value|Hello%20World!|50%25|http%3A%2F%2Fexample.com%2Fhome%2Findex|http://example.com/home/index|OX|OX|/foo/bar/here|here?ref=/foo/bar|up/foo/barvalue/here|1024,Hello%20World!,768|/foo/bar,1024/here|/foo/b/here|red,green,blue|red,green,blue|semi,;,dot,.,comma,,|semi=;,dot=.,comma=,', ['uri' => '{+var}|{+hello}|{+half}|{base}index|{+base}index|O{+empty}X|O{+undef}X|{+path}/here|here?ref={+path}|up{+path}{var}/here|{+x,hello,y}|{+path,x}/here|{+path:6}/here|{+list}|{+list*}|{+keys}|{+keys*}', 'params' => $params]],
+ # level 2 - Fragment Expansion: {#var}
+ ['#value|#Hello%20World!|#50%25|foo#|foo|#1024,Hello%20World!,768|#/foo/bar,1024/here|#/foo/b/here|#red,green,blue|#red,green,blue|#semi,;,dot,.,comma,,|#semi=;,dot=.,comma=,', ['uri' => '{#var}|{#hello}|{#half}|foo{#empty}|foo{#undef}|{#x,hello,y}|{#path,x}/here|{#path:6}/here|{#list}|{#list*}|{#keys}|{#keys*}', 'params' => $params]],
+ # Label Expansion with Dot-Prefix: {.var}
+ ['.fred|.fred.fred|.50%25.fred|www.example.com|X.value|X.|X|X.val|X.red,green,blue|X.red.green.blue|X.semi,%3B,dot,.,comma,%2C|X.semi=%3B.dot=..comma=%2C|X|X', ['uri' => '{.who}|{.who,who}|{.half,who}|www{.dom*}|X{.var}|X{.empty}|X{.undef}|X{.var:3}|X{.list}|X{.list*}|X{.keys}|X{.keys*}|X{.empty_keys}|X{.empty_keys*}', 'params' => $params]],
+ # Path Segment Expansion: {/var}
+ ['/fred|/fred/fred|/50%25/fred|/fred/me%2Ftoo|/value|/value/|/value|/value/1024/here|/v/value|/red,green,blue|/red/green/blue|/red/green/blue/%2Ffoo|/semi,%3B,dot,.,comma,%2C|/semi=%3B/dot=./comma=%2C', ['uri' => '{/who}|{/who,who}|{/half,who}|{/who,dub}|{/var}|{/var,empty}|{/var,undef}|{/var,x}/here|{/var:1,var}|{/list}|{/list*}|{/list*,path:4}|{/keys}|{/keys*}', 'params' => $params]],
+ # Path-Style Parameter Expansion: {;var}
+ [';who=fred|;half=50%25|;empty|;v=6;empty;who=fred|;v=6;who=fred|;x=1024;y=768|;x=1024;y=768;empty|;x=1024;y=768|;hello=Hello|;list=red,green,blue|;list=red;list=green;list=blue|;keys=semi,%3B,dot,.,comma,%2C|;semi=%3B;dot=.;comma=%2C', ['uri' => '{;who}|{;half}|{;empty}|{;v,empty,who}|{;v,bar,who}|{;x,y}|{;x,y,empty}|{;x,y,undef}|{;hello:5}|{;list}|{;list*}|{;keys}|{;keys*}', 'params' => $params]],
+ # Form-Style Query Expansion: {?var}
+ ['?who=fred|?half=50%25|?x=1024&y=768|?x=1024&y=768&empty=|?x=1024&y=768|?var=val|?list=red,green,blue|?list=red&list=green&list=blue|?keys=semi,%3B,dot,.,comma,%2C|?semi=%3B&dot=.&comma=%2C|?list_with_empty=|?john=', ['uri' => '{?who}|{?half}|{?x,y}|{?x,y,empty}|{?x,y,undef}|{?var:3}|{?list}|{?list*}|{?keys}|{?keys*}|{?list_with_empty*}|{?keys_with_empty*}', 'params' => $params]],
+ # Form-Style Query Continuation: {&var}
+ ['&who=fred|&half=50%25|?fixed=yes&x=1024|&x=1024&y=768&empty=|&x=1024&y=768|&var=val|&list=red,green,blue|&list=red&list=green&list=blue|&keys=semi,%3B,dot,.,comma,%2C|&semi=%3B&dot=.&comma=%2C', ['uri' => '{&who}|{&half}|?fixed=yes{&x}|{&x,y,empty}|{&x,y,undef}|{&var:3}|{&list}|{&list*}|{&keys}|{&keys*}', 'params' => $params]],
+ # Test empty values
+ ['|||', ['uri' => '{empty}|{empty*}|{?empty}|{?empty*}', 'params' => ['empty' => []]]],
+ ];
+ }
+
+ public static function dataExpandWithArrayModifier()
+ {
+ return [
+ # List
+ [
+ # '?choices[]=a&choices[]=b&choices[]=c',
+ '?choices%5B%5D=a&choices%5B%5D=b&choices%5B%5D=c',
+ ['uri' => '{?choices%}', 'params' => ['choices' => ['a', 'b', 'c']]],
+ ],
+ # Keys
+ [
+ # '?choices[a]=1&choices[b]=2&choices[c][test]=3',
+ '?choices%5Ba%5D=1&choices%5Bb%5D=2&choices%5Bc%5D%5Btest%5D=3',
+ ['uri' => '{?choices%}', 'params' => ['choices' => ['a' => 1, 'b' => 2, 'c' => ['test' => 3]]]],
+ ],
+ # Mixed
+ [
+ # '?list[]=a&list[]=b&keys[a]=1&keys[b]=2',
+ '?list%5B%5D=a&list%5B%5D=b&keys%5Ba%5D=1&keys%5Bb%5D=2',
+ ['uri' => '{?list%,keys%}', 'params' => ['list' => ['a', 'b'], 'keys' => ['a' => 1, 'b' => 2]]],
+ ],
+ ];
+ }
+
+ public static function dataBaseTemplate()
+ {
+ return [
+ [
+ 'http://google.com/api/1/users/1',
+ # base uri
+ ['uri' => '{+host}/api/{v}', 'params' => ['host' => 'http://google.com', 'v' => 1]],
+ # other uri
+ ['uri' => '/{resource}/{id}', 'params' => ['resource' => 'users', 'id' => 1]],
+ ],
+ # test override base params
+ [
+ 'http://github.com/api/1/users/1',
+ # base uri
+ ['uri' => '{+host}/api/{v}', 'params' => ['host' => 'http://google.com', 'v' => 1]],
+ # other uri
+ ['uri' => '/{resource}/{id}', 'params' => ['host' => 'http://github.com', 'resource' => 'users', 'id' => 1]],
+ ],
+ ];
+ }
+
+ public static function dataExtraction()
+ {
+ return [['/no/{term:1}/random/foo{?query,list%,keys%}', '/no/j/random/foo?query=1,2,3&list%5B%5D=a&list%5B%5D=b&keys%5Ba%5D=1&keys%5Bb%5D=2&keys%5Bc%5D%5Btest%5D%5Btest%5D=1', ['term:1' => 'j', 'query' => [1, 2, 3], 'list' => ['a', 'b'], 'keys' => ['a' => 1, 'b' => 2, 'c' => ['test' => ['test' => 1]]]]], ['/no/{term:1}/random/{term}/{test*}/foo{?query,number}', '/no/j/random/john/a,b,c/foo?query=1,2,3&number=10', ['term:1' => 'j', 'term' => 'john', 'test' => ['a', 'b', 'c'], 'query' => [1, 2, 3], 'number' => 10]], ['/search/{term:1}/{term}/{?q*,limit}', '/search/j/john/?a=1&b=2&limit=10', ['term:1' => 'j', 'term' => 'john', 'q' => ['a' => 1, 'b' => 2], 'limit' => 10]], ['http://www.example.com/foo{?query,number}', 'http://www.example.com/foo?query=5', ['query' => 5, 'number' => null]], ['{count}|{count*}|{/count}|{/count*}|{;count}|{;count*}|{?count}|{?count*}|{&count*}', 'one,two,three|one,two,three|/one,two,three|/one/two/three|;count=one,two,three|;count=one;count=two;count=three|?count=one,two,three|?count=one&count=two&count=three|&count=one&count=two&count=three', ['count' => ['one', 'two', 'three']]], ['http://example.com/{term:1}/{term}/search{?q*,lang}', 'http://example.com/j/john/search?q=Hello%20World%21&q=3&lang=th,jp,en', ['q' => ['Hello World!', 3], 'lang' => ['th', 'jp', 'en'], 'term' => 'john', 'term:1' => 'j']], ['/foo/bar/{number}', '/foo/bar/0', ['number' => 0]], ['/some/{path}{?ref}', '/some/foo', ['path' => 'foo', 'ref' => null]]];
+ }
+
+ /**
+ * @dataProvider dataExpansion
+ */
+ public function testExpansion($expected, $input)
+ {
+ $service = $this->service();
+ $result = $service->expand($input['uri'], $input['params']);
+
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * @dataProvider dataExpandWithArrayModifier
+ */
+ public function testExpandWithArrayModifier($expected, $input)
+ {
+ $service = $this->service();
+ $result = $service->expand($input['uri'], $input['params']);
+
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * @dataProvider dataBaseTemplate
+ */
+ public function testBaseTemplate($expected, $base, $other)
+ {
+ $service = $this->service($base['uri'], $base['params']);
+ $result = $service->expand($other['uri'], $other['params']);
+
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * @dataProvider dataExtraction
+ */
+ public function testExtract($template, $uri, $expected)
+ {
+ $service = $this->service();
+ $actual = $service->extract($template, $uri);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testExpandFromFixture()
+ {
+ $dir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'fixtures' . DIRECTORY_SEPARATOR;
+ $files = ['spec-examples.json', 'spec-examples-by-section.json', 'extended-tests.json'];
+ $service = $this->service();
+
+ foreach ($files as $file) {
+ $content = json_decode(file_get_contents($dir . $file), $array = true);
+
+ # iterate through each fixture
+ foreach ($content as $fixture) {
+ $vars = $fixture['variables'];
+
+ # assert each test cases
+ foreach ($fixture['testcases'] as $case) {
+ [$uri, $expected] = $case;
+
+ $actual = $service->expand($uri, $vars);
+
+ if (is_array($expected)) {
+ $expected = current(array_filter($expected, fn($input) => $actual === $input));
+ }
+
+ $this->assertEquals($expected, $actual);
+ }
+ }
+ }
+ }
+
+ public static function dataExtractStrictMode()
+ {
+ $dataTest = [['/search/{term:1}/{term}/{?q*,limit}', '/search/j/john/?a=1&b=2&limit=10', ['term:1' => 'j', 'term' => 'john', 'limit' => '10', 'q' => ['a' => '1', 'b' => '2']]], ['http://example.com/{term:1}/{term}/search{?q*,lang}', 'http://example.com/j/john/search?q=Hello%20World%21&q=3&lang=th,jp,en', ['term:1' => 'j', 'term' => 'john', 'lang' => ['th', 'jp', 'en'], 'q' => ['Hello World!', '3']]], ['/foo/bar/{number}', '/foo/bar/0', ['number' => 0]], ['/', '/', []]];
+
+ $rfc3986AllowedPathCharacters = ['-', '.', '_', '~', '!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '=', ':', '@'];
+
+ foreach ($rfc3986AllowedPathCharacters as $char) {
+ $title = "RFC3986 path character ($char)";
+ $title = str_replace("'", 'single quote', $title); // PhpStorm workaround
+ if ($char === ',') { // , means array on RFC6570
+ $params = ['term' => ['foo', 'baz']];
+ } else {
+ $params = ['term' => "foo{$char}baz"];
+ }
+
+ $data = ['/search/{term}', "/search/foo{$char}baz", $params];
+
+ $dataTest[$title] = $data;
+ $data = ['/search/{;term}', "/search/;term=foo{$char}baz", $params];
+ $dataTest['Named ' . $title] = $data;
+ }
+
+ $rfc3986AllowedQueryCharacters = $rfc3986AllowedPathCharacters;
+ $rfc3986AllowedQueryCharacters[] = '/';
+ $rfc3986AllowedQueryCharacters[] = '?';
+ unset($rfc3986AllowedQueryCharacters[array_search('&', $rfc3986AllowedQueryCharacters, true)]);
+
+ foreach ($rfc3986AllowedQueryCharacters as $char) {
+ $title = "RFC3986 query character ($char)";
+ $title = str_replace("'", 'single quote', $title); // PhpStorm workaround
+ if ($char === ',') { // , means array on RFC6570
+ $params = ['term' => ['foo', 'baz']];
+ } else {
+ $params = ['term' => "foo{$char}baz"];
+ }
+
+ $data = ['/search/{?term}', "/search/?term=foo{$char}baz", $params];
+ $dataTest['Named ' . $title] = $data;
+ }
+
+ return $dataTest;
+ }
+
+ public static function extractStrictModeNotMatchProvider()
+ {
+ return [['/', '/a'], ['/{test}', '/a/'], ['/search/{term:1}/{term}/{?q*,limit}', '/search/j/?a=1&b=2&limit=10'], ['http://www.example.com/foo{?query,number}', 'http://www.example.com/foo?query=5'], ['http://www.example.com/foo{?query,number}', 'http://www.example.com/foo'], ['http://example.com/{term:1}/{term}/search{?q*,lang}', 'http://example.com/j/john/search?q=']];
+ }
+
+ #[DataProvider('dataExtractStrictMode')]
+ public function testExtractStrictMode(string $template, string $uri, array $expectedParams)
+ {
+ $service = $this->service();
+ $params = $service->extract($template, $uri, true);
+
+ $this->assertTrue(isset($params));
+ $this->assertEquals($expectedParams, $params);
+ }
+
+ #[DataProvider('extractStrictModeNotMatchProvider')]
+ public function testExtractStrictModeNotMatch(string $template, string $uri)
+ {
+ $service = $this->service();
+ $actual = $service->extract($template, $uri, true);
+
+ $this->assertFalse(isset($actual));
+ }
+}
diff --git a/test/fixtures/README.md b/tests/fixtures/README.md
similarity index 100%
rename from test/fixtures/README.md
rename to tests/fixtures/README.md
diff --git a/test/fixtures/extended-tests.json b/tests/fixtures/extended-tests.json
similarity index 100%
rename from test/fixtures/extended-tests.json
rename to tests/fixtures/extended-tests.json
diff --git a/test/fixtures/json2xml.xslt b/tests/fixtures/json2xml.xslt
similarity index 100%
rename from test/fixtures/json2xml.xslt
rename to tests/fixtures/json2xml.xslt
diff --git a/test/fixtures/negative-tests.json b/tests/fixtures/negative-tests.json
similarity index 100%
rename from test/fixtures/negative-tests.json
rename to tests/fixtures/negative-tests.json
diff --git a/test/fixtures/spec-examples-by-section.json b/tests/fixtures/spec-examples-by-section.json
similarity index 100%
rename from test/fixtures/spec-examples-by-section.json
rename to tests/fixtures/spec-examples-by-section.json
diff --git a/test/fixtures/spec-examples.json b/tests/fixtures/spec-examples.json
similarity index 100%
rename from test/fixtures/spec-examples.json
rename to tests/fixtures/spec-examples.json
diff --git a/test/fixtures/transform-json-tests.xslt b/tests/fixtures/transform-json-tests.xslt
similarity index 100%
rename from test/fixtures/transform-json-tests.xslt
rename to tests/fixtures/transform-json-tests.xslt