diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..45a28b4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,30 @@ +name: MAIN + +on: + push: + branches: + - main + +jobs: + tests-php8: + runs-on: ubuntu-20.04 + + strategy: + fail-fast: true + matrix: + php-versions: ['8.1', '8.2', '8.3'] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + + - name: Install dependencies + run: composer require -W phpunit/phpunit + + - name: Run tests + run: composer run-script test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..116f35f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor +composer.lock +.idea \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fc5ba5 --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# PHP URI Template + +This is a URI Template implementation in PHP based on [RFC 6570 URI Template](http://tools.ietf.org/html/rfc6570). In addition to URI expansion, it also supports URI extraction. + +* This repository is strictly typed, cleaner version of [rize/UriTemplate](https://github.com/rize/UriTemplate) +* To ensure that the package functions properly, tests are duplicated exactly as they are. + +## Installation + +Installation via `composer`: + +``` +composer require ozdemirrulass/uri-template +``` + + +## Usage + +### Expansion + +A very simple usage (string expansion). + +```php +expand('/{username}/profile', ['username' => 'john']); + +>> '/john/profile' +``` + +`Ozdemirrulass\UriTemplate` supports all `Expression Types` and `Levels` specified by RFC6570. + +```php +expand('/search/{term:1}/{term}/{?q*,limit}', [ + 'term' => 'john', + 'q' => ['a', 'b'], + 'limit' => 10, +]) + +>> '/search/j/john/?q=a&q=b&limit=10' +``` + +#### `/` Path segment expansion + +```php +expand('http://{host}{/segments*}/{file}{.extensions*}', [ + 'host' => 'www.host.com', + 'segments' => ['path', 'to', 'a'], + 'file' => 'file', + 'extensions' => ['x', 'y'], +]); + +>> 'http://www.host.com/path/to/a/file.x.y' +``` + +`Ozdemirrulass\UriTemplate` accepts `base-uri` as a 1st argument and `default params` as a 2nd argument. This is very useful when you're working with API endpoint. + +Take a look at real world example. + +```php + 1.1]); +$uri->expand('/statuses/show/{id}.json', ['id' => '210462857140252672']); + +>> https://api.twitter.com/1.1/statuses/show/210462857140252672.json +``` + +### Extraction + +It also supports URI Extraction (extract all variables from URI). Let's take a look at the example. + +```php + 1.1]); + +$params = $uri->extract('/search/{term:1}/{term}/{?q*,limit}', '/search/j/john/?q=a&q=b&limit=10'); + +>> print_r($params); +( + [term:1] => j + [term] => john + [q] => Array + ( + [0] => a + [1] => b + ) + + [limit] => 10 +) +``` + +Note that in the example above, result returned by `extract` method has an extra keys named `term:1` for `prefix` modifier. This key was added just for our convenience to access prefix data. + +#### `strict` mode + +```php +extract($template, $uri, $strict = false) +``` + +Normally `extract` method will try to extract vars from a uri even if it's partially matched. For example + +```php +extract('/{?a,b}', '/?a=1') + +>> print_r($params); +( + [a] => 1 + [b] => null +) +``` + +With `strict mode`, it will allow you to extract uri only when variables in template are fully matched with given uri. + +Which is useful when you want to determine whether the given uri is matched against your template or not (in case you want to use it as routing service). + +```php +extract('/{?a,b}', '/?a=1', true); + +>>> null + +// Now we give `b` some value +$params = $uri->extract('/{?a,b}', '/?a=1&b=2', true); + +>>> print_r($params) +( + [a] => 1 + [b] => 2 +) +``` + +#### Array modifier `%` + +By default, RFC 6570 only has 2 types of operators `:` and `*`. This `%` array operator was added to the library because current spec can't handle array style query e.g. `list[]=a` or `key[user]=john`. + +Example usage for `%` modifier + +```php +expand('{?list%,keys%}', [ + 'list' => [ + 'a', 'b', + ), + 'keys' => [ + 'a' => 1, + 'b' => 2, + ), +]); + +// '?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' + +// [] get encoded to %5B%5D i.e. '?list[]=a&list[]=b&keys[a]=1&keys[b]=2' +$params = $uri->extract('{?list%,keys%}', '?list%5B%5D=a&list%5B%5D=b&keys%5Ba%5D=1&keys%5Bb%5D=2', ) + +>> print_r($params); +( + [list] => Array + ( + [0] => a + [1] => b + ) + + [keys] => Array + ( + [a] => 1 + [b] => 2 + ) +) +``` + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6a78b20 --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "ozdemirrulass/uri-template", + "description": "Enhanced PHP URI Template (RFC 6570) supports both expansion & extraction", + "keywords": ["URI", "Template", "RFC 6570"], + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "Ozdemirrulass\\": "src/Ozdemirrulass/" + } + }, + "authors": [ + { + "name": "ozdemirrulass", + "email": "ozdemirrulass@gmail.com" + } + ], + "scripts": { + "test": "vendor/bin/phpunit test/" + }, + "minimum-stability": "stable", + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.2@dev" + } +} diff --git a/src/Ozdemirrulass/UriTemplate.php b/src/Ozdemirrulass/UriTemplate.php new file mode 100644 index 0000000..3364da8 --- /dev/null +++ b/src/Ozdemirrulass/UriTemplate.php @@ -0,0 +1,61 @@ +params; + $uri = $this->base_uri.$uri; + $result = array(); + + if ((strpos($uri, '{')) === false) { + return $uri; + } + + $parser = $this->parser; + $nodes = $parser->parse($uri); + + foreach ($nodes as $node) { + $result[] = $node->expand($params); + } + + return implode('', $result); + } + + public function extract(string $template, string $uri, bool $strict = false) + { + $params = array(); + $nodes = $this->parser->parse($template); + + foreach ($nodes as $node) { + if ($strict && !strlen((string)$uri)) { + return null; + } + + $match = $node->match($this->parser, $uri, $params, $strict); + + list($uri, $params) = $match; + } + + if ($strict && strlen((string)$uri)) { + return null; + } + + return $params; + } + + +} \ No newline at end of file diff --git a/src/Ozdemirrulass/UriTemplate/Node/Abstraction.php b/src/Ozdemirrulass/UriTemplate/Node/Abstraction.php new file mode 100644 index 0000000..67ca084 --- /dev/null +++ b/src/Ozdemirrulass/UriTemplate/Node/Abstraction.php @@ -0,0 +1,36 @@ +token; + } + + public function match(Parser $parser, string $uri, array $params = array(), bool $strict = false): ?array + { + $length = strlen($this->token); + if (substr($uri, 0, $length) === $this->token) { + $uri = substr($uri, $length); + } elseif ($strict) { + return null; + } + + return array($uri, $params); + } + + public function getToken(): string + { + return $this->token; + } +} diff --git a/src/Ozdemirrulass/UriTemplate/Node/Expression.php b/src/Ozdemirrulass/UriTemplate/Node/Expression.php new file mode 100644 index 0000000..db5867a --- /dev/null +++ b/src/Ozdemirrulass/UriTemplate/Node/Expression.php @@ -0,0 +1,106 @@ +operator; + } + + public function getVariables(): ?array + { + return $this->variables; + } + + public function getForwardLookupSeparator(): ?string + { + return $this->forwardLookupSeparator; + } + + public function setForwardLookupSeparator(string $forwardLookupSeparator): void + { + $this->forwardLookupSeparator = $forwardLookupSeparator; + } + + public function expand(array $params = array()): ?string + { + $data = array(); + $op = $this->operator; + + foreach ($this->variables as $var) { + $val = $op->expand($var, $params); + + if (!is_null($val)) { + $data[] = $val; + } + } + + return $data ? $op->first.implode($op->sep, $data) : null; + } + + public function match(Parser $parser, string $uri, array $params = array(), bool $strict = false): ?array + { + $op = $this->operator; + + if ($op->id && isset($uri[0]) && $uri[0] !== $op->id) { + return array($uri, $params); + } + + if ($op->id) { + $uri = substr($uri, 1); + } + + foreach ($this->sortVariables($this->variables) as $var) { + $regex = '#'.$op->toRegex($var).'#'; + $val = null; + + $remainingUri = ''; + $preparedUri = $uri; + if ($this->forwardLookupSeparator) { + $lastOccurrenceOfSeparator = stripos($uri, $this->forwardLookupSeparator); + $preparedUri = substr($uri, 0, $lastOccurrenceOfSeparator); + $remainingUri = substr($uri, $lastOccurrenceOfSeparator); + } + + if (preg_match($regex, $preparedUri, $match)) { + $preparedUri = preg_replace($regex, '', $preparedUri, $limit = 1); + $val = $op->extract($var, $match[0]); + } elseif ($strict) { + return null; + } + + $uri = $preparedUri.$remainingUri; + + $params[$var->getToken()] = $val; + } + + return array($uri, $params); + } + + protected function sortVariables(array $vars): array + { + usort($vars, function ($a, $b) { + return $a->options['modifier'] >= $b->options['modifier'] ? 1 : -1; + }); + + return $vars; + } +} diff --git a/src/Ozdemirrulass/UriTemplate/Node/Literal.php b/src/Ozdemirrulass/UriTemplate/Node/Literal.php new file mode 100644 index 0000000..992f52f --- /dev/null +++ b/src/Ozdemirrulass/UriTemplate/Node/Literal.php @@ -0,0 +1,10 @@ + null, + 'value' => null, + ); + + public function __construct(string $token, array $options = array()) + { + parent::__construct($token); + $this->options = $options + $this->options; + + $name = $token; + if ($options['modifier'] === ':') { + $name = substr($name, 0, strpos($name, $options['modifier'])); + } + + $this->name = $name; + } +} diff --git a/src/Ozdemirrulass/UriTemplate/Operator/Abstraction.php b/src/Ozdemirrulass/UriTemplate/Operator/Abstraction.php new file mode 100644 index 0000000..a696b14 --- /dev/null +++ b/src/Ozdemirrulass/UriTemplate/Operator/Abstraction.php @@ -0,0 +1,284 @@ + [ + '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' => '#', + ], + ]; + + public const RESERVED_CHARS = [ + '%3A' => ':', + '%2F' => '/', + '%3F' => '?', + '%23' => '#', + '%5B' => '[', + '%5D' => ']', + '%40' => '@', + '%21' => '!', + '%24' => '$', + '%26' => '&', + '%27' => "'", + '%28' => '(', + '%29' => ')', + '%2A' => '*', + '%2B' => '+', + '%2C' => ',', + '%3B' => ';', + '%3D' => '=', + ]; + protected const PATH_REGEX = '(?:[a-zA-Z0-9\-\._~!\$&\'\(\)\*\+,;=%:@]+|%(?![A-Fa-f0-9]{2}))'; + protected const QUERY_REGEX = '(?:[a-zA-Z0-9\-\._~!\$\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))'; + + public function __construct( + public string $id, + public bool $named, + public string $sep, + public string $empty, + public bool $reserved, + public ?int $start, + public ?string $first + ) { + } + + abstract public function toRegex(Variable $var); + + public function expand(Variable $var, array $params = array()): ?string + { + $options = $var->options; + $name = $var->name; + $is_explode = in_array($options['modifier'], array('*', '%')); + + if (!isset($params[$name])) { + return null; + } + + $val = $params[$name]; + + if (!is_array($val)) { + return $this->expandString($var, $val); + } elseif (!$is_explode) { + return $this->expandNonExplode($var, $val); + } else { + return $this->expandExplode($var, $val); + } + } + + public function expandExplode(Variable $var, array $val): ?string + { + if (empty($val)) { + return null; + } + + return $this->encode($var, $val); + } + + public function expandString(Variable $var, $val): string + { + $val = (string)$val; + $options = $var->options; + + if ($options['modifier'] === ':') { + $val = substr($val, 0, (int)$options['value']); + } + + return $this->encode($var, $val); + } + + public function expandNonExplode(Variable $var, array $val): ?string + { + if (empty($val)) { + return null; + } + + return $this->encode($var, $val); + } + + + public function encode(Variable $var, $values): string + { + $values = (array)$values; + $list = isset($values[0]); + $reserved = $this->reserved; + $maps = static::RESERVED_CHARS; + $sep = $this->sep; + $assoc_sep = '='; + + if ($var->options['modifier'] !== '*') { + $assoc_sep = $sep = ','; + } + + array_walk($values, function (&$v, $k) use ($assoc_sep, $reserved, $list, $maps) { + $encoded = rawurlencode($v); + + if (!$list) { + $encoded = rawurlencode($k).$assoc_sep.$encoded; + } + + if (!$reserved) { + $v = $encoded; + } else { + $v = str_replace( + array_keys($maps), + $maps, + $encoded + ); + } + }); + + return implode($sep, $values); + } + + public function decode($values) + { + $single = !is_array($values); + $values = (array)$values; + + array_walk($values, function (&$v) { + $v = rawurldecode($v); + }); + + return $single ? reset($values) : $values; + } + + public function extract(Variable $var, $data) + { + $value = $data; + $vals = array_filter(explode($this->sep, $data)); + $options = $var->options; + + switch ($options['modifier']) { + case '*': + $data = array(); + foreach ($vals as $val) { + if (str_contains($val, '=')) { + list($k, $v) = explode('=', $val); + $data[$k] = $v; + } else { + $data[] = $val; + } + } + + break; + case ':': + break; + default: + $data = str_contains($data, $this->sep) ? $vals : $value; + } + + return $this->decode($data); + } + + public static function createById($id) + { + if (!isset(static::TYPES[$id])) { + throw new Exception("Invalid operator [$id]"); + } + + if (isset(static::$loaded[$id])) { + return static::$loaded[$id]; + } + + $op = static::TYPES[$id]; + $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): bool + { + return isset(static::TYPES[$id]); + } + + /** + * @return string + */ + protected function getRegex(): string + { + return match ($this->id) { + '?', '&', '#' => self::QUERY_REGEX, + default => self::PATH_REGEX, + }; + } + + +} \ No newline at end of file diff --git a/src/Ozdemirrulass/UriTemplate/Operator/Named.php b/src/Ozdemirrulass/UriTemplate/Operator/Named.php new file mode 100644 index 0000000..9ecdf35 --- /dev/null +++ b/src/Ozdemirrulass/UriTemplate/Operator/Named.php @@ -0,0 +1,163 @@ +name; + $value = $this->getRegex(); + $options = $var->options; + + if ($options['modifier']) { + switch ($options['modifier']) { + case '*': + $regex = "{$name}+=(?:{$value}+(?:{$this->sep}{$name}+={$value}*)*)" + ."|{$value}+=(?:{$value}+(?:{$this->sep}{$value}+={$value}*)*)"; + break; + case ':': + $regex = "{$value}\{0,{$options['value']}\}"; + break; + + case '%': + $name = $name.'+(?:%5B|\[)[^=]*='; + $regex = "{$name}(?:{$value}+(?:{$this->sep}{$name}{$value}*)*)"; + break; + default: + throw new \Exception("Unknown modifier `{$options['modifier']}`"); + } + } else { + $regex = "{$name}=(?:{$value}+(?:,{$value}+)*)*"; + } + + return '(?:&)?'.$regex; + } + + public function expandString(Variable $var, $val): string + { + $val = (string)$val; + $options = $var->options; + $result = $this->encode($var, $var->name); + + if ($val === '') { + return $result.$this->empty; + } else { + $result .= '='; + } + + if ($options['modifier'] === ':') { + $val = mb_substr($val, 0, (int)$options['value']); + } + + return $result.$this->encode($var, $val); + } + + public function expandNonExplode(Variable $var, array $val): ?string + { + if (empty($val)) { + return null; + } + + $result = $this->encode($var, $var->name); + + $result .= '='; + + return $result.$this->encode($var, $val); + } + + public function expandExplode(Variable $var, array $val): ?string + { + if (empty($val)) { + return null; + } + $list = isset($val[0]); + $data = array(); + foreach ($val as $k => $v) { + $key = $list ? $var->name : $k; + if ($list) { + $data[$key][] = $v; + } else { + $data[$key] = $v; + } + } + + if (!$list and $var->options['modifier'] === '%') { + $data = array($var->name => $data); + } + + return $this->encodeExplodeVars($var, $data); + } + + public function extract(Variable $var, $data) + { + if ($data[0] === '&') { + $data = substr($data, 1); + } + + $value = $data; + $vals = explode($this->sep, $data); + $options = $var->options; + + switch ($options['modifier']) { + case '%': + parse_str($data, $query); + + return $query[$var->name]; + + case '*': + $data = array(); + + foreach ($vals as $val) { + list($k, $v) = explode('=', $val); + + // 2 + if ($k === $var->getToken()) { + $data[] = $v; + } // 4 + else { + $data[$k] = $v; + } + } + + break; + case ':': + break; + default: + $value = str_replace($var->getToken().'=', '', $value); + $data = explode(',', $value); + + if (sizeof($data) === 1) { + $data = current($data); + } + } + + return $this->decode($data); + } + + public function encodeExplodeVars(Variable $var, $data): array|string|null + { + $query = http_build_query($data, '', $this->sep); + $query = str_replace('+', '%20', $query); + + if ($var->options['modifier'] === '%') { + $query = preg_replace('#%5B\d+%5D#', '%5B%5D', $query); + } else { + $query = preg_replace('#%5B\d+%5D#', '', $query); + } + + if ($this->reserved) { + $query = str_replace( + array_keys(static::RESERVED_CHARS), + static::RESERVED_CHARS, + $query + ); + } + + return $query; + } +} diff --git a/src/Ozdemirrulass/UriTemplate/Operator/UnNamed.php b/src/Ozdemirrulass/UriTemplate/Operator/UnNamed.php new file mode 100644 index 0000000..ddb9655 --- /dev/null +++ b/src/Ozdemirrulass/UriTemplate/Operator/UnNamed.php @@ -0,0 +1,35 @@ +getRegex(); + $options = $var->options; + + if ($options['modifier']) { + switch ($options['modifier']) { + case '*': + $regex = "{$value}+(?:{$this->sep}{$value}+)*"; + break; + case ':': + $regex = $value.'{0,'.$options['value'].'}'; + break; + case '%': + throw new \Exception("% (array) modifier only works with Named type operators e.g. ;,?,&"); + default: + throw new \Exception("Unknown modifier `{$options['modifier']}`"); + } + } else { + $regex = "{$value}*(?:,{$value}+)*"; + } + + return $regex; + } +} \ No newline at end of file diff --git a/src/Ozdemirrulass/UriTemplate/Parser.php b/src/Ozdemirrulass/UriTemplate/Parser.php new file mode 100644 index 0000000..94bac56 --- /dev/null +++ b/src/Ozdemirrulass/UriTemplate/Parser.php @@ -0,0 +1,141 @@ +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 ($previousNode instanceof Expression && $previousNode->getOperator() instanceof UnNamed) { + $previousNode->setForwardLookupSeparator($node->getOperator()->id); + } + } + } + + $nodes[] = $node; + } + + return $nodes; + } + + private function createNode(string $token): Literal|Expression + { + if ($token[0] !== '{') { + $node = $this->createLiteralNode($token); + } else { + $node = $this->parseExpression(substr($token, 1, -1)); + } + + return $node; + } + + protected function createLiteralNode(string $token): Literal + { + return new Literal($token); + } + + protected function parseExpression(string $expression): Node\Expression + { + $token = $expression; + $prefix = $token[0]; + + if (!Operator\Abstraction::isValid($prefix)) { + if (!preg_match('#'.self::REGEX_VARNAME.'#', $token)) { + throw new \Exception("Invalid operator [$prefix] found at {$token}"); + } + + $prefix = null; + } + + if ($prefix) { + $token = substr($token, 1); + } + + $vars = array(); + foreach (explode(',', $token) as $var) { + $vars[] = $this->parseVariable($var); + } + + return $this->createExpressionNode( + $token, + $this->createOperatorNode((string)$prefix), + $vars + ); + } + + protected function createOperatorNode(string $token) + { + return Operator\Abstraction::createById($token); + } + + protected function createExpressionNode( + $token, + Operator\Abstraction $operator = null, + array $vars = array() + ): Node\Expression { + return new Node\Expression($token, $operator, $vars); + } + + protected function parseVariable($var): Variable + { + $var = trim($var); + $val = null; + $modifier = null; + + if (str_contains($var, ':')) { + $modifier = ':'; + list($varname, $val) = explode(':', $var); + + if (!is_numeric($val)) { + throw new \Exception("Value for `:` modifier must be numeric value [$varname:$val]"); + } + } + + switch ($last = substr($var, -1)) { + case '*': + case '%': + + if ($modifier) { + throw new \Exception("Multiple modifiers per variable are not allowed [$var]"); + } + + $modifier = $last; + $var = substr($var, 0, -1); + break; + } + + return $this->createVariableNode( + $var, + array( + 'modifier' => $modifier, + 'value' => $val, + ) + ); + } + + protected function createVariableNode(string $token, array $options = array()): Variable + { + return new Variable($token, $options); + } + +} \ No newline at end of file diff --git a/test/Rize/Uri/Node/ParserTest.php b/test/Rize/Uri/Node/ParserTest.php new file mode 100644 index 0000000..58a5f24 --- /dev/null +++ b/test/Rize/Uri/Node/ParserTest.php @@ -0,0 +1,131 @@ + ':', + '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 new file mode 100644 index 0000000..d97b93a --- /dev/null +++ b/test/Rize/UriTemplateTest.php @@ -0,0 +1,688 @@ + 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/test/fixtures/README.md b/test/fixtures/README.md new file mode 100644 index 0000000..3eb519d --- /dev/null +++ b/test/fixtures/README.md @@ -0,0 +1,90 @@ + +URI Template Tests +================== + +This is a set of tests for implementations of +[RFC6570](http://tools.ietf.org/html/rfc6570) - URI Template. It is designed +to be reused by any implementation, to improve interoperability and +implementation quality. + +If your project uses Git for version control, you can make uritemplate-tests into a [submodule](http://help.github.com/submodules/). + +Test Format +----------- + +Each test file is a [JSON](http://tools.ietf.org/html/RFC6627) document +containing an object whose properties are groups of related tests. +Alternatively, all tests are available in XML as well, with the XML files +being generated by transform-json-tests.xslt which uses json2xml.xslt as a +general-purpose JSON-to-XML parsing library. + +Each group, in turn, is an object with three children: + +* level - the level of the tests covered, as per the RFC (optional; if absent, + assume level 4). +* variables - an object representing the variables that are available to the + tests in the suite +* testcases - a list of testcases, where each case is a two-member list, the + first being the template, the second being the result of expanding the + template with the provided variables. + +Note that the result string can be a few different things: + +* string - if the second member is a string, the result of expansion is + expected to match it, character-for-character. +* list - if the second member is a list of strings, the result of expansion + is expected to match one of them; this allows for templates that can + expand into different, equally-acceptable URIs. +* false - if the second member is boolean false, expansion is expected to + fail (i.e., the template was invalid). + +For example: + + { + "Level 1 Examples" : + { + "level": 1, + "variables": { + "var" : "value", + "hello" : "Hello World!" + }, + "testcases" : [ + ["{var}", "value"], + ["{hello}", "Hello%20World%21"] + ] + } + } + + +Tests Included +-------------- + +The following test files are included: + +* spec-examples.json - The complete set of example templates from the RFC +* spec-examples-by-section.json - The examples, section by section +* extended-tests.json - more complex test cases +* negative-tests.json - invalid templates + +For all these test files, XML versions with the names *.xml can be +generated with the transform-json-tests.xslt XSLT stylesheet. The XSLT +contains the names of the above test files as a parameter, and can be +started with any XML as input (i.e., the XML input is ignored). + +License +------- + + Copyright 2011-2012 The Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/test/fixtures/extended-tests.json b/test/fixtures/extended-tests.json new file mode 100644 index 0000000..8ae8134 --- /dev/null +++ b/test/fixtures/extended-tests.json @@ -0,0 +1,118 @@ +{ + "Additional Examples 1":{ + "level":4, + "variables":{ + "id" : "person", + "token" : "12345", + "fields" : ["id", "name", "picture"], + "format" : "json", + "q" : "URI Templates", + "page" : "5", + "lang" : "en", + "geocode" : ["37.76","-122.427"], + "first_name" : "John", + "last.name" : "Doe", + "Some%20Thing" : "foo", + "number" : 6, + "long" : 37.76, + "lat" : -122.427, + "group_id" : "12345", + "query" : "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }", + "uri" : "http://example.org/?uri=http%3A%2F%2Fexample.org%2F", + "word" : "drücken", + "Stra%C3%9Fe" : "Grüner Weg", + "random" : "šö䟜ñꀣ¥‡ÑÒÓÔÕÖ×ØÙÚàáâãäåæçÿ", + "assoc_special_chars" : + { "šö䟜ñꀣ¥‡ÑÒÓÔÕ" : "Ö×ØÙÚàáâãäåæçÿ" } + }, + "testcases":[ + + [ "{/id*}" , "/person" ], + [ "{/id*}{?fields,first_name,last.name,token}" , [ + "/person?fields=id,name,picture&first_name=John&last.name=Doe&token=12345", + "/person?fields=id,picture,name&first_name=John&last.name=Doe&token=12345", + "/person?fields=picture,name,id&first_name=John&last.name=Doe&token=12345", + "/person?fields=picture,id,name&first_name=John&last.name=Doe&token=12345", + "/person?fields=name,picture,id&first_name=John&last.name=Doe&token=12345", + "/person?fields=name,id,picture&first_name=John&last.name=Doe&token=12345"] + ], + ["/search.{format}{?q,geocode,lang,locale,page,result_type}", + [ "/search.json?q=URI%20Templates&geocode=37.76,-122.427&lang=en&page=5", + "/search.json?q=URI%20Templates&geocode=-122.427,37.76&lang=en&page=5"] + ], + ["/test{/Some%20Thing}", "/test/foo" ], + ["/set{?number}", "/set?number=6"], + ["/loc{?long,lat}" , "/loc?long=37.76&lat=-122.427"], + ["/base{/group_id,first_name}/pages{/page,lang}{?format,q}","/base/12345/John/pages/5/en?format=json&q=URI%20Templates"], + ["/sparql{?query}", "/sparql?query=PREFIX%20dc%3A%20%3Chttp%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%3E%20SELECT%20%3Fbook%20%3Fwho%20WHERE%20%7B%20%3Fbook%20dc%3Acreator%20%3Fwho%20%7D"], + ["/go{?uri}", "/go?uri=http%3A%2F%2Fexample.org%2F%3Furi%3Dhttp%253A%252F%252Fexample.org%252F"], + ["/service{?word}", "/service?word=dr%C3%BCcken"], + ["/lookup{?Stra%C3%9Fe}", "/lookup?Stra%25C3%259Fe=Gr%C3%BCner%20Weg"], + ["{random}" , "%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF"], + ["{?assoc_special_chars*}", "?%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95=%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF"] + ] + }, + "Additional Examples 2":{ + "level":4, + "variables":{ + "id" : ["person","albums"], + "token" : "12345", + "fields" : ["id", "name", "picture"], + "format" : "atom", + "q" : "URI Templates", + "page" : "10", + "start" : "5", + "lang" : "en", + "geocode" : ["37.76","-122.427"] + }, + "testcases":[ + + [ "{/id*}" , ["/person/albums","/albums/person"] ], + [ "{/id*}{?fields,token}" , [ + "/person/albums?fields=id,name,picture&token=12345", + "/person/albums?fields=id,picture,name&token=12345", + "/person/albums?fields=picture,name,id&token=12345", + "/person/albums?fields=picture,id,name&token=12345", + "/person/albums?fields=name,picture,id&token=12345", + "/person/albums?fields=name,id,picture&token=12345", + "/albums/person?fields=id,name,picture&token=12345", + "/albums/person?fields=id,picture,name&token=12345", + "/albums/person?fields=picture,name,id&token=12345", + "/albums/person?fields=picture,id,name&token=12345", + "/albums/person?fields=name,picture,id&token=12345", + "/albums/person?fields=name,id,picture&token=12345"] + ] + ] + }, + "Additional Examples 3: Empty Variables":{ + "variables" : { + "empty_list" : [], + "empty_assoc" : {} + }, + "testcases":[ + [ "{/empty_list}", [ "" ] ], + [ "{/empty_list*}", [ "" ] ], + [ "{?empty_list}", [ ""] ], + [ "{?empty_list*}", [ "" ] ], + [ "{?empty_assoc}", [ "" ] ], + [ "{?empty_assoc*}", [ "" ] ] + ] + }, + "Additional Examples 4: Numeric Keys":{ + "variables" : { + "42" : "The Answer to the Ultimate Question of Life, the Universe, and Everything", + "1337" : ["leet", "as","it", "can","be"], + "german" : { + "11": "elf", + "12": "zwölf" + } + }, + "testcases":[ + [ "{42}", "The%20Answer%20to%20the%20Ultimate%20Question%20of%20Life%2C%20the%20Universe%2C%20and%20Everything"], + [ "{?42}", "?42=The%20Answer%20to%20the%20Ultimate%20Question%20of%20Life%2C%20the%20Universe%2C%20and%20Everything"], + [ "{1337}", "leet,as,it,can,be"], + [ "{?1337*}", "?1337=leet&1337=as&1337=it&1337=can&1337=be"], + [ "{?german*}", [ "?11=elf&12=zw%C3%B6lf", "?12=zw%C3%B6lf&11=elf"] ] + ] + } +} diff --git a/test/fixtures/json2xml.xslt b/test/fixtures/json2xml.xslt new file mode 100644 index 0000000..59b3548 --- /dev/null +++ b/test/fixtures/json2xml.xslt @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \b + + + + + + + + + + + \v + + + + + \f + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/negative-tests.json b/test/fixtures/negative-tests.json new file mode 100644 index 0000000..552a6bf --- /dev/null +++ b/test/fixtures/negative-tests.json @@ -0,0 +1,57 @@ +{ + "Failure Tests":{ + "level":4, + "variables":{ + "id" : "thing", + "var" : "value", + "hello" : "Hello World!", + "with space" : "fail", + " leading_space" : "Hi!", + "trailing_space " : "Bye!", + "empty" : "", + "path" : "/foo/bar", + "x" : "1024", + "y" : "768", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "example" : "red", + "searchTerms" : "uri templates", + "~thing" : "some-user", + "default-graph-uri" : ["http://www.example/book/","http://www.example/papers/"], + "query" : "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }" + + }, + "testcases":[ + [ "{/id*", false ], + [ "/id*}", false ], + [ "{/?id}", false ], + [ "{var:prefix}", false ], + [ "{hello:2*}", false ] , + [ "{??hello}", false ] , + [ "{!hello}", false ] , + [ "{with space}", false], + [ "{ leading_space}", false], + [ "{trailing_space }", false], + [ "{=path}", false ] , + [ "{$var}", false ], + [ "{|var*}", false ], + [ "{*keys?}", false ], + [ "{?empty=default,var}", false ], + [ "{var}{-prefix|/-/|var}" , false ], + [ "?q={searchTerms}&c={example:color?}" , false ], + [ "x{?empty|foo=none}" , false ], + [ "/h{#hello+}" , false ], + [ "/h#{hello+}" , false ], + [ "{keys:1}", false ], + [ "{+keys:1}", false ], + [ "{;keys:1*}", false ], + [ "?{-join|&|var,list}" , false ], + [ "/people/{~thing}", false], + [ "/{default-graph-uri}", false ], + [ "/sparql{?query,default-graph-uri}", false ], + [ "/sparql{?query){&default-graph-uri*}", false ], + [ "/resolution{?x, y}" , false ] + + ] + } +} \ No newline at end of file diff --git a/test/fixtures/spec-examples-by-section.json b/test/fixtures/spec-examples-by-section.json new file mode 100644 index 0000000..5aef182 --- /dev/null +++ b/test/fixtures/spec-examples-by-section.json @@ -0,0 +1,439 @@ +{ + "3.2.1 Variable Expansion" : + { + "variables": { + "count" : ["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" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : [], + "undef" : null + }, + "testcases" : [ + ["{count}", "one,two,three"], + ["{count*}", "one,two,three"], + ["{/count}", "/one,two,three"], + ["{/count*}", "/one/two/three"], + ["{;count}", ";count=one,two,three"], + ["{;count*}", ";count=one;count=two;count=three"], + ["{?count}", "?count=one,two,three"], + ["{?count*}", "?count=one&count=two&count=three"], + ["{&count*}", "&count=one&count=two&count=three"] + ] + }, + "3.2.2 Simple String Expansion" : + { + "variables": { + "count" : ["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" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : [], + "undef" : null + }, + "testcases" : [ + ["{var}", "value"], + ["{hello}", "Hello%20World%21"], + ["{half}", "50%25"], + ["O{empty}X", "OX"], + ["O{undef}X", "OX"], + ["{x,y}", "1024,768"], + ["{x,hello,y}", "1024,Hello%20World%21,768"], + ["?{x,empty}", "?1024,"], + ["?{x,undef}", "?1024"], + ["?{undef,y}", "?768"], + ["{var:3}", "val"], + ["{var:30}", "value"], + ["{list}", "red,green,blue"], + ["{list*}", "red,green,blue"], + ["{keys}", [ + "comma,%2C,dot,.,semi,%3B", + "comma,%2C,semi,%3B,dot,.", + "dot,.,comma,%2C,semi,%3B", + "dot,.,semi,%3B,comma,%2C", + "semi,%3B,comma,%2C,dot,.", + "semi,%3B,dot,.,comma,%2C" + ]], + ["{keys*}", [ + "comma=%2C,dot=.,semi=%3B", + "comma=%2C,semi=%3B,dot=.", + "dot=.,comma=%2C,semi=%3B", + "dot=.,semi=%3B,comma=%2C", + "semi=%3B,comma=%2C,dot=.", + "semi=%3B,dot=.,comma=%2C" + ]] + ] + }, + "3.2.3 Reserved Expansion" : + { + "variables": { + "count" : ["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" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : [], + "undef" : null + }, + "testcases" : [ + ["{+var}", "value"], + ["{/var,empty}", "/value/"], + ["{/var,undef}", "/value"], + ["{+hello}", "Hello%20World!"], + ["{+half}", "50%25"], + ["{base}index", "http%3A%2F%2Fexample.com%2Fhome%2Findex"], + ["{+base}index", "http://example.com/home/index"], + ["O{+empty}X", "OX"], + ["O{+undef}X", "OX"], + ["{+path}/here", "/foo/bar/here"], + ["{+path:6}/here", "/foo/b/here"], + ["here?ref={+path}", "here?ref=/foo/bar"], + ["up{+path}{var}/here", "up/foo/barvalue/here"], + ["{+x,hello,y}", "1024,Hello%20World!,768"], + ["{+path,x}/here", "/foo/bar,1024/here"], + ["{+list}", "red,green,blue"], + ["{+list*}", "red,green,blue"], + ["{+keys}", [ + "comma,,,dot,.,semi,;", + "comma,,,semi,;,dot,.", + "dot,.,comma,,,semi,;", + "dot,.,semi,;,comma,,", + "semi,;,comma,,,dot,.", + "semi,;,dot,.,comma,," + ]], + ["{+keys*}", [ + "comma=,,dot=.,semi=;", + "comma=,,semi=;,dot=.", + "dot=.,comma=,,semi=;", + "dot=.,semi=;,comma=,", + "semi=;,comma=,,dot=.", + "semi=;,dot=.,comma=," + ]] + ] + }, + "3.2.4 Fragment Expansion" : + { + "variables": { + "count" : ["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" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : [], + "undef" : null + }, + "testcases" : [ + ["{#var}", "#value"], + ["{#hello}", "#Hello%20World!"], + ["{#half}", "#50%25"], + ["foo{#empty}", "foo#"], + ["foo{#undef}", "foo"], + ["{#x,hello,y}", "#1024,Hello%20World!,768"], + ["{#path,x}/here", "#/foo/bar,1024/here"], + ["{#path:6}/here", "#/foo/b/here"], + ["{#list}", "#red,green,blue"], + ["{#list*}", "#red,green,blue"], + ["{#keys}", [ + "#comma,,,dot,.,semi,;", + "#comma,,,semi,;,dot,.", + "#dot,.,comma,,,semi,;", + "#dot,.,semi,;,comma,,", + "#semi,;,comma,,,dot,.", + "#semi,;,dot,.,comma,," + ]] + ] + }, + "3.2.5 Label Expansion with Dot-Prefix" : + { + "variables": { + "count" : ["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" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : [], + "undef" : null + }, + "testcases" : [ + ["{.who}", ".fred"], + ["{.who,who}", ".fred.fred"], + ["{.half,who}", ".50%25.fred"], + ["www{.dom*}", "www.example.com"], + ["X{.var}", "X.value"], + ["X{.var:3}", "X.val"], + ["X{.empty}", "X."], + ["X{.undef}", "X"], + ["X{.list}", "X.red,green,blue"], + ["X{.list*}", "X.red.green.blue"], + ["{#keys}", [ + "#comma,,,dot,.,semi,;", + "#comma,,,semi,;,dot,.", + "#dot,.,comma,,,semi,;", + "#dot,.,semi,;,comma,,", + "#semi,;,comma,,,dot,.", + "#semi,;,dot,.,comma,," + ]], + ["{#keys*}", [ + "#comma=,,dot=.,semi=;", + "#comma=,,semi=;,dot=.", + "#dot=.,comma=,,semi=;", + "#dot=.,semi=;,comma=,", + "#semi=;,comma=,,dot=.", + "#semi=;,dot=.,comma=," + ]], + ["X{.empty_keys}", "X"], + ["X{.empty_keys*}", "X"] + ] + }, + "3.2.6 Path Segment Expansion" : + { + "variables": { + "count" : ["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" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : [], + "undef" : null + }, + "testcases" : [ + ["{/who}", "/fred"], + ["{/who,who}", "/fred/fred"], + ["{/half,who}", "/50%25/fred"], + ["{/who,dub}", "/fred/me%2Ftoo"], + ["{/var}", "/value"], + ["{/var,empty}", "/value/"], + ["{/var,undef}", "/value"], + ["{/var,x}/here", "/value/1024/here"], + ["{/var:1,var}", "/v/value"], + ["{/list}", "/red,green,blue"], + ["{/list*}", "/red/green/blue"], + ["{/list*,path:4}", "/red/green/blue/%2Ffoo"], + ["{/keys}", [ + "/comma,%2C,dot,.,semi,%3B", + "/comma,%2C,semi,%3B,dot,.", + "/dot,.,comma,%2C,semi,%3B", + "/dot,.,semi,%3B,comma,%2C", + "/semi,%3B,comma,%2C,dot,.", + "/semi,%3B,dot,.,comma,%2C" + ]], + ["{/keys*}", [ + "/comma=%2C/dot=./semi=%3B", + "/comma=%2C/semi=%3B/dot=.", + "/dot=./comma=%2C/semi=%3B", + "/dot=./semi=%3B/comma=%2C", + "/semi=%3B/comma=%2C/dot=.", + "/semi=%3B/dot=./comma=%2C" + ]] + ] + }, + "3.2.7 Path-Style Parameter Expansion" : + { + "variables": { + "count" : ["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" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : [], + "undef" : null + }, + "testcases" : [ + ["{;who}", ";who=fred"], + ["{;half}", ";half=50%25"], + ["{;empty}", ";empty"], + ["{;hello:5}", ";hello=Hello"], + ["{;v,empty,who}", ";v=6;empty;who=fred"], + ["{;v,bar,who}", ";v=6;who=fred"], + ["{;x,y}", ";x=1024;y=768"], + ["{;x,y,empty}", ";x=1024;y=768;empty"], + ["{;x,y,undef}", ";x=1024;y=768"], + ["{;list}", ";list=red,green,blue"], + ["{;list*}", ";list=red;list=green;list=blue"], + ["{;keys}", [ + ";keys=comma,%2C,dot,.,semi,%3B", + ";keys=comma,%2C,semi,%3B,dot,.", + ";keys=dot,.,comma,%2C,semi,%3B", + ";keys=dot,.,semi,%3B,comma,%2C", + ";keys=semi,%3B,comma,%2C,dot,.", + ";keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{;keys*}", [ + ";comma=%2C;dot=.;semi=%3B", + ";comma=%2C;semi=%3B;dot=.", + ";dot=.;comma=%2C;semi=%3B", + ";dot=.;semi=%3B;comma=%2C", + ";semi=%3B;comma=%2C;dot=.", + ";semi=%3B;dot=.;comma=%2C" + ]] + ] + }, + "3.2.8 Form-Style Query Expansion" : + { + "variables": { + "count" : ["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" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : [], + "undef" : null + }, + "testcases" : [ + ["{?who}", "?who=fred"], + ["{?half}", "?half=50%25"], + ["{?x,y}", "?x=1024&y=768"], + ["{?x,y,empty}", "?x=1024&y=768&empty="], + ["{?x,y,undef}", "?x=1024&y=768"], + ["{?var:3}", "?var=val"], + ["{?list}", "?list=red,green,blue"], + ["{?list*}", "?list=red&list=green&list=blue"], + ["{?keys}", [ + "?keys=comma,%2C,dot,.,semi,%3B", + "?keys=comma,%2C,semi,%3B,dot,.", + "?keys=dot,.,comma,%2C,semi,%3B", + "?keys=dot,.,semi,%3B,comma,%2C", + "?keys=semi,%3B,comma,%2C,dot,.", + "?keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{?keys*}", [ + "?comma=%2C&dot=.&semi=%3B", + "?comma=%2C&semi=%3B&dot=.", + "?dot=.&comma=%2C&semi=%3B", + "?dot=.&semi=%3B&comma=%2C", + "?semi=%3B&comma=%2C&dot=.", + "?semi=%3B&dot=.&comma=%2C" + ]] + ] + }, + "3.2.9 Form-Style Query Continuation" : + { + "variables": { + "count" : ["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" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : [], + "undef" : null + }, + "testcases" : [ + ["{&who}", "&who=fred"], + ["{&half}", "&half=50%25"], + ["?fixed=yes{&x}", "?fixed=yes&x=1024"], + ["{&var:3}", "&var=val"], + ["{&x,y,empty}", "&x=1024&y=768&empty="], + ["{&x,y,undef}", "&x=1024&y=768"], + ["{&list}", "&list=red,green,blue"], + ["{&list*}", "&list=red&list=green&list=blue"], + ["{&keys}", [ + "&keys=comma,%2C,dot,.,semi,%3B", + "&keys=comma,%2C,semi,%3B,dot,.", + "&keys=dot,.,comma,%2C,semi,%3B", + "&keys=dot,.,semi,%3B,comma,%2C", + "&keys=semi,%3B,comma,%2C,dot,.", + "&keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{&keys*}", [ + "&comma=%2C&dot=.&semi=%3B", + "&comma=%2C&semi=%3B&dot=.", + "&dot=.&comma=%2C&semi=%3B", + "&dot=.&semi=%3B&comma=%2C", + "&semi=%3B&comma=%2C&dot=.", + "&semi=%3B&dot=.&comma=%2C" + ]] + ] + } +} diff --git a/test/fixtures/spec-examples.json b/test/fixtures/spec-examples.json new file mode 100644 index 0000000..2e8e942 --- /dev/null +++ b/test/fixtures/spec-examples.json @@ -0,0 +1,218 @@ +{ + "Level 1 Examples" : + { + "level": 1, + "variables": { + "var" : "value", + "hello" : "Hello World!" + }, + "testcases" : [ + ["{var}", "value"], + ["{hello}", "Hello%20World%21"] + ] + }, + "Level 2 Examples" : + { + "level": 2, + "variables": { + "var" : "value", + "hello" : "Hello World!", + "path" : "/foo/bar" + }, + "testcases" : [ + ["{+var}", "value"], + ["{+hello}", "Hello%20World!"], + ["{+path}/here", "/foo/bar/here"], + ["here?ref={+path}", "here?ref=/foo/bar"] + ] + }, + "Level 3 Examples" : + { + "level": 3, + "variables": { + "var" : "value", + "hello" : "Hello World!", + "empty" : "", + "path" : "/foo/bar", + "x" : "1024", + "y" : "768" + }, + "testcases" : [ + ["map?{x,y}", "map?1024,768"], + ["{x,hello,y}", "1024,Hello%20World%21,768"], + ["{+x,hello,y}", "1024,Hello%20World!,768"], + ["{+path,x}/here", "/foo/bar,1024/here"], + ["{#x,hello,y}", "#1024,Hello%20World!,768"], + ["{#path,x}/here", "#/foo/bar,1024/here"], + ["X{.var}", "X.value"], + ["X{.x,y}", "X.1024.768"], + ["{/var}", "/value"], + ["{/var,x}/here", "/value/1024/here"], + ["{;x,y}", ";x=1024;y=768"], + ["{;x,y,empty}", ";x=1024;y=768;empty"], + ["{?x,y}", "?x=1024&y=768"], + ["{?x,y,empty}", "?x=1024&y=768&empty="], + ["?fixed=yes{&x}", "?fixed=yes&x=1024"], + ["{&x,y,empty}", "&x=1024&y=768&empty="] + ] + }, + "Level 4 Examples" : + { + "level": 4, + "variables": { + "var": "value", + "hello": "Hello World!", + "path": "/foo/bar", + "list": ["red", "green", "blue"], + "keys": {"semi": ";", "dot": ".", "comma":","} + }, + "testcases": [ + ["{var:3}", "val"], + ["{var:30}", "value"], + ["{list}", "red,green,blue"], + ["{list*}", "red,green,blue"], + ["{keys}", [ + "comma,%2C,dot,.,semi,%3B", + "comma,%2C,semi,%3B,dot,.", + "dot,.,comma,%2C,semi,%3B", + "dot,.,semi,%3B,comma,%2C", + "semi,%3B,comma,%2C,dot,.", + "semi,%3B,dot,.,comma,%2C" + ]], + ["{keys*}", [ + "comma=%2C,dot=.,semi=%3B", + "comma=%2C,semi=%3B,dot=.", + "dot=.,comma=%2C,semi=%3B", + "dot=.,semi=%3B,comma=%2C", + "semi=%3B,comma=%2C,dot=.", + "semi=%3B,dot=.,comma=%2C" + ]], + ["{+path:6}/here", "/foo/b/here"], + ["{+list}", "red,green,blue"], + ["{+list*}", "red,green,blue"], + ["{+keys}", [ + "comma,,,dot,.,semi,;", + "comma,,,semi,;,dot,.", + "dot,.,comma,,,semi,;", + "dot,.,semi,;,comma,,", + "semi,;,comma,,,dot,.", + "semi,;,dot,.,comma,," + ]], + ["{+keys*}", [ + "comma=,,dot=.,semi=;", + "comma=,,semi=;,dot=.", + "dot=.,comma=,,semi=;", + "dot=.,semi=;,comma=,", + "semi=;,comma=,,dot=.", + "semi=;,dot=.,comma=," + ]], + ["{#path:6}/here", "#/foo/b/here"], + ["{#list}", "#red,green,blue"], + ["{#list*}", "#red,green,blue"], + ["{#keys}", [ + "#comma,,,dot,.,semi,;", + "#comma,,,semi,;,dot,.", + "#dot,.,comma,,,semi,;", + "#dot,.,semi,;,comma,,", + "#semi,;,comma,,,dot,.", + "#semi,;,dot,.,comma,," + ]], + ["{#keys*}", [ + "#comma=,,dot=.,semi=;", + "#comma=,,semi=;,dot=.", + "#dot=.,comma=,,semi=;", + "#dot=.,semi=;,comma=,", + "#semi=;,comma=,,dot=.", + "#semi=;,dot=.,comma=," + ]], + ["X{.var:3}", "X.val"], + ["X{.list}", "X.red,green,blue"], + ["X{.list*}", "X.red.green.blue"], + ["X{.keys}", [ + "X.comma,%2C,dot,.,semi,%3B", + "X.comma,%2C,semi,%3B,dot,.", + "X.dot,.,comma,%2C,semi,%3B", + "X.dot,.,semi,%3B,comma,%2C", + "X.semi,%3B,comma,%2C,dot,.", + "X.semi,%3B,dot,.,comma,%2C" + ]], + ["{/var:1,var}", "/v/value"], + ["{/list}", "/red,green,blue"], + ["{/list*}", "/red/green/blue"], + ["{/list*,path:4}", "/red/green/blue/%2Ffoo"], + ["{/keys}", [ + "/comma,%2C,dot,.,semi,%3B", + "/comma,%2C,semi,%3B,dot,.", + "/dot,.,comma,%2C,semi,%3B", + "/dot,.,semi,%3B,comma,%2C", + "/semi,%3B,comma,%2C,dot,.", + "/semi,%3B,dot,.,comma,%2C" + ]], + ["{/keys*}", [ + "/comma=%2C/dot=./semi=%3B", + "/comma=%2C/semi=%3B/dot=.", + "/dot=./comma=%2C/semi=%3B", + "/dot=./semi=%3B/comma=%2C", + "/semi=%3B/comma=%2C/dot=.", + "/semi=%3B/dot=./comma=%2C" + ]], + ["{;hello:5}", ";hello=Hello"], + ["{;list}", ";list=red,green,blue"], + ["{;list*}", ";list=red;list=green;list=blue"], + ["{;keys}", [ + ";keys=comma,%2C,dot,.,semi,%3B", + ";keys=comma,%2C,semi,%3B,dot,.", + ";keys=dot,.,comma,%2C,semi,%3B", + ";keys=dot,.,semi,%3B,comma,%2C", + ";keys=semi,%3B,comma,%2C,dot,.", + ";keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{;keys*}", [ + ";comma=%2C;dot=.;semi=%3B", + ";comma=%2C;semi=%3B;dot=.", + ";dot=.;comma=%2C;semi=%3B", + ";dot=.;semi=%3B;comma=%2C", + ";semi=%3B;comma=%2C;dot=.", + ";semi=%3B;dot=.;comma=%2C" + ]], + ["{?var:3}", "?var=val"], + ["{?list}", "?list=red,green,blue"], + ["{?list*}", "?list=red&list=green&list=blue"], + ["{?keys}", [ + "?keys=comma,%2C,dot,.,semi,%3B", + "?keys=comma,%2C,semi,%3B,dot,.", + "?keys=dot,.,comma,%2C,semi,%3B", + "?keys=dot,.,semi,%3B,comma,%2C", + "?keys=semi,%3B,comma,%2C,dot,.", + "?keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{?keys*}", [ + "?comma=%2C&dot=.&semi=%3B", + "?comma=%2C&semi=%3B&dot=.", + "?dot=.&comma=%2C&semi=%3B", + "?dot=.&semi=%3B&comma=%2C", + "?semi=%3B&comma=%2C&dot=.", + "?semi=%3B&dot=.&comma=%2C" + ]], + ["{&var:3}", "&var=val"], + ["{&list}", "&list=red,green,blue"], + ["{&list*}", "&list=red&list=green&list=blue"], + ["{&keys}", [ + "&keys=comma,%2C,dot,.,semi,%3B", + "&keys=comma,%2C,semi,%3B,dot,.", + "&keys=dot,.,comma,%2C,semi,%3B", + "&keys=dot,.,semi,%3B,comma,%2C", + "&keys=semi,%3B,comma,%2C,dot,.", + "&keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{&keys*}", [ + "&comma=%2C&dot=.&semi=%3B", + "&comma=%2C&semi=%3B&dot=.", + "&dot=.&comma=%2C&semi=%3B", + "&dot=.&semi=%3B&comma=%2C", + "&semi=%3B&comma=%2C&dot=.", + "&semi=%3B&dot=.&comma=%2C" + ]] + ] + } +} diff --git a/test/fixtures/transform-json-tests.xslt b/test/fixtures/transform-json-tests.xslt new file mode 100644 index 0000000..d956b6b --- /dev/null +++ b/test/fixtures/transform-json-tests.xslt @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file