├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── composer.json ├── phpstan.neon ├── phpunit.xml ├── src └── Rize │ ├── UriTemplate.php │ └── UriTemplate │ ├── Node │ ├── Abstraction.php │ ├── Expression.php │ ├── Literal.php │ └── Variable.php │ ├── Operator │ ├── Abstraction.php │ ├── Named.php │ └── UnNamed.php │ ├── Parser.php │ └── UriTemplate.php └── tests ├── Rize ├── Uri │ └── Node │ │ └── ParserTest.php └── UriTemplateTest.php └── fixtures ├── README.md ├── extended-tests.json ├── json2xml.xslt ├── negative-tests.json ├── spec-examples-by-section.json ├── spec-examples.json └── transform-json-tests.xslt /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setParallelConfig(ParallelConfigFactory::detect()) 11 | ->setRiskyAllowed(true) 12 | ->setUsingCache(false) 13 | ->setRules([ 14 | '@PER-CS2.0' => true, 15 | ]) 16 | ->setFinder( 17 | (new Finder()) 18 | ->in([__DIR__ . '/src', __DIR__ . '/tests']) 19 | ); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2014] [Marut Khumtong] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP URI Template 2 | 3 | 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 (used by [Google Cloud Core](https://github.com/googleapis/google-cloud-php-core) and [Google Cloud Client Library](https://github.com/googleapis/google-cloud-php)). 4 | 5 | ![CI](https://github.com/rize/UriTemplate/workflows/CI/badge.svg) [![Total Downloads](https://poser.pugx.org/rize/uri-template/downloads)](https://packagist.org/packages/rize/uri-template) [![Latest Stable Version](https://poser.pugx.org/rize/uri-template/v)](https://packagist.org/packages/rize/uri-template) [![PHP Version Require](https://poser.pugx.org/rize/uri-template/require/php)](https://packagist.org/packages/rize/uri-template) 6 | 7 | > [!NOTE] 8 | > 9 | > Due to the deprecation of implictly nullable parameter types in [PHP 8.4](https://wiki.php.net/rfc/deprecate-implicitly-nullable-types), we must introduce breaking change by adding explicit nullable types (`?T`) which requires PHP 7.1+. 10 | > 11 | > As a result, version [0.4.0](https://github.com/rize/UriTemplate/releases/tag/0.4.0) and later will no longer support PHP versions below 8.1. 12 | 13 | ## Usage 14 | 15 | ### Expansion 16 | 17 | A very simple usage (string expansion). 18 | 19 | ```php 20 | expand('/{username}/profile', ['username' => 'john']); 26 | 27 | >> '/john/profile' 28 | ``` 29 | 30 | `Rize\UriTemplate` supports all `Expression Types` and `Levels` specified by RFC6570. 31 | 32 | ```php 33 | expand('/search/{term:1}/{term}/{?q*,limit}', [ 39 | 'term' => 'john', 40 | 'q' => ['a', 'b'], 41 | 'limit' => 10, 42 | ]) 43 | 44 | >> '/search/j/john/?q=a&q=b&limit=10' 45 | ``` 46 | 47 | #### `/` Path segment expansion 48 | 49 | ```php 50 | expand('http://{host}{/segments*}/{file}{.extensions*}', [ 56 | 'host' => 'www.host.com', 57 | 'segments' => ['path', 'to', 'a'], 58 | 'file' => 'file', 59 | 'extensions' => ['x', 'y'], 60 | ]); 61 | 62 | >> 'http://www.host.com/path/to/a/file.x.y' 63 | ``` 64 | 65 | `Rize\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. 66 | 67 | Take a look at real world example. 68 | 69 | ```php 70 | 1.1]); 75 | $uri->expand('/statuses/show/{id}.json', ['id' => '210462857140252672']); 76 | 77 | >> https://api.twitter.com/1.1/statuses/show/210462857140252672.json 78 | ``` 79 | 80 | ### Extraction 81 | 82 | It also supports URI Extraction (extract all variables from URI). Let's take a look at the example. 83 | 84 | ```php 85 | 1.1]); 90 | 91 | $params = $uri->extract('/search/{term:1}/{term}/{?q*,limit}', '/search/j/john/?q=a&q=b&limit=10'); 92 | 93 | >> print_r($params); 94 | ( 95 | [term:1] => j 96 | [term] => john 97 | [q] => Array 98 | ( 99 | [0] => a 100 | [1] => b 101 | ) 102 | 103 | [limit] => 10 104 | ) 105 | ``` 106 | 107 | 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. 108 | 109 | #### `strict` mode 110 | 111 | ```php 112 | extract($template, $uri, $strict = false) 118 | ``` 119 | 120 | Normally `extract` method will try to extract vars from a uri even if it's partially matched. For example 121 | 122 | ```php 123 | extract('/{?a,b}', '/?a=1') 129 | 130 | >> print_r($params); 131 | ( 132 | [a] => 1 133 | [b] => null 134 | ) 135 | ``` 136 | 137 | With `strict mode`, it will allow you to extract uri only when variables in template are fully matched with given uri. 138 | 139 | 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). 140 | 141 | ```php 142 | extract('/{?a,b}', '/?a=1', true); 150 | 151 | >>> null 152 | 153 | // Now we give `b` some value 154 | $params = $uri->extract('/{?a,b}', '/?a=1&b=2', true); 155 | 156 | >>> print_r($params) 157 | ( 158 | [a] => 1 159 | [b] => 2 160 | ) 161 | ``` 162 | 163 | #### Array modifier `%` 164 | 165 | 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`. 166 | 167 | Example usage for `%` modifier 168 | 169 | ```php 170 | expand('{?list%,keys%}', [ 173 | 'list' => [ 174 | 'a', 'b', 175 | ), 176 | 'keys' => [ 177 | 'a' => 1, 178 | 'b' => 2, 179 | ), 180 | ]); 181 | 182 | // '?list[]=a&list[]=b&keys[a]=1&keys[b]=2' 183 | >> '?list%5B%5D=a&list%5B%5D=b&keys%5Ba%5D=1&keys%5Bb%5D=2' 184 | 185 | // [] get encoded to %5B%5D i.e. '?list[]=a&list[]=b&keys[a]=1&keys[b]=2' 186 | $params = $uri->extract('{?list%,keys%}', '?list%5B%5D=a&list%5B%5D=b&keys%5Ba%5D=1&keys%5Bb%5D=2', ) 187 | 188 | >> print_r($params); 189 | ( 190 | [list] => Array 191 | ( 192 | [0] => a 193 | [1] => b 194 | ) 195 | 196 | [keys] => Array 197 | ( 198 | [a] => 1 199 | [b] => 2 200 | ) 201 | ) 202 | ``` 203 | 204 | ## Installation 205 | 206 | Using `composer` 207 | 208 | ``` 209 | { 210 | "require": { 211 | "rize/uri-template": "~0.3" 212 | } 213 | } 214 | ``` 215 | 216 | ### Changelogs 217 | 218 | * **0.2.0** Add a new modifier `%` which allows user to use `list[]=a&list[]=b` query pattern. 219 | * **0.2.1** Add nested array support for `%` modifier 220 | * **0.2.5** Add strict mode support for `extract` method 221 | * **0.3.0** Improve code quality + RFC3986 support for `extract` method by @Maks3w 222 | * **0.3.1** Improve `extract` method to parse two or more adjacent variables separated by dot by @corleonis 223 | * **0.4.0** Fixes the deprecation of implicitly nullable parameter types introduced in PHP 8.4. This version requires PHP 8.1 or later. 224 | 225 | ## Contributors 226 | 227 | ### Code Contributors 228 | 229 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 230 | 231 | 232 | ### Financial Contributors 233 | 234 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/rize-uri-template/contribute)] 235 | 236 | #### Individuals 237 | 238 | 239 | 240 | #### Organizations 241 | 242 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/rize-uri-template/contribute)] 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rize/uri-template", 3 | "type": "library", 4 | "description": "PHP URI Template (RFC 6570) supports both expansion & extraction", 5 | "keywords": ["URI", "Template", "RFC 6570"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Marut K", 10 | "homepage": "http://twitter.com/rezigned" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=8.1" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "~10.0", 18 | "phpstan/phpstan": "^1.12", 19 | "friendsofphp/php-cs-fixer": "^3.63" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Rize\\": "src/Rize" 24 | } 25 | }, 26 | "scripts": { 27 | "test": "vendor/bin/phpunit", 28 | "cs": "vendor/bin/php-cs-fixer fix --dry-run", 29 | "cs-fix": "vendor/bin/php-cs-fixer fix", 30 | "phpstan": "vendor/bin/phpstan" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | paths: 4 | - src 5 | - tests -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | tests 14 | 15 | 16 | 17 | 18 | 19 | src 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Rize/UriTemplate.php: -------------------------------------------------------------------------------- 1 | parser = $parser ?: $this->createNodeParser(); 18 | } 19 | 20 | /** 21 | * Expands URI Template. 22 | * 23 | * @param mixed $params 24 | */ 25 | public function expand(string $uri, $params = []): string 26 | { 27 | $params += $this->params; 28 | $uri = $this->base_uri . $uri; 29 | $result = []; 30 | 31 | // quick check 32 | if (!str_contains($uri, '{')) { 33 | return $uri; 34 | } 35 | 36 | $parser = $this->parser; 37 | $nodes = $parser->parse($uri); 38 | 39 | foreach ($nodes as $node) { 40 | $result[] = $node->expand($parser, $params); 41 | } 42 | 43 | return implode('', $result); 44 | } 45 | 46 | /** 47 | * Extracts variables from URI. 48 | * 49 | * @return null|array params or null if not match and $strict is true 50 | */ 51 | public function extract(string $template, string $uri, bool $strict = false): ?array 52 | { 53 | $params = []; 54 | $nodes = $this->parser->parse($template); 55 | 56 | // PHP 8.1.0RC4-dev still throws deprecation warning for `strlen`. 57 | // $uri = (string) $uri; 58 | 59 | foreach ($nodes as $node) { 60 | // if strict is given, and there's no remaining uri just return null 61 | if ($strict && (string) $uri === '') { 62 | return null; 63 | } 64 | 65 | // URI will be truncated from the start when a match is found 66 | $match = $node->match($this->parser, $uri, $params, $strict); 67 | 68 | if ($match === null) { 69 | return null; 70 | } 71 | 72 | [$uri, $params] = $match; 73 | } 74 | 75 | // if there's remaining $uri, matching is failed 76 | if ($strict && (string) $uri !== '') { 77 | return null; 78 | } 79 | 80 | return $params; 81 | } 82 | 83 | public function getParser(): Parser 84 | { 85 | return $this->parser; 86 | } 87 | 88 | protected function createNodeParser(): Parser 89 | { 90 | static $parser; 91 | 92 | if ($parser) { 93 | return $parser; 94 | } 95 | 96 | return $parser = new Parser(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Rize/UriTemplate/Node/Abstraction.php: -------------------------------------------------------------------------------- 1 | $params 18 | */ 19 | public function expand(Parser $parser, array $params = []): ?string 20 | { 21 | return $this->token; 22 | } 23 | 24 | /** 25 | * Matches given URI against current node. 26 | * 27 | * @param array $params 28 | * 29 | * @return null|array{0: string, 1: array} `uri and params` or `null` if not match and $strict is true 30 | */ 31 | public function match(Parser $parser, string $uri, array $params = [], bool $strict = false): ?array 32 | { 33 | // match literal string from start to end 34 | if (str_starts_with($uri, $this->token)) { 35 | $uri = substr($uri, strlen($this->token)); 36 | } 37 | 38 | // when there's no match, just return null if strict mode is given 39 | elseif ($strict) { 40 | return null; 41 | } 42 | 43 | return [$uri, $params]; 44 | } 45 | 46 | public function getToken(): string 47 | { 48 | return $this->token; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Rize/UriTemplate/Node/Expression.php: -------------------------------------------------------------------------------- 1 | operator; 24 | } 25 | 26 | public function getVariables(): ?array 27 | { 28 | return $this->variables; 29 | } 30 | 31 | public function getForwardLookupSeparator(): string 32 | { 33 | return $this->forwardLookupSeparator; 34 | } 35 | 36 | public function setForwardLookupSeparator(string $forwardLookupSeparator): void 37 | { 38 | $this->forwardLookupSeparator = $forwardLookupSeparator; 39 | } 40 | 41 | public function expand(Parser $parser, array $params = []): ?string 42 | { 43 | $data = []; 44 | $op = $this->operator; 45 | 46 | if ($this->variables === null) { 47 | return $op->first; 48 | } 49 | 50 | // check for variable modifiers 51 | foreach ($this->variables as $var) { 52 | $val = $op->expand($parser, $var, $params); 53 | 54 | // skip null value 55 | if (!is_null($val)) { 56 | $data[] = $val; 57 | } 58 | } 59 | 60 | return $data ? $op->first . implode($op->sep, $data) : null; 61 | } 62 | 63 | /** 64 | * Matches given URI against current node. 65 | * 66 | * @return null|array `uri and params` or `null` if not match and $strict is true 67 | */ 68 | public function match(Parser $parser, string $uri, array $params = [], bool $strict = false): ?array 69 | { 70 | $op = $this->operator; 71 | 72 | // check expression operator first 73 | if ($op->id && isset($uri[0]) && $uri[0] !== $op->id) { 74 | return [$uri, $params]; 75 | } 76 | 77 | // remove operator from input 78 | if ($op->id) { 79 | $uri = substr($uri, 1); 80 | } 81 | 82 | foreach ($this->sortVariables($this->variables) as $var) { 83 | $regex = '#' . $op->toRegex($parser, $var) . '#'; 84 | $val = null; 85 | 86 | // do a forward lookup and get just the relevant part 87 | $remainingUri = ''; 88 | $preparedUri = $uri; 89 | if ($this->forwardLookupSeparator) { 90 | $lastOccurrenceOfSeparator = stripos($uri, $this->forwardLookupSeparator); 91 | $preparedUri = substr($uri, 0, $lastOccurrenceOfSeparator); 92 | $remainingUri = substr($uri, $lastOccurrenceOfSeparator); 93 | } 94 | 95 | if (preg_match($regex, $preparedUri, $match)) { 96 | // remove matched part from input 97 | $preparedUri = preg_replace($regex, '', $preparedUri, 1); 98 | $val = $op->extract($parser, $var, $match[0]); 99 | } 100 | 101 | // if strict is given, we quit immediately when there's no match 102 | elseif ($strict) { 103 | return null; 104 | } 105 | 106 | $uri = $preparedUri . $remainingUri; 107 | 108 | $params[$var->getToken()] = $val; 109 | } 110 | 111 | return [$uri, $params]; 112 | } 113 | 114 | /** 115 | * Sort variables before extracting data from uri. 116 | * We have to sort vars by non-explode to explode. 117 | */ 118 | protected function sortVariables(array $vars): array 119 | { 120 | usort($vars, static fn($a, $b) => $a->options['modifier'] <=> $b->options['modifier']); 121 | 122 | return $vars; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Rize/UriTemplate/Node/Literal.php: -------------------------------------------------------------------------------- 1 | null, 'value' => null]; 13 | 14 | public function __construct(string $token, array $options = []) 15 | { 16 | parent::__construct($token); 17 | $this->options = $options + $this->options; 18 | 19 | // normalize var name e.g. from 'term:1' becomes 'term' 20 | $name = $token; 21 | if ($options['modifier'] === ':') { 22 | $name = strstr($name, $options['modifier'], true); 23 | } 24 | 25 | $this->name = $name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Rize/UriTemplate/Operator/Abstraction.php: -------------------------------------------------------------------------------- 1 | ':', 66 | '%2F' => '/', 67 | '%3F' => '?', 68 | '%23' => '#', 69 | '%5B' => '[', 70 | '%5D' => ']', 71 | '%40' => '@', 72 | '%21' => '!', 73 | '%24' => '$', 74 | '%26' => '&', 75 | '%27' => "'", 76 | '%28' => '(', 77 | '%29' => ')', 78 | '%2A' => '*', 79 | '%2B' => '+', 80 | '%2C' => ',', 81 | '%3B' => ';', 82 | '%3D' => '=', 83 | ]; 84 | 85 | protected static $types = [ 86 | '' => [ 87 | 'sep' => ',', 88 | 'named' => false, 89 | 'empty' => '', 90 | 'reserved' => false, 91 | 'start' => 0, 92 | 'first' => null, 93 | ], 94 | '+' => [ 95 | 'sep' => ',', 96 | 'named' => false, 97 | 'empty' => '', 98 | 'reserved' => true, 99 | 'start' => 1, 100 | 'first' => null, 101 | ], 102 | '.' => [ 103 | 'sep' => '.', 104 | 'named' => false, 105 | 'empty' => '', 106 | 'reserved' => false, 107 | 'start' => 1, 108 | 'first' => '.', 109 | ], 110 | '/' => [ 111 | 'sep' => '/', 112 | 'named' => false, 113 | 'empty' => '', 114 | 'reserved' => false, 115 | 'start' => 1, 116 | 'first' => '/', 117 | ], 118 | ';' => [ 119 | 'sep' => ';', 120 | 'named' => true, 121 | 'empty' => '', 122 | 'reserved' => false, 123 | 'start' => 1, 124 | 'first' => ';', 125 | ], 126 | '?' => [ 127 | 'sep' => '&', 128 | 'named' => true, 129 | 'empty' => '=', 130 | 'reserved' => false, 131 | 'start' => 1, 132 | 'first' => '?', 133 | ], 134 | '&' => [ 135 | 'sep' => '&', 136 | 'named' => true, 137 | 'empty' => '=', 138 | 'reserved' => false, 139 | 'start' => 1, 140 | 'first' => '&', 141 | ], 142 | '#' => [ 143 | 'sep' => ',', 144 | 'named' => false, 145 | 'empty' => '', 146 | 'reserved' => true, 147 | 'start' => 1, 148 | 'first' => '#', 149 | ], 150 | ]; 151 | 152 | protected static $loaded = []; 153 | 154 | /** 155 | * RFC 3986 Allowed path characters regex except the path delimiter '/'. 156 | * 157 | * @var string 158 | */ 159 | protected static $pathRegex = '(?:[a-zA-Z0-9\-\._~!\$&\'\(\)\*\+,;=%:@]+|%(?![A-Fa-f0-9]{2}))'; 160 | 161 | /** 162 | * RFC 3986 Allowed query characters regex except the query parameter delimiter '&'. 163 | * 164 | * @var string 165 | */ 166 | protected static $queryRegex = '(?:[a-zA-Z0-9\-\._~!\$\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))'; 167 | 168 | public function __construct($id, $named, $sep, $empty, $reserved, $start, $first) 169 | { 170 | $this->id = $id; 171 | $this->named = $named; 172 | $this->sep = $sep; 173 | $this->empty = $empty; 174 | $this->start = $start; 175 | $this->first = $first; 176 | $this->reserved = $reserved; 177 | } 178 | 179 | abstract public function toRegex(Parser $parser, Node\Variable $var): string; 180 | 181 | public function expand(Parser $parser, Node\Variable $var, array $params = []) 182 | { 183 | $options = $var->options; 184 | $name = $var->name; 185 | $is_explode = in_array($options['modifier'], ['*', '%']); 186 | 187 | // skip null 188 | if (!isset($params[$name])) { 189 | return null; 190 | } 191 | 192 | $val = $params[$name]; 193 | 194 | // This algorithm is based on RFC6570 http://tools.ietf.org/html/rfc6570 195 | // non-array, e.g. string 196 | if (!is_array($val)) { 197 | return $this->expandString($parser, $var, $val); 198 | } 199 | 200 | // non-explode ':' 201 | if (!$is_explode) { 202 | return $this->expandNonExplode($parser, $var, $val); 203 | } 204 | 205 | // explode '*', '%' 206 | 207 | return $this->expandExplode($parser, $var, $val); 208 | } 209 | 210 | public function expandString(Parser $parser, Node\Variable $var, $val) 211 | { 212 | $val = (string) $val; 213 | $options = $var->options; 214 | $result = null; 215 | 216 | if ($options['modifier'] === ':') { 217 | $val = substr($val, 0, (int) $options['value']); 218 | } 219 | 220 | return $result . $this->encode($parser, $var, $val); 221 | } 222 | 223 | /** 224 | * Non explode modifier ':'. 225 | */ 226 | public function expandNonExplode(Parser $parser, Node\Variable $var, array $val): ?string 227 | { 228 | if (empty($val)) { 229 | return null; 230 | } 231 | 232 | return $this->encode($parser, $var, $val); 233 | } 234 | 235 | /** 236 | * Explode modifier '*', '%'. 237 | */ 238 | public function expandExplode(Parser $parser, Node\Variable $var, array $val): ?string 239 | { 240 | if (empty($val)) { 241 | return null; 242 | } 243 | 244 | return $this->encode($parser, $var, $val); 245 | } 246 | 247 | /** 248 | * Encodes variable according to spec (reserved or unreserved). 249 | * 250 | * @return string encoded string 251 | */ 252 | public function encode(Parser $parser, Node\Variable $var, mixed $values) 253 | { 254 | $values = (array) $values; 255 | $list = isset($values[0]); 256 | $reserved = $this->reserved; 257 | $maps = static::$reserved_chars; 258 | $sep = $this->sep; 259 | $assoc_sep = '='; 260 | 261 | // non-explode modifier always use ',' as a separator 262 | if ($var->options['modifier'] !== '*') { 263 | $assoc_sep = $sep = ','; 264 | } 265 | 266 | array_walk($values, function (&$v, $k) use ($assoc_sep, $reserved, $list, $maps): void { 267 | $encoded = rawurlencode($v); 268 | 269 | // assoc? encode key too 270 | if (!$list) { 271 | $encoded = rawurlencode($k) . $assoc_sep . $encoded; 272 | } 273 | 274 | // rawurlencode is compliant with 'unreserved' set 275 | if (!$reserved) { 276 | $v = $encoded; 277 | } 278 | 279 | // decode chars in reserved set 280 | else { 281 | $v = str_replace( 282 | array_keys($maps), 283 | $maps, 284 | $encoded, 285 | ); 286 | } 287 | }); 288 | 289 | return implode($sep, $values); 290 | } 291 | 292 | /** 293 | * Decodes variable. 294 | * 295 | * @return string decoded string 296 | */ 297 | public function decode(Parser $parser, Node\Variable $var, mixed $values) 298 | { 299 | $single = !is_array($values); 300 | $values = (array) $values; 301 | 302 | array_walk($values, function (&$v, $k): void { 303 | $v = rawurldecode($v); 304 | }); 305 | 306 | return $single ? reset($values) : $values; 307 | } 308 | 309 | /** 310 | * Extracts value from variable. 311 | */ 312 | public function extract(Parser $parser, Node\Variable $var, string $data): array|string 313 | { 314 | $value = $data; 315 | $vals = array_filter(explode($this->sep, $data)); 316 | $options = $var->options; 317 | 318 | switch ($options['modifier']) { 319 | case '*': 320 | $value = []; 321 | foreach ($vals as $val) { 322 | if (str_contains($val, '=')) { 323 | [$k, $v] = explode('=', $val); 324 | $value[$k] = $v; 325 | } else { 326 | $value[] = $val; 327 | } 328 | } 329 | 330 | break; 331 | 332 | case ':': 333 | break; 334 | 335 | default: 336 | $value = str_contains($value, (string) $this->sep) ? $vals : $value; 337 | } 338 | 339 | return $this->decode($parser, $var, $value); 340 | } 341 | 342 | public static function createById($id) 343 | { 344 | if (!isset(static::$types[$id])) { 345 | throw new \InvalidArgumentException("Invalid operator [{$id}]"); 346 | } 347 | 348 | if (isset(static::$loaded[$id])) { 349 | return static::$loaded[$id]; 350 | } 351 | 352 | $op = static::$types[$id]; 353 | $class = __NAMESPACE__ . '\\' . ($op['named'] ? 'Named' : 'UnNamed'); 354 | 355 | return static::$loaded[$id] = new $class($id, $op['named'], $op['sep'], $op['empty'], $op['reserved'], $op['start'], $op['first']); 356 | } 357 | 358 | public static function isValid($id): bool 359 | { 360 | return isset(static::$types[$id]); 361 | } 362 | 363 | /** 364 | * Returns the correct regex given the variable location in the URI. 365 | */ 366 | protected function getRegex(): string 367 | { 368 | return match ($this->id) { 369 | '?', '&', '#' => self::$queryRegex, 370 | default => self::$pathRegex, 371 | }; 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/Rize/UriTemplate/Operator/Named.php: -------------------------------------------------------------------------------- 1 | name; 20 | $value = $this->getRegex(); 21 | $options = $var->options; 22 | 23 | if ($options['modifier']) { 24 | switch ($options['modifier']) { 25 | case '*': 26 | // 2 | 4 27 | $regex = "{$name}+=(?:{$value}+(?:{$this->sep}{$name}+={$value}*)*)" 28 | . "|{$value}+=(?:{$value}+(?:{$this->sep}{$value}+={$value}*)*)"; 29 | 30 | break; 31 | 32 | case ':': 33 | $regex = "{$value}\\{0,{$options['value']}\\}"; 34 | 35 | break; 36 | 37 | case '%': 38 | // 5 39 | $name .= '+(?:%5B|\[)[^=]*='; 40 | $regex = "{$name}(?:{$value}+(?:{$this->sep}{$name}{$value}*)*)"; 41 | 42 | break; 43 | 44 | default: 45 | throw new \InvalidArgumentException("Unknown modifier `{$options['modifier']}`"); 46 | } 47 | } else { 48 | // 1, 3 49 | $regex = "{$name}=(?:{$value}+(?:,{$value}+)*)*"; 50 | } 51 | 52 | return '(?:&)?' . $regex; 53 | } 54 | 55 | public function expandString(Parser $parser, Node\Variable $var, $val): string 56 | { 57 | $val = (string) $val; 58 | $options = $var->options; 59 | $result = $this->encode($parser, $var, $var->name); 60 | 61 | // handle empty value 62 | if ($val === '') { 63 | return $result . $this->empty; 64 | } 65 | 66 | $result .= '='; 67 | 68 | if ($options['modifier'] === ':') { 69 | $val = mb_substr($val, 0, (int) $options['value']); 70 | } 71 | 72 | return $result . $this->encode($parser, $var, $val); 73 | } 74 | 75 | public function expandNonExplode(Parser $parser, Node\Variable $var, array $val): ?string 76 | { 77 | if (empty($val)) { 78 | return null; 79 | } 80 | 81 | $result = $this->encode($parser, $var, $var->name); 82 | 83 | $result .= '='; 84 | 85 | return $result . $this->encode($parser, $var, $val); 86 | } 87 | 88 | public function expandExplode(Parser $parser, Node\Variable $var, array $val): ?string 89 | { 90 | if (empty($val)) { 91 | return null; 92 | } 93 | 94 | $list = isset($val[0]); 95 | $data = []; 96 | foreach ($val as $k => $v) { 97 | // if value is a list, use `varname` as keyname, otherwise use `key` name 98 | $key = $list ? $var->name : $k; 99 | if ($list) { 100 | $data[$key][] = $v; 101 | } else { 102 | $data[$key] = $v; 103 | } 104 | } 105 | 106 | // if it's array modifier, we have to use variable name as index 107 | // e.g. if variable name is 'query' and value is ['limit' => 1] 108 | // then we convert it to ['query' => ['limit' => 1]] 109 | if (!$list && $var->options['modifier'] === '%') { 110 | $data = [$var->name => $data]; 111 | } 112 | 113 | return $this->encodeExplodeVars($var, $data); 114 | } 115 | 116 | public function extract(Parser $parser, Node\Variable $var, $data): array|string 117 | { 118 | // get rid of optional `&` at the beginning 119 | if ($data[0] === '&') { 120 | $data = substr($data, 1); 121 | } 122 | 123 | $value = $data; 124 | $vals = explode($this->sep, $data); 125 | $options = $var->options; 126 | 127 | switch ($options['modifier']) { 128 | case '%': 129 | parse_str($value, $query); 130 | 131 | return $query[$var->name]; 132 | 133 | case '*': 134 | $value = []; 135 | 136 | foreach ($vals as $val) { 137 | [$k, $v] = explode('=', $val); 138 | 139 | // 2 140 | if ($k === $var->getToken()) { 141 | $value[] = $v; 142 | } 143 | 144 | // 4 145 | else { 146 | $value[$k] = $v; 147 | } 148 | } 149 | 150 | break; 151 | 152 | case ':': 153 | break; 154 | 155 | default: 156 | // 1, 3 157 | // remove key from value e.g. 'lang=en,th' becomes 'en,th' 158 | $value = str_replace($var->getToken() . '=', '', $value); 159 | $value = explode(',', $value); 160 | 161 | if (count($value) === 1) { 162 | $value = current($value); 163 | } 164 | } 165 | 166 | return $this->decode($parser, $var, $value); 167 | } 168 | 169 | public function encodeExplodeVars(Node\Variable $var, $data): null|array|string 170 | { 171 | // http_build_query uses PHP_QUERY_RFC1738 encoding by default 172 | // i.e. spaces are encoded as '+' (plus signs) we need to convert 173 | // it to %20 RFC3986 174 | $query = http_build_query($data, '', $this->sep); 175 | $query = str_replace('+', '%20', $query); 176 | 177 | // `%` array modifier 178 | if ($var->options['modifier'] === '%') { 179 | 180 | // it also uses numeric based-index by default e.g. list[] becomes list[0] 181 | $query = preg_replace('#%5B\d+%5D#', '%5B%5D', $query); 182 | } 183 | 184 | // `:`, `*` modifiers 185 | else { 186 | // by default, http_build_query will convert array values to `a[]=1&a[]=2` 187 | // which is different from the spec. It should be `a=1&a=2` 188 | $query = preg_replace('#%5B\d+%5D#', '', $query); 189 | } 190 | 191 | // handle reserved charset 192 | if ($this->reserved) { 193 | $query = str_replace( 194 | array_keys(static::$reserved_chars), 195 | static::$reserved_chars, 196 | $query, 197 | ); 198 | } 199 | 200 | return $query; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Rize/UriTemplate/Operator/UnNamed.php: -------------------------------------------------------------------------------- 1 | getRegex(); 19 | $options = $var->options; 20 | 21 | if ($options['modifier']) { 22 | switch ($options['modifier']) { 23 | case '*': 24 | // 2 | 4 25 | $regex = "{$value}+(?:{$this->sep}{$value}+)*"; 26 | 27 | break; 28 | 29 | case ':': 30 | $regex = $value . '{0,' . $options['value'] . '}'; 31 | 32 | break; 33 | 34 | case '%': 35 | throw new \InvalidArgumentException('% (array) modifier only works with Named type operators e.g. ;,?,&'); 36 | 37 | default: 38 | throw new \InvalidArgumentException("Unknown modifier `{$options['modifier']}`"); 39 | } 40 | } else { 41 | // 1, 3 42 | $regex = "{$value}*(?:,{$value}+)*"; 43 | } 44 | 45 | return $regex; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Rize/UriTemplate/Parser.php: -------------------------------------------------------------------------------- 1 | createNode($part); 26 | 27 | // if current node has dot separator that requires a forward lookup 28 | // for the previous node iff previous node's operator is UnNamed 29 | if ($node instanceof Expression && $node->getOperator()->id === '.') { 30 | if (count($nodes) > 0) { 31 | $previousNode = $nodes[count($nodes) - 1]; 32 | if ($previousNode instanceof Expression && $previousNode->getOperator() instanceof UnNamed) { 33 | $previousNode->setForwardLookupSeparator($node->getOperator()->id); 34 | } 35 | } 36 | } 37 | 38 | $nodes[] = $node; 39 | } 40 | 41 | return $nodes; 42 | } 43 | 44 | protected function createNode(string $token): Abstraction 45 | { 46 | // literal string 47 | if ($token[0] !== '{') { 48 | $node = $this->createLiteralNode($token); 49 | } else { 50 | // remove `{}` from expression and parse it 51 | $node = $this->parseExpression(substr($token, 1, -1)); 52 | } 53 | 54 | return $node; 55 | } 56 | 57 | protected function parseExpression(string $expression): Expression 58 | { 59 | $token = $expression; 60 | $prefix = $token[0]; 61 | 62 | // not a valid operator? 63 | if (!Operator\Abstraction::isValid($prefix)) { 64 | // not valid chars? 65 | if (!preg_match('#' . self::REGEX_VARNAME . '#', $token)) { 66 | throw new \InvalidArgumentException("Invalid operator [{$prefix}] found at {$token}"); 67 | } 68 | 69 | // default operator 70 | $prefix = null; 71 | } 72 | 73 | // remove operator prefix if exists e.g. '?' 74 | if ($prefix) { 75 | $token = substr($token, 1); 76 | } 77 | 78 | // parse variables 79 | $vars = []; 80 | foreach (explode(',', $token) as $var) { 81 | $vars[] = $this->parseVariable($var); 82 | } 83 | 84 | return $this->createExpressionNode( 85 | $token, 86 | $this->createOperatorNode($prefix), 87 | $vars, 88 | ); 89 | } 90 | 91 | protected function parseVariable(string $var): Variable 92 | { 93 | $var = trim($var); 94 | $val = null; 95 | $modifier = null; 96 | 97 | // check for prefix (:) / explode (*) / array (%) modifier 98 | if (str_contains($var, ':')) { 99 | $modifier = ':'; 100 | [$varname, $val] = explode(':', $var); 101 | 102 | // error checking 103 | if (!is_numeric($val)) { 104 | throw new \InvalidArgumentException("Value for `:` modifier must be numeric value [{$varname}:{$val}]"); 105 | } 106 | } 107 | 108 | switch ($last = substr($var, -1)) { 109 | case '*': 110 | case '%': 111 | // there can be only 1 modifier per var 112 | if ($modifier) { 113 | throw new \InvalidArgumentException("Multiple modifiers per variable are not allowed [{$var}]"); 114 | } 115 | 116 | $modifier = $last; 117 | $var = substr($var, 0, -1); 118 | 119 | break; 120 | } 121 | 122 | return $this->createVariableNode( 123 | $var, 124 | ['modifier' => $modifier, 'value' => $val], 125 | ); 126 | } 127 | 128 | protected function createVariableNode($token, $options = []): Variable 129 | { 130 | return new Variable($token, $options); 131 | } 132 | 133 | protected function createExpressionNode($token, ?Operator\Abstraction $operator = null, array $vars = []): Expression 134 | { 135 | return new Expression($token, $operator, $vars); 136 | } 137 | 138 | protected function createLiteralNode(string $token): Node\Literal 139 | { 140 | return new Node\Literal($token); 141 | } 142 | 143 | protected function createOperatorNode($token): Operator\Abstraction 144 | { 145 | return Operator\Abstraction::createById($token); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Rize/UriTemplate/UriTemplate.php: -------------------------------------------------------------------------------- 1 | ':', 'value' => 1], 26 | )], 27 | ), new Node\Literal('/'), new Node\Expression( 28 | 'term', 29 | Operator\Abstraction::createById(''), 30 | [new Node\Variable( 31 | 'term', 32 | ['modifier' => null, 'value' => null], 33 | )], 34 | ), new Node\Literal('/'), new Node\Expression( 35 | 'test*', 36 | Operator\Abstraction::createById(''), 37 | [new Node\Variable( 38 | 'test', 39 | ['modifier' => '*', 'value' => null], 40 | )], 41 | ), new Node\Literal('/foo'), new Node\Expression( 42 | 'query,number', 43 | Operator\Abstraction::createById('?'), 44 | [new Node\Variable( 45 | 'query', 46 | ['modifier' => null, 'value' => null], 47 | ), new Node\Variable( 48 | 'number', 49 | ['modifier' => null, 'value' => null], 50 | )], 51 | )]; 52 | 53 | $service = $this->service(); 54 | $actual = $service->parse($input); 55 | 56 | $this->assertEquals($expected, $actual); 57 | } 58 | 59 | public function testParseTemplateWithLiteral() 60 | { 61 | // will pass 62 | $uri = new UriTemplate('http://www.example.com/v1/company/', []); 63 | $params = $uri->extract('/{countryCode}/{registrationNumber}/test{.format}', '/gb/0123456/test.json'); 64 | static::assertEquals(['countryCode' => 'gb', 'registrationNumber' => '0123456', 'format' => 'json'], $params); 65 | } 66 | 67 | #[Depends('testParseTemplateWithLiteral')] 68 | public function testParseTemplateWithTwoVariablesAndDotBetween() 69 | { 70 | // will fail 71 | $uri = new UriTemplate('http://www.example.com/v1/company/', []); 72 | $params = $uri->extract('/{countryCode}/{registrationNumber}{.format}', '/gb/0123456.json'); 73 | static::assertEquals(['countryCode' => 'gb', 'registrationNumber' => '0123456', 'format' => 'json'], $params); 74 | } 75 | 76 | #[Depends('testParseTemplateWithLiteral')] 77 | public function testParseTemplateWithTwoVariablesAndDotBetweenStrict() 78 | { 79 | // will fail 80 | $uri = new UriTemplate('http://www.example.com/v1/company/', []); 81 | $params = $uri->extract('/{countryCode}/{registrationNumber}{.format}', '/gb/0123456.json', true); 82 | static::assertEquals(['countryCode' => 'gb', 'registrationNumber' => '0123456', 'format' => 'json'], $params); 83 | } 84 | 85 | #[Depends('testParseTemplateWithLiteral')] 86 | public function testParseTemplateWithThreeVariablesAndDotBetweenStrict() 87 | { 88 | // will fail 89 | $uri = new UriTemplate('http://www.example.com/v1/company/', []); 90 | $params = $uri->extract('/{countryCode}/{registrationNumber}{.namespace}{.format}', '/gb/0123456.company.json'); 91 | static::assertEquals(['countryCode' => 'gb', 'registrationNumber' => '0123456', 'namespace' => 'company', 'format' => 'json'], $params); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Rize/UriTemplateTest.php: -------------------------------------------------------------------------------- 1 | ["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]; 23 | 24 | return [ 25 | ['http://example.com/~john', ['uri' => 'http://example.com/~{username}', 'params' => ['username' => 'john']]], 26 | ['http://example.com/dictionary/d/dog', ['uri' => 'http://example.com/dictionary/{term:1}/{term}', 'params' => ['term' => 'dog'], 'extract' => ['term:1' => 'd', 'term' => 'dog']]], 27 | # Form-style parameters expression 28 | ['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']]], 29 | ['http://www.example.com/john', ['uri' => 'http://www.example.com/{username}', 'params' => ['username' => 'john']]], 30 | ['http://www.example.com/foo?query=mycelium&number=100', ['uri' => 'http://www.example.com/foo{?query,number}', 'params' => ['query' => 'mycelium', 'number' => 100]]], 31 | # 'query' is undefined 32 | ['http://www.example.com/foo?number=100', [ 33 | 'uri' => 'http://www.example.com/foo{?query,number}', 34 | 'params' => ['number' => 100], 35 | # we can't extract undefined values 36 | 'extract' => false, 37 | ]], 38 | # undefined variables 39 | ['http://www.example.com/foo', ['uri' => 'http://www.example.com/foo{?query,number}', 'params' => [], 'extract' => ['query' => null, 'number' => null]]], 40 | ['http://www.example.com/foo', ['uri' => 'http://www.example.com/foo{?number}', 'params' => [], 'extract' => ['number' => null]]], 41 | ['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']]]], 42 | ['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]]], 43 | # level 1 - Simple String Expansion: {var} 44 | ['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]], 45 | # level 2 - Reserved Expansion: {+var} 46 | ['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]], 47 | # level 2 - Fragment Expansion: {#var} 48 | ['#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]], 49 | # Label Expansion with Dot-Prefix: {.var} 50 | ['.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]], 51 | # Path Segment Expansion: {/var} 52 | ['/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]], 53 | # Path-Style Parameter Expansion: {;var} 54 | [';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]], 55 | # Form-Style Query Expansion: {?var} 56 | ['?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]], 57 | # Form-Style Query Continuation: {&var} 58 | ['&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]], 59 | # Test empty values 60 | ['|||', ['uri' => '{empty}|{empty*}|{?empty}|{?empty*}', 'params' => ['empty' => []]]], 61 | ]; 62 | } 63 | 64 | public static function dataExpandWithArrayModifier() 65 | { 66 | return [ 67 | # List 68 | [ 69 | # '?choices[]=a&choices[]=b&choices[]=c', 70 | '?choices%5B%5D=a&choices%5B%5D=b&choices%5B%5D=c', 71 | ['uri' => '{?choices%}', 'params' => ['choices' => ['a', 'b', 'c']]], 72 | ], 73 | # Keys 74 | [ 75 | # '?choices[a]=1&choices[b]=2&choices[c][test]=3', 76 | '?choices%5Ba%5D=1&choices%5Bb%5D=2&choices%5Bc%5D%5Btest%5D=3', 77 | ['uri' => '{?choices%}', 'params' => ['choices' => ['a' => 1, 'b' => 2, 'c' => ['test' => 3]]]], 78 | ], 79 | # Mixed 80 | [ 81 | # '?list[]=a&list[]=b&keys[a]=1&keys[b]=2', 82 | '?list%5B%5D=a&list%5B%5D=b&keys%5Ba%5D=1&keys%5Bb%5D=2', 83 | ['uri' => '{?list%,keys%}', 'params' => ['list' => ['a', 'b'], 'keys' => ['a' => 1, 'b' => 2]]], 84 | ], 85 | ]; 86 | } 87 | 88 | public static function dataBaseTemplate() 89 | { 90 | return [ 91 | [ 92 | 'http://google.com/api/1/users/1', 93 | # base uri 94 | ['uri' => '{+host}/api/{v}', 'params' => ['host' => 'http://google.com', 'v' => 1]], 95 | # other uri 96 | ['uri' => '/{resource}/{id}', 'params' => ['resource' => 'users', 'id' => 1]], 97 | ], 98 | # test override base params 99 | [ 100 | 'http://github.com/api/1/users/1', 101 | # base uri 102 | ['uri' => '{+host}/api/{v}', 'params' => ['host' => 'http://google.com', 'v' => 1]], 103 | # other uri 104 | ['uri' => '/{resource}/{id}', 'params' => ['host' => 'http://github.com', 'resource' => 'users', 'id' => 1]], 105 | ], 106 | ]; 107 | } 108 | 109 | public static function dataExtraction() 110 | { 111 | 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]]]; 112 | } 113 | 114 | /** 115 | * @dataProvider dataExpansion 116 | */ 117 | public function testExpansion($expected, $input) 118 | { 119 | $service = $this->service(); 120 | $result = $service->expand($input['uri'], $input['params']); 121 | 122 | $this->assertEquals($expected, $result); 123 | } 124 | 125 | /** 126 | * @dataProvider dataExpandWithArrayModifier 127 | */ 128 | public function testExpandWithArrayModifier($expected, $input) 129 | { 130 | $service = $this->service(); 131 | $result = $service->expand($input['uri'], $input['params']); 132 | 133 | $this->assertEquals($expected, $result); 134 | } 135 | 136 | /** 137 | * @dataProvider dataBaseTemplate 138 | */ 139 | public function testBaseTemplate($expected, $base, $other) 140 | { 141 | $service = $this->service($base['uri'], $base['params']); 142 | $result = $service->expand($other['uri'], $other['params']); 143 | 144 | $this->assertEquals($expected, $result); 145 | } 146 | 147 | /** 148 | * @dataProvider dataExtraction 149 | */ 150 | public function testExtract($template, $uri, $expected) 151 | { 152 | $service = $this->service(); 153 | $actual = $service->extract($template, $uri); 154 | 155 | $this->assertEquals($expected, $actual); 156 | } 157 | 158 | public function testExpandFromFixture() 159 | { 160 | $dir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'fixtures' . DIRECTORY_SEPARATOR; 161 | $files = ['spec-examples.json', 'spec-examples-by-section.json', 'extended-tests.json']; 162 | $service = $this->service(); 163 | 164 | foreach ($files as $file) { 165 | $content = json_decode(file_get_contents($dir . $file), $array = true); 166 | 167 | # iterate through each fixture 168 | foreach ($content as $fixture) { 169 | $vars = $fixture['variables']; 170 | 171 | # assert each test cases 172 | foreach ($fixture['testcases'] as $case) { 173 | [$uri, $expected] = $case; 174 | 175 | $actual = $service->expand($uri, $vars); 176 | 177 | if (is_array($expected)) { 178 | $expected = current(array_filter($expected, fn($input) => $actual === $input)); 179 | } 180 | 181 | $this->assertEquals($expected, $actual); 182 | } 183 | } 184 | } 185 | } 186 | 187 | public static function dataExtractStrictMode() 188 | { 189 | $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]], ['/', '/', []]]; 190 | 191 | $rfc3986AllowedPathCharacters = ['-', '.', '_', '~', '!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '=', ':', '@']; 192 | 193 | foreach ($rfc3986AllowedPathCharacters as $char) { 194 | $title = "RFC3986 path character ($char)"; 195 | $title = str_replace("'", 'single quote', $title); // PhpStorm workaround 196 | if ($char === ',') { // , means array on RFC6570 197 | $params = ['term' => ['foo', 'baz']]; 198 | } else { 199 | $params = ['term' => "foo{$char}baz"]; 200 | } 201 | 202 | $data = ['/search/{term}', "/search/foo{$char}baz", $params]; 203 | 204 | $dataTest[$title] = $data; 205 | $data = ['/search/{;term}', "/search/;term=foo{$char}baz", $params]; 206 | $dataTest['Named ' . $title] = $data; 207 | } 208 | 209 | $rfc3986AllowedQueryCharacters = $rfc3986AllowedPathCharacters; 210 | $rfc3986AllowedQueryCharacters[] = '/'; 211 | $rfc3986AllowedQueryCharacters[] = '?'; 212 | unset($rfc3986AllowedQueryCharacters[array_search('&', $rfc3986AllowedQueryCharacters, true)]); 213 | 214 | foreach ($rfc3986AllowedQueryCharacters as $char) { 215 | $title = "RFC3986 query character ($char)"; 216 | $title = str_replace("'", 'single quote', $title); // PhpStorm workaround 217 | if ($char === ',') { // , means array on RFC6570 218 | $params = ['term' => ['foo', 'baz']]; 219 | } else { 220 | $params = ['term' => "foo{$char}baz"]; 221 | } 222 | 223 | $data = ['/search/{?term}', "/search/?term=foo{$char}baz", $params]; 224 | $dataTest['Named ' . $title] = $data; 225 | } 226 | 227 | return $dataTest; 228 | } 229 | 230 | public static function extractStrictModeNotMatchProvider() 231 | { 232 | 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=']]; 233 | } 234 | 235 | #[DataProvider('dataExtractStrictMode')] 236 | public function testExtractStrictMode(string $template, string $uri, array $expectedParams) 237 | { 238 | $service = $this->service(); 239 | $params = $service->extract($template, $uri, true); 240 | 241 | $this->assertTrue(isset($params)); 242 | $this->assertEquals($expectedParams, $params); 243 | } 244 | 245 | #[DataProvider('extractStrictModeNotMatchProvider')] 246 | public function testExtractStrictModeNotMatch(string $template, string $uri) 247 | { 248 | $service = $this->service(); 249 | $actual = $service->extract($template, $uri, true); 250 | 251 | $this->assertFalse(isset($actual)); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /tests/fixtures/README.md: -------------------------------------------------------------------------------- 1 | 2 | URI Template Tests 3 | ================== 4 | 5 | This is a set of tests for implementations of 6 | [RFC6570](http://tools.ietf.org/html/rfc6570) - URI Template. It is designed 7 | to be reused by any implementation, to improve interoperability and 8 | implementation quality. 9 | 10 | If your project uses Git for version control, you can make uritemplate-tests into a [submodule](http://help.github.com/submodules/). 11 | 12 | Test Format 13 | ----------- 14 | 15 | Each test file is a [JSON](http://tools.ietf.org/html/RFC6627) document 16 | containing an object whose properties are groups of related tests. 17 | Alternatively, all tests are available in XML as well, with the XML files 18 | being generated by transform-json-tests.xslt which uses json2xml.xslt as a 19 | general-purpose JSON-to-XML parsing library. 20 | 21 | Each group, in turn, is an object with three children: 22 | 23 | * level - the level of the tests covered, as per the RFC (optional; if absent, 24 | assume level 4). 25 | * variables - an object representing the variables that are available to the 26 | tests in the suite 27 | * testcases - a list of testcases, where each case is a two-member list, the 28 | first being the template, the second being the result of expanding the 29 | template with the provided variables. 30 | 31 | Note that the result string can be a few different things: 32 | 33 | * string - if the second member is a string, the result of expansion is 34 | expected to match it, character-for-character. 35 | * list - if the second member is a list of strings, the result of expansion 36 | is expected to match one of them; this allows for templates that can 37 | expand into different, equally-acceptable URIs. 38 | * false - if the second member is boolean false, expansion is expected to 39 | fail (i.e., the template was invalid). 40 | 41 | For example: 42 | 43 | { 44 | "Level 1 Examples" : 45 | { 46 | "level": 1, 47 | "variables": { 48 | "var" : "value", 49 | "hello" : "Hello World!" 50 | }, 51 | "testcases" : [ 52 | ["{var}", "value"], 53 | ["{hello}", "Hello%20World%21"] 54 | ] 55 | } 56 | } 57 | 58 | 59 | Tests Included 60 | -------------- 61 | 62 | The following test files are included: 63 | 64 | * spec-examples.json - The complete set of example templates from the RFC 65 | * spec-examples-by-section.json - The examples, section by section 66 | * extended-tests.json - more complex test cases 67 | * negative-tests.json - invalid templates 68 | 69 | For all these test files, XML versions with the names *.xml can be 70 | generated with the transform-json-tests.xslt XSLT stylesheet. The XSLT 71 | contains the names of the above test files as a parameter, and can be 72 | started with any XML as input (i.e., the XML input is ignored). 73 | 74 | License 75 | ------- 76 | 77 | Copyright 2011-2012 The Authors 78 | 79 | Licensed under the Apache License, Version 2.0 (the "License"); 80 | you may not use this file except in compliance with the License. 81 | You may obtain a copy of the License at 82 | 83 | http://www.apache.org/licenses/LICENSE-2.0 84 | 85 | Unless required by applicable law or agreed to in writing, software 86 | distributed under the License is distributed on an "AS IS" BASIS, 87 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 88 | See the License for the specific language governing permissions and 89 | limitations under the License. 90 | 91 | -------------------------------------------------------------------------------- /tests/fixtures/extended-tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "Additional Examples 1":{ 3 | "level":4, 4 | "variables":{ 5 | "id" : "person", 6 | "token" : "12345", 7 | "fields" : ["id", "name", "picture"], 8 | "format" : "json", 9 | "q" : "URI Templates", 10 | "page" : "5", 11 | "lang" : "en", 12 | "geocode" : ["37.76","-122.427"], 13 | "first_name" : "John", 14 | "last.name" : "Doe", 15 | "Some%20Thing" : "foo", 16 | "number" : 6, 17 | "long" : 37.76, 18 | "lat" : -122.427, 19 | "group_id" : "12345", 20 | "query" : "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }", 21 | "uri" : "http://example.org/?uri=http%3A%2F%2Fexample.org%2F", 22 | "word" : "drücken", 23 | "Stra%C3%9Fe" : "Grüner Weg", 24 | "random" : "šö䟜ñꀣ¥‡ÑÒÓÔÕÖרÙÚàáâãäåæçÿ", 25 | "assoc_special_chars" : 26 | { "šö䟜ñꀣ¥‡ÑÒÓÔÕ" : "ÖרÙÚàáâãäåæçÿ" } 27 | }, 28 | "testcases":[ 29 | 30 | [ "{/id*}" , "/person" ], 31 | [ "{/id*}{?fields,first_name,last.name,token}" , [ 32 | "/person?fields=id,name,picture&first_name=John&last.name=Doe&token=12345", 33 | "/person?fields=id,picture,name&first_name=John&last.name=Doe&token=12345", 34 | "/person?fields=picture,name,id&first_name=John&last.name=Doe&token=12345", 35 | "/person?fields=picture,id,name&first_name=John&last.name=Doe&token=12345", 36 | "/person?fields=name,picture,id&first_name=John&last.name=Doe&token=12345", 37 | "/person?fields=name,id,picture&first_name=John&last.name=Doe&token=12345"] 38 | ], 39 | ["/search.{format}{?q,geocode,lang,locale,page,result_type}", 40 | [ "/search.json?q=URI%20Templates&geocode=37.76,-122.427&lang=en&page=5", 41 | "/search.json?q=URI%20Templates&geocode=-122.427,37.76&lang=en&page=5"] 42 | ], 43 | ["/test{/Some%20Thing}", "/test/foo" ], 44 | ["/set{?number}", "/set?number=6"], 45 | ["/loc{?long,lat}" , "/loc?long=37.76&lat=-122.427"], 46 | ["/base{/group_id,first_name}/pages{/page,lang}{?format,q}","/base/12345/John/pages/5/en?format=json&q=URI%20Templates"], 47 | ["/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"], 48 | ["/go{?uri}", "/go?uri=http%3A%2F%2Fexample.org%2F%3Furi%3Dhttp%253A%252F%252Fexample.org%252F"], 49 | ["/service{?word}", "/service?word=dr%C3%BCcken"], 50 | ["/lookup{?Stra%C3%9Fe}", "/lookup?Stra%25C3%259Fe=Gr%C3%BCner%20Weg"], 51 | ["{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"], 52 | ["{?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"] 53 | ] 54 | }, 55 | "Additional Examples 2":{ 56 | "level":4, 57 | "variables":{ 58 | "id" : ["person","albums"], 59 | "token" : "12345", 60 | "fields" : ["id", "name", "picture"], 61 | "format" : "atom", 62 | "q" : "URI Templates", 63 | "page" : "10", 64 | "start" : "5", 65 | "lang" : "en", 66 | "geocode" : ["37.76","-122.427"] 67 | }, 68 | "testcases":[ 69 | 70 | [ "{/id*}" , ["/person/albums","/albums/person"] ], 71 | [ "{/id*}{?fields,token}" , [ 72 | "/person/albums?fields=id,name,picture&token=12345", 73 | "/person/albums?fields=id,picture,name&token=12345", 74 | "/person/albums?fields=picture,name,id&token=12345", 75 | "/person/albums?fields=picture,id,name&token=12345", 76 | "/person/albums?fields=name,picture,id&token=12345", 77 | "/person/albums?fields=name,id,picture&token=12345", 78 | "/albums/person?fields=id,name,picture&token=12345", 79 | "/albums/person?fields=id,picture,name&token=12345", 80 | "/albums/person?fields=picture,name,id&token=12345", 81 | "/albums/person?fields=picture,id,name&token=12345", 82 | "/albums/person?fields=name,picture,id&token=12345", 83 | "/albums/person?fields=name,id,picture&token=12345"] 84 | ] 85 | ] 86 | }, 87 | "Additional Examples 3: Empty Variables":{ 88 | "variables" : { 89 | "empty_list" : [], 90 | "empty_assoc" : {} 91 | }, 92 | "testcases":[ 93 | [ "{/empty_list}", [ "" ] ], 94 | [ "{/empty_list*}", [ "" ] ], 95 | [ "{?empty_list}", [ ""] ], 96 | [ "{?empty_list*}", [ "" ] ], 97 | [ "{?empty_assoc}", [ "" ] ], 98 | [ "{?empty_assoc*}", [ "" ] ] 99 | ] 100 | }, 101 | "Additional Examples 4: Numeric Keys":{ 102 | "variables" : { 103 | "42" : "The Answer to the Ultimate Question of Life, the Universe, and Everything", 104 | "1337" : ["leet", "as","it", "can","be"], 105 | "german" : { 106 | "11": "elf", 107 | "12": "zwölf" 108 | } 109 | }, 110 | "testcases":[ 111 | [ "{42}", "The%20Answer%20to%20the%20Ultimate%20Question%20of%20Life%2C%20the%20Universe%2C%20and%20Everything"], 112 | [ "{?42}", "?42=The%20Answer%20to%20the%20Ultimate%20Question%20of%20Life%2C%20the%20Universe%2C%20and%20Everything"], 113 | [ "{1337}", "leet,as,it,can,be"], 114 | [ "{?1337*}", "?1337=leet&1337=as&1337=it&1337=can&1337=be"], 115 | [ "{?german*}", [ "?11=elf&12=zw%C3%B6lf", "?12=zw%C3%B6lf&11=elf"] ] 116 | ] 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/fixtures/json2xml.xslt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | \b 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | \v 60 | 61 | 62 | 63 | 64 | \f 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 174 | 175 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /tests/fixtures/negative-tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "Failure Tests":{ 3 | "level":4, 4 | "variables":{ 5 | "id" : "thing", 6 | "var" : "value", 7 | "hello" : "Hello World!", 8 | "with space" : "fail", 9 | " leading_space" : "Hi!", 10 | "trailing_space " : "Bye!", 11 | "empty" : "", 12 | "path" : "/foo/bar", 13 | "x" : "1024", 14 | "y" : "768", 15 | "list" : ["red", "green", "blue"], 16 | "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, 17 | "example" : "red", 18 | "searchTerms" : "uri templates", 19 | "~thing" : "some-user", 20 | "default-graph-uri" : ["http://www.example/book/","http://www.example/papers/"], 21 | "query" : "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }" 22 | 23 | }, 24 | "testcases":[ 25 | [ "{/id*", false ], 26 | [ "/id*}", false ], 27 | [ "{/?id}", false ], 28 | [ "{var:prefix}", false ], 29 | [ "{hello:2*}", false ] , 30 | [ "{??hello}", false ] , 31 | [ "{!hello}", false ] , 32 | [ "{with space}", false], 33 | [ "{ leading_space}", false], 34 | [ "{trailing_space }", false], 35 | [ "{=path}", false ] , 36 | [ "{$var}", false ], 37 | [ "{|var*}", false ], 38 | [ "{*keys?}", false ], 39 | [ "{?empty=default,var}", false ], 40 | [ "{var}{-prefix|/-/|var}" , false ], 41 | [ "?q={searchTerms}&c={example:color?}" , false ], 42 | [ "x{?empty|foo=none}" , false ], 43 | [ "/h{#hello+}" , false ], 44 | [ "/h#{hello+}" , false ], 45 | [ "{keys:1}", false ], 46 | [ "{+keys:1}", false ], 47 | [ "{;keys:1*}", false ], 48 | [ "?{-join|&|var,list}" , false ], 49 | [ "/people/{~thing}", false], 50 | [ "/{default-graph-uri}", false ], 51 | [ "/sparql{?query,default-graph-uri}", false ], 52 | [ "/sparql{?query){&default-graph-uri*}", false ], 53 | [ "/resolution{?x, y}" , false ] 54 | 55 | ] 56 | } 57 | } -------------------------------------------------------------------------------- /tests/fixtures/spec-examples-by-section.json: -------------------------------------------------------------------------------- 1 | { 2 | "3.2.1 Variable Expansion" : 3 | { 4 | "variables": { 5 | "count" : ["one", "two", "three"], 6 | "dom" : ["example", "com"], 7 | "dub" : "me/too", 8 | "hello" : "Hello World!", 9 | "half" : "50%", 10 | "var" : "value", 11 | "who" : "fred", 12 | "base" : "http://example.com/home/", 13 | "path" : "/foo/bar", 14 | "list" : ["red", "green", "blue"], 15 | "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, 16 | "v" : "6", 17 | "x" : "1024", 18 | "y" : "768", 19 | "empty" : "", 20 | "empty_keys" : [], 21 | "undef" : null 22 | }, 23 | "testcases" : [ 24 | ["{count}", "one,two,three"], 25 | ["{count*}", "one,two,three"], 26 | ["{/count}", "/one,two,three"], 27 | ["{/count*}", "/one/two/three"], 28 | ["{;count}", ";count=one,two,three"], 29 | ["{;count*}", ";count=one;count=two;count=three"], 30 | ["{?count}", "?count=one,two,three"], 31 | ["{?count*}", "?count=one&count=two&count=three"], 32 | ["{&count*}", "&count=one&count=two&count=three"] 33 | ] 34 | }, 35 | "3.2.2 Simple String Expansion" : 36 | { 37 | "variables": { 38 | "count" : ["one", "two", "three"], 39 | "dom" : ["example", "com"], 40 | "dub" : "me/too", 41 | "hello" : "Hello World!", 42 | "half" : "50%", 43 | "var" : "value", 44 | "who" : "fred", 45 | "base" : "http://example.com/home/", 46 | "path" : "/foo/bar", 47 | "list" : ["red", "green", "blue"], 48 | "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, 49 | "v" : "6", 50 | "x" : "1024", 51 | "y" : "768", 52 | "empty" : "", 53 | "empty_keys" : [], 54 | "undef" : null 55 | }, 56 | "testcases" : [ 57 | ["{var}", "value"], 58 | ["{hello}", "Hello%20World%21"], 59 | ["{half}", "50%25"], 60 | ["O{empty}X", "OX"], 61 | ["O{undef}X", "OX"], 62 | ["{x,y}", "1024,768"], 63 | ["{x,hello,y}", "1024,Hello%20World%21,768"], 64 | ["?{x,empty}", "?1024,"], 65 | ["?{x,undef}", "?1024"], 66 | ["?{undef,y}", "?768"], 67 | ["{var:3}", "val"], 68 | ["{var:30}", "value"], 69 | ["{list}", "red,green,blue"], 70 | ["{list*}", "red,green,blue"], 71 | ["{keys}", [ 72 | "comma,%2C,dot,.,semi,%3B", 73 | "comma,%2C,semi,%3B,dot,.", 74 | "dot,.,comma,%2C,semi,%3B", 75 | "dot,.,semi,%3B,comma,%2C", 76 | "semi,%3B,comma,%2C,dot,.", 77 | "semi,%3B,dot,.,comma,%2C" 78 | ]], 79 | ["{keys*}", [ 80 | "comma=%2C,dot=.,semi=%3B", 81 | "comma=%2C,semi=%3B,dot=.", 82 | "dot=.,comma=%2C,semi=%3B", 83 | "dot=.,semi=%3B,comma=%2C", 84 | "semi=%3B,comma=%2C,dot=.", 85 | "semi=%3B,dot=.,comma=%2C" 86 | ]] 87 | ] 88 | }, 89 | "3.2.3 Reserved Expansion" : 90 | { 91 | "variables": { 92 | "count" : ["one", "two", "three"], 93 | "dom" : ["example", "com"], 94 | "dub" : "me/too", 95 | "hello" : "Hello World!", 96 | "half" : "50%", 97 | "var" : "value", 98 | "who" : "fred", 99 | "base" : "http://example.com/home/", 100 | "path" : "/foo/bar", 101 | "list" : ["red", "green", "blue"], 102 | "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, 103 | "v" : "6", 104 | "x" : "1024", 105 | "y" : "768", 106 | "empty" : "", 107 | "empty_keys" : [], 108 | "undef" : null 109 | }, 110 | "testcases" : [ 111 | ["{+var}", "value"], 112 | ["{/var,empty}", "/value/"], 113 | ["{/var,undef}", "/value"], 114 | ["{+hello}", "Hello%20World!"], 115 | ["{+half}", "50%25"], 116 | ["{base}index", "http%3A%2F%2Fexample.com%2Fhome%2Findex"], 117 | ["{+base}index", "http://example.com/home/index"], 118 | ["O{+empty}X", "OX"], 119 | ["O{+undef}X", "OX"], 120 | ["{+path}/here", "/foo/bar/here"], 121 | ["{+path:6}/here", "/foo/b/here"], 122 | ["here?ref={+path}", "here?ref=/foo/bar"], 123 | ["up{+path}{var}/here", "up/foo/barvalue/here"], 124 | ["{+x,hello,y}", "1024,Hello%20World!,768"], 125 | ["{+path,x}/here", "/foo/bar,1024/here"], 126 | ["{+list}", "red,green,blue"], 127 | ["{+list*}", "red,green,blue"], 128 | ["{+keys}", [ 129 | "comma,,,dot,.,semi,;", 130 | "comma,,,semi,;,dot,.", 131 | "dot,.,comma,,,semi,;", 132 | "dot,.,semi,;,comma,,", 133 | "semi,;,comma,,,dot,.", 134 | "semi,;,dot,.,comma,," 135 | ]], 136 | ["{+keys*}", [ 137 | "comma=,,dot=.,semi=;", 138 | "comma=,,semi=;,dot=.", 139 | "dot=.,comma=,,semi=;", 140 | "dot=.,semi=;,comma=,", 141 | "semi=;,comma=,,dot=.", 142 | "semi=;,dot=.,comma=," 143 | ]] 144 | ] 145 | }, 146 | "3.2.4 Fragment Expansion" : 147 | { 148 | "variables": { 149 | "count" : ["one", "two", "three"], 150 | "dom" : ["example", "com"], 151 | "dub" : "me/too", 152 | "hello" : "Hello World!", 153 | "half" : "50%", 154 | "var" : "value", 155 | "who" : "fred", 156 | "base" : "http://example.com/home/", 157 | "path" : "/foo/bar", 158 | "list" : ["red", "green", "blue"], 159 | "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, 160 | "v" : "6", 161 | "x" : "1024", 162 | "y" : "768", 163 | "empty" : "", 164 | "empty_keys" : [], 165 | "undef" : null 166 | }, 167 | "testcases" : [ 168 | ["{#var}", "#value"], 169 | ["{#hello}", "#Hello%20World!"], 170 | ["{#half}", "#50%25"], 171 | ["foo{#empty}", "foo#"], 172 | ["foo{#undef}", "foo"], 173 | ["{#x,hello,y}", "#1024,Hello%20World!,768"], 174 | ["{#path,x}/here", "#/foo/bar,1024/here"], 175 | ["{#path:6}/here", "#/foo/b/here"], 176 | ["{#list}", "#red,green,blue"], 177 | ["{#list*}", "#red,green,blue"], 178 | ["{#keys}", [ 179 | "#comma,,,dot,.,semi,;", 180 | "#comma,,,semi,;,dot,.", 181 | "#dot,.,comma,,,semi,;", 182 | "#dot,.,semi,;,comma,,", 183 | "#semi,;,comma,,,dot,.", 184 | "#semi,;,dot,.,comma,," 185 | ]] 186 | ] 187 | }, 188 | "3.2.5 Label Expansion with Dot-Prefix" : 189 | { 190 | "variables": { 191 | "count" : ["one", "two", "three"], 192 | "dom" : ["example", "com"], 193 | "dub" : "me/too", 194 | "hello" : "Hello World!", 195 | "half" : "50%", 196 | "var" : "value", 197 | "who" : "fred", 198 | "base" : "http://example.com/home/", 199 | "path" : "/foo/bar", 200 | "list" : ["red", "green", "blue"], 201 | "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, 202 | "v" : "6", 203 | "x" : "1024", 204 | "y" : "768", 205 | "empty" : "", 206 | "empty_keys" : [], 207 | "undef" : null 208 | }, 209 | "testcases" : [ 210 | ["{.who}", ".fred"], 211 | ["{.who,who}", ".fred.fred"], 212 | ["{.half,who}", ".50%25.fred"], 213 | ["www{.dom*}", "www.example.com"], 214 | ["X{.var}", "X.value"], 215 | ["X{.var:3}", "X.val"], 216 | ["X{.empty}", "X."], 217 | ["X{.undef}", "X"], 218 | ["X{.list}", "X.red,green,blue"], 219 | ["X{.list*}", "X.red.green.blue"], 220 | ["{#keys}", [ 221 | "#comma,,,dot,.,semi,;", 222 | "#comma,,,semi,;,dot,.", 223 | "#dot,.,comma,,,semi,;", 224 | "#dot,.,semi,;,comma,,", 225 | "#semi,;,comma,,,dot,.", 226 | "#semi,;,dot,.,comma,," 227 | ]], 228 | ["{#keys*}", [ 229 | "#comma=,,dot=.,semi=;", 230 | "#comma=,,semi=;,dot=.", 231 | "#dot=.,comma=,,semi=;", 232 | "#dot=.,semi=;,comma=,", 233 | "#semi=;,comma=,,dot=.", 234 | "#semi=;,dot=.,comma=," 235 | ]], 236 | ["X{.empty_keys}", "X"], 237 | ["X{.empty_keys*}", "X"] 238 | ] 239 | }, 240 | "3.2.6 Path Segment Expansion" : 241 | { 242 | "variables": { 243 | "count" : ["one", "two", "three"], 244 | "dom" : ["example", "com"], 245 | "dub" : "me/too", 246 | "hello" : "Hello World!", 247 | "half" : "50%", 248 | "var" : "value", 249 | "who" : "fred", 250 | "base" : "http://example.com/home/", 251 | "path" : "/foo/bar", 252 | "list" : ["red", "green", "blue"], 253 | "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, 254 | "v" : "6", 255 | "x" : "1024", 256 | "y" : "768", 257 | "empty" : "", 258 | "empty_keys" : [], 259 | "undef" : null 260 | }, 261 | "testcases" : [ 262 | ["{/who}", "/fred"], 263 | ["{/who,who}", "/fred/fred"], 264 | ["{/half,who}", "/50%25/fred"], 265 | ["{/who,dub}", "/fred/me%2Ftoo"], 266 | ["{/var}", "/value"], 267 | ["{/var,empty}", "/value/"], 268 | ["{/var,undef}", "/value"], 269 | ["{/var,x}/here", "/value/1024/here"], 270 | ["{/var:1,var}", "/v/value"], 271 | ["{/list}", "/red,green,blue"], 272 | ["{/list*}", "/red/green/blue"], 273 | ["{/list*,path:4}", "/red/green/blue/%2Ffoo"], 274 | ["{/keys}", [ 275 | "/comma,%2C,dot,.,semi,%3B", 276 | "/comma,%2C,semi,%3B,dot,.", 277 | "/dot,.,comma,%2C,semi,%3B", 278 | "/dot,.,semi,%3B,comma,%2C", 279 | "/semi,%3B,comma,%2C,dot,.", 280 | "/semi,%3B,dot,.,comma,%2C" 281 | ]], 282 | ["{/keys*}", [ 283 | "/comma=%2C/dot=./semi=%3B", 284 | "/comma=%2C/semi=%3B/dot=.", 285 | "/dot=./comma=%2C/semi=%3B", 286 | "/dot=./semi=%3B/comma=%2C", 287 | "/semi=%3B/comma=%2C/dot=.", 288 | "/semi=%3B/dot=./comma=%2C" 289 | ]] 290 | ] 291 | }, 292 | "3.2.7 Path-Style Parameter Expansion" : 293 | { 294 | "variables": { 295 | "count" : ["one", "two", "three"], 296 | "dom" : ["example", "com"], 297 | "dub" : "me/too", 298 | "hello" : "Hello World!", 299 | "half" : "50%", 300 | "var" : "value", 301 | "who" : "fred", 302 | "base" : "http://example.com/home/", 303 | "path" : "/foo/bar", 304 | "list" : ["red", "green", "blue"], 305 | "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, 306 | "v" : "6", 307 | "x" : "1024", 308 | "y" : "768", 309 | "empty" : "", 310 | "empty_keys" : [], 311 | "undef" : null 312 | }, 313 | "testcases" : [ 314 | ["{;who}", ";who=fred"], 315 | ["{;half}", ";half=50%25"], 316 | ["{;empty}", ";empty"], 317 | ["{;hello:5}", ";hello=Hello"], 318 | ["{;v,empty,who}", ";v=6;empty;who=fred"], 319 | ["{;v,bar,who}", ";v=6;who=fred"], 320 | ["{;x,y}", ";x=1024;y=768"], 321 | ["{;x,y,empty}", ";x=1024;y=768;empty"], 322 | ["{;x,y,undef}", ";x=1024;y=768"], 323 | ["{;list}", ";list=red,green,blue"], 324 | ["{;list*}", ";list=red;list=green;list=blue"], 325 | ["{;keys}", [ 326 | ";keys=comma,%2C,dot,.,semi,%3B", 327 | ";keys=comma,%2C,semi,%3B,dot,.", 328 | ";keys=dot,.,comma,%2C,semi,%3B", 329 | ";keys=dot,.,semi,%3B,comma,%2C", 330 | ";keys=semi,%3B,comma,%2C,dot,.", 331 | ";keys=semi,%3B,dot,.,comma,%2C" 332 | ]], 333 | ["{;keys*}", [ 334 | ";comma=%2C;dot=.;semi=%3B", 335 | ";comma=%2C;semi=%3B;dot=.", 336 | ";dot=.;comma=%2C;semi=%3B", 337 | ";dot=.;semi=%3B;comma=%2C", 338 | ";semi=%3B;comma=%2C;dot=.", 339 | ";semi=%3B;dot=.;comma=%2C" 340 | ]] 341 | ] 342 | }, 343 | "3.2.8 Form-Style Query Expansion" : 344 | { 345 | "variables": { 346 | "count" : ["one", "two", "three"], 347 | "dom" : ["example", "com"], 348 | "dub" : "me/too", 349 | "hello" : "Hello World!", 350 | "half" : "50%", 351 | "var" : "value", 352 | "who" : "fred", 353 | "base" : "http://example.com/home/", 354 | "path" : "/foo/bar", 355 | "list" : ["red", "green", "blue"], 356 | "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, 357 | "v" : "6", 358 | "x" : "1024", 359 | "y" : "768", 360 | "empty" : "", 361 | "empty_keys" : [], 362 | "undef" : null 363 | }, 364 | "testcases" : [ 365 | ["{?who}", "?who=fred"], 366 | ["{?half}", "?half=50%25"], 367 | ["{?x,y}", "?x=1024&y=768"], 368 | ["{?x,y,empty}", "?x=1024&y=768&empty="], 369 | ["{?x,y,undef}", "?x=1024&y=768"], 370 | ["{?var:3}", "?var=val"], 371 | ["{?list}", "?list=red,green,blue"], 372 | ["{?list*}", "?list=red&list=green&list=blue"], 373 | ["{?keys}", [ 374 | "?keys=comma,%2C,dot,.,semi,%3B", 375 | "?keys=comma,%2C,semi,%3B,dot,.", 376 | "?keys=dot,.,comma,%2C,semi,%3B", 377 | "?keys=dot,.,semi,%3B,comma,%2C", 378 | "?keys=semi,%3B,comma,%2C,dot,.", 379 | "?keys=semi,%3B,dot,.,comma,%2C" 380 | ]], 381 | ["{?keys*}", [ 382 | "?comma=%2C&dot=.&semi=%3B", 383 | "?comma=%2C&semi=%3B&dot=.", 384 | "?dot=.&comma=%2C&semi=%3B", 385 | "?dot=.&semi=%3B&comma=%2C", 386 | "?semi=%3B&comma=%2C&dot=.", 387 | "?semi=%3B&dot=.&comma=%2C" 388 | ]] 389 | ] 390 | }, 391 | "3.2.9 Form-Style Query Continuation" : 392 | { 393 | "variables": { 394 | "count" : ["one", "two", "three"], 395 | "dom" : ["example", "com"], 396 | "dub" : "me/too", 397 | "hello" : "Hello World!", 398 | "half" : "50%", 399 | "var" : "value", 400 | "who" : "fred", 401 | "base" : "http://example.com/home/", 402 | "path" : "/foo/bar", 403 | "list" : ["red", "green", "blue"], 404 | "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, 405 | "v" : "6", 406 | "x" : "1024", 407 | "y" : "768", 408 | "empty" : "", 409 | "empty_keys" : [], 410 | "undef" : null 411 | }, 412 | "testcases" : [ 413 | ["{&who}", "&who=fred"], 414 | ["{&half}", "&half=50%25"], 415 | ["?fixed=yes{&x}", "?fixed=yes&x=1024"], 416 | ["{&var:3}", "&var=val"], 417 | ["{&x,y,empty}", "&x=1024&y=768&empty="], 418 | ["{&x,y,undef}", "&x=1024&y=768"], 419 | ["{&list}", "&list=red,green,blue"], 420 | ["{&list*}", "&list=red&list=green&list=blue"], 421 | ["{&keys}", [ 422 | "&keys=comma,%2C,dot,.,semi,%3B", 423 | "&keys=comma,%2C,semi,%3B,dot,.", 424 | "&keys=dot,.,comma,%2C,semi,%3B", 425 | "&keys=dot,.,semi,%3B,comma,%2C", 426 | "&keys=semi,%3B,comma,%2C,dot,.", 427 | "&keys=semi,%3B,dot,.,comma,%2C" 428 | ]], 429 | ["{&keys*}", [ 430 | "&comma=%2C&dot=.&semi=%3B", 431 | "&comma=%2C&semi=%3B&dot=.", 432 | "&dot=.&comma=%2C&semi=%3B", 433 | "&dot=.&semi=%3B&comma=%2C", 434 | "&semi=%3B&comma=%2C&dot=.", 435 | "&semi=%3B&dot=.&comma=%2C" 436 | ]] 437 | ] 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /tests/fixtures/spec-examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "Level 1 Examples" : 3 | { 4 | "level": 1, 5 | "variables": { 6 | "var" : "value", 7 | "hello" : "Hello World!" 8 | }, 9 | "testcases" : [ 10 | ["{var}", "value"], 11 | ["{hello}", "Hello%20World%21"] 12 | ] 13 | }, 14 | "Level 2 Examples" : 15 | { 16 | "level": 2, 17 | "variables": { 18 | "var" : "value", 19 | "hello" : "Hello World!", 20 | "path" : "/foo/bar" 21 | }, 22 | "testcases" : [ 23 | ["{+var}", "value"], 24 | ["{+hello}", "Hello%20World!"], 25 | ["{+path}/here", "/foo/bar/here"], 26 | ["here?ref={+path}", "here?ref=/foo/bar"] 27 | ] 28 | }, 29 | "Level 3 Examples" : 30 | { 31 | "level": 3, 32 | "variables": { 33 | "var" : "value", 34 | "hello" : "Hello World!", 35 | "empty" : "", 36 | "path" : "/foo/bar", 37 | "x" : "1024", 38 | "y" : "768" 39 | }, 40 | "testcases" : [ 41 | ["map?{x,y}", "map?1024,768"], 42 | ["{x,hello,y}", "1024,Hello%20World%21,768"], 43 | ["{+x,hello,y}", "1024,Hello%20World!,768"], 44 | ["{+path,x}/here", "/foo/bar,1024/here"], 45 | ["{#x,hello,y}", "#1024,Hello%20World!,768"], 46 | ["{#path,x}/here", "#/foo/bar,1024/here"], 47 | ["X{.var}", "X.value"], 48 | ["X{.x,y}", "X.1024.768"], 49 | ["{/var}", "/value"], 50 | ["{/var,x}/here", "/value/1024/here"], 51 | ["{;x,y}", ";x=1024;y=768"], 52 | ["{;x,y,empty}", ";x=1024;y=768;empty"], 53 | ["{?x,y}", "?x=1024&y=768"], 54 | ["{?x,y,empty}", "?x=1024&y=768&empty="], 55 | ["?fixed=yes{&x}", "?fixed=yes&x=1024"], 56 | ["{&x,y,empty}", "&x=1024&y=768&empty="] 57 | ] 58 | }, 59 | "Level 4 Examples" : 60 | { 61 | "level": 4, 62 | "variables": { 63 | "var": "value", 64 | "hello": "Hello World!", 65 | "path": "/foo/bar", 66 | "list": ["red", "green", "blue"], 67 | "keys": {"semi": ";", "dot": ".", "comma":","} 68 | }, 69 | "testcases": [ 70 | ["{var:3}", "val"], 71 | ["{var:30}", "value"], 72 | ["{list}", "red,green,blue"], 73 | ["{list*}", "red,green,blue"], 74 | ["{keys}", [ 75 | "comma,%2C,dot,.,semi,%3B", 76 | "comma,%2C,semi,%3B,dot,.", 77 | "dot,.,comma,%2C,semi,%3B", 78 | "dot,.,semi,%3B,comma,%2C", 79 | "semi,%3B,comma,%2C,dot,.", 80 | "semi,%3B,dot,.,comma,%2C" 81 | ]], 82 | ["{keys*}", [ 83 | "comma=%2C,dot=.,semi=%3B", 84 | "comma=%2C,semi=%3B,dot=.", 85 | "dot=.,comma=%2C,semi=%3B", 86 | "dot=.,semi=%3B,comma=%2C", 87 | "semi=%3B,comma=%2C,dot=.", 88 | "semi=%3B,dot=.,comma=%2C" 89 | ]], 90 | ["{+path:6}/here", "/foo/b/here"], 91 | ["{+list}", "red,green,blue"], 92 | ["{+list*}", "red,green,blue"], 93 | ["{+keys}", [ 94 | "comma,,,dot,.,semi,;", 95 | "comma,,,semi,;,dot,.", 96 | "dot,.,comma,,,semi,;", 97 | "dot,.,semi,;,comma,,", 98 | "semi,;,comma,,,dot,.", 99 | "semi,;,dot,.,comma,," 100 | ]], 101 | ["{+keys*}", [ 102 | "comma=,,dot=.,semi=;", 103 | "comma=,,semi=;,dot=.", 104 | "dot=.,comma=,,semi=;", 105 | "dot=.,semi=;,comma=,", 106 | "semi=;,comma=,,dot=.", 107 | "semi=;,dot=.,comma=," 108 | ]], 109 | ["{#path:6}/here", "#/foo/b/here"], 110 | ["{#list}", "#red,green,blue"], 111 | ["{#list*}", "#red,green,blue"], 112 | ["{#keys}", [ 113 | "#comma,,,dot,.,semi,;", 114 | "#comma,,,semi,;,dot,.", 115 | "#dot,.,comma,,,semi,;", 116 | "#dot,.,semi,;,comma,,", 117 | "#semi,;,comma,,,dot,.", 118 | "#semi,;,dot,.,comma,," 119 | ]], 120 | ["{#keys*}", [ 121 | "#comma=,,dot=.,semi=;", 122 | "#comma=,,semi=;,dot=.", 123 | "#dot=.,comma=,,semi=;", 124 | "#dot=.,semi=;,comma=,", 125 | "#semi=;,comma=,,dot=.", 126 | "#semi=;,dot=.,comma=," 127 | ]], 128 | ["X{.var:3}", "X.val"], 129 | ["X{.list}", "X.red,green,blue"], 130 | ["X{.list*}", "X.red.green.blue"], 131 | ["X{.keys}", [ 132 | "X.comma,%2C,dot,.,semi,%3B", 133 | "X.comma,%2C,semi,%3B,dot,.", 134 | "X.dot,.,comma,%2C,semi,%3B", 135 | "X.dot,.,semi,%3B,comma,%2C", 136 | "X.semi,%3B,comma,%2C,dot,.", 137 | "X.semi,%3B,dot,.,comma,%2C" 138 | ]], 139 | ["{/var:1,var}", "/v/value"], 140 | ["{/list}", "/red,green,blue"], 141 | ["{/list*}", "/red/green/blue"], 142 | ["{/list*,path:4}", "/red/green/blue/%2Ffoo"], 143 | ["{/keys}", [ 144 | "/comma,%2C,dot,.,semi,%3B", 145 | "/comma,%2C,semi,%3B,dot,.", 146 | "/dot,.,comma,%2C,semi,%3B", 147 | "/dot,.,semi,%3B,comma,%2C", 148 | "/semi,%3B,comma,%2C,dot,.", 149 | "/semi,%3B,dot,.,comma,%2C" 150 | ]], 151 | ["{/keys*}", [ 152 | "/comma=%2C/dot=./semi=%3B", 153 | "/comma=%2C/semi=%3B/dot=.", 154 | "/dot=./comma=%2C/semi=%3B", 155 | "/dot=./semi=%3B/comma=%2C", 156 | "/semi=%3B/comma=%2C/dot=.", 157 | "/semi=%3B/dot=./comma=%2C" 158 | ]], 159 | ["{;hello:5}", ";hello=Hello"], 160 | ["{;list}", ";list=red,green,blue"], 161 | ["{;list*}", ";list=red;list=green;list=blue"], 162 | ["{;keys}", [ 163 | ";keys=comma,%2C,dot,.,semi,%3B", 164 | ";keys=comma,%2C,semi,%3B,dot,.", 165 | ";keys=dot,.,comma,%2C,semi,%3B", 166 | ";keys=dot,.,semi,%3B,comma,%2C", 167 | ";keys=semi,%3B,comma,%2C,dot,.", 168 | ";keys=semi,%3B,dot,.,comma,%2C" 169 | ]], 170 | ["{;keys*}", [ 171 | ";comma=%2C;dot=.;semi=%3B", 172 | ";comma=%2C;semi=%3B;dot=.", 173 | ";dot=.;comma=%2C;semi=%3B", 174 | ";dot=.;semi=%3B;comma=%2C", 175 | ";semi=%3B;comma=%2C;dot=.", 176 | ";semi=%3B;dot=.;comma=%2C" 177 | ]], 178 | ["{?var:3}", "?var=val"], 179 | ["{?list}", "?list=red,green,blue"], 180 | ["{?list*}", "?list=red&list=green&list=blue"], 181 | ["{?keys}", [ 182 | "?keys=comma,%2C,dot,.,semi,%3B", 183 | "?keys=comma,%2C,semi,%3B,dot,.", 184 | "?keys=dot,.,comma,%2C,semi,%3B", 185 | "?keys=dot,.,semi,%3B,comma,%2C", 186 | "?keys=semi,%3B,comma,%2C,dot,.", 187 | "?keys=semi,%3B,dot,.,comma,%2C" 188 | ]], 189 | ["{?keys*}", [ 190 | "?comma=%2C&dot=.&semi=%3B", 191 | "?comma=%2C&semi=%3B&dot=.", 192 | "?dot=.&comma=%2C&semi=%3B", 193 | "?dot=.&semi=%3B&comma=%2C", 194 | "?semi=%3B&comma=%2C&dot=.", 195 | "?semi=%3B&dot=.&comma=%2C" 196 | ]], 197 | ["{&var:3}", "&var=val"], 198 | ["{&list}", "&list=red,green,blue"], 199 | ["{&list*}", "&list=red&list=green&list=blue"], 200 | ["{&keys}", [ 201 | "&keys=comma,%2C,dot,.,semi,%3B", 202 | "&keys=comma,%2C,semi,%3B,dot,.", 203 | "&keys=dot,.,comma,%2C,semi,%3B", 204 | "&keys=dot,.,semi,%3B,comma,%2C", 205 | "&keys=semi,%3B,comma,%2C,dot,.", 206 | "&keys=semi,%3B,dot,.,comma,%2C" 207 | ]], 208 | ["{&keys*}", [ 209 | "&comma=%2C&dot=.&semi=%3B", 210 | "&comma=%2C&semi=%3B&dot=.", 211 | "&dot=.&comma=%2C&semi=%3B", 212 | "&dot=.&semi=%3B&comma=%2C", 213 | "&semi=%3B&comma=%2C&dot=.", 214 | "&semi=%3B&dot=.&comma=%2C" 215 | ]] 216 | ] 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /tests/fixtures/transform-json-tests.xslt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | --------------------------------------------------------------------------------