├── .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 |  [](https://packagist.org/packages/rize/uri-template) [](https://packagist.org/packages/rize/uri-template) [](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 |
--------------------------------------------------------------------------------