url-templates 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid non strings",
3
+ "data": 12,
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid key character (contains !)",
3
+ "data": "{fo!o}",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid consecutive varspec delimiters , ,",
3
+ "data": "{foo,,bar}",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid consecutive prefix delimiters ::",
3
+ "data": "{foo::3}",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid only literal (no expressions)",
3
+ "data": "abc-XYZ_123~",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid literal before, expression, literal after",
3
+ "data": "path/{foo}/end",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid literal before, expression, literal middle, expression, literal after",
3
+ "data": "x{foo}y{bar}z",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid expression: single key, no operator",
3
+ "data": "{foo}",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid expression: single key with operator +",
3
+ "data": "{+foo}",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid expression: single key with prefix",
3
+ "data": "{foo:3}",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid expression: single key with explode",
3
+ "data": "{foo*}",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid expression: multiple keys separated by comma, each having prefix or explode",
3
+ "data": "{foo:2,bar*,baz:4}",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid operator (invalid symbol ^)",
3
+ "data": "{^foo}",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid expression: with operator and multiple keys",
3
+ "data": "{+foo,bar*}",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid percent encoding (incomplete)",
3
+ "data": "{foo%2}",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid percent-encoded key",
3
+ "data": "{foo%20bar}",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid literal contains control character",
3
+ "data": "abc\u0001def",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "valid mix of multiple expressions and literals",
3
+ "data": "http://example.com/{+path}/details/{id*}",
4
+ "valid": true
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid literal character (space not allowed)",
3
+ "data": "foo bar",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid char after explode modifier",
3
+ "data": "{foo*1}",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid prefix type (non-digit after :)",
3
+ "data": "{foo:ab}",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid prefix length (too large number)",
3
+ "data": "{foo:12345}",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid { in literal part",
3
+ "data": "foo{bar",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid } in literal part",
3
+ "data": "foo}bar",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid modifier having both prefix and explode",
3
+ "data": "{foo:3*}",
4
+ "valid": false
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "invalid empty key",
3
+ "data": "{,foo}",
4
+ "valid": false
5
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "$comment": "dry validation related tests only",
3
+ "$ref": "#/#/#/#/uri-template"
4
+ }
package/index.js CHANGED
@@ -5,7 +5,11 @@ const operators = new Set(['+', '#', '.', '/', '?', '&', ';']);
5
5
  const separator = (operator) => (operator === '' || operator === '+' || operator == '#' ? ',' : operator === '?' ? '&' : operator);
6
6
  const isDefined = (value) => value !== undefined && value !== null;
7
7
  const encodeUnreserved = (string) => encodeURIComponent(string).replace(/[!'()*]/g, (char) => '%' + char.charCodeAt(0).toString(16).toUpperCase());
8
- const encodeReserved = (string) => string.split(/(%[0-9A-Fa-f]{2})/g).map((part, i) => (i % 2 ? part : encodeURI(part).replace(/%5B/g, '[').replace(/%5D/g, ']'))).join('');
8
+ const encodeReserved = (string) =>
9
+ string
10
+ .split(/(%[0-9A-Fa-f]{2})/g)
11
+ .map((part, i) => (i % 2 ? part : encodeURI(part).replace(/%5B/g, '[').replace(/%5D/g, ']')))
12
+ .join('');
9
13
  const encodeValue = (operator, value, key) => {
10
14
  value = operator === '+' || operator === '#' ? encodeReserved(value) : encodeUnreserved(value);
11
15
  return key ? encodeReserved(key) + '=' + value : value;
@@ -14,28 +18,32 @@ const ast = [];
14
18
  // url-template validator
15
19
  function isUrlTemplate(template, inspect) {
16
20
  if (typeof template !== 'string') throw new TypeError('uri-template must be a string.');
17
- if (!/^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%{}]*$/.test(template)) throw new Error('invalid character(s) in uri-template.');
21
+ if (!/^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%{}]*$/.test(template)) throw new SyntaxError('invalid character(s) in uri-template.');
18
22
  for (let i = 0; i < template.length; ) {
19
23
  const start = template.indexOf('{', i);
20
- if ((start === -1 && template.indexOf('}', i) > start) || template.indexOf('}', i) < start) throw new Error('unstarted expression in uri-template');
21
- if (start === -1) { inspect && ast.push(template.slice(i)); break; }
24
+ const nextClose = template.indexOf('}', i);
25
+ if (nextClose !== -1 && (start === -1 || nextClose < start)) throw new SyntaxError(`at index ${nextClose}: unstarted expression in uri-template.`);
26
+ if (start === -1) {
27
+ inspect && ast.push(template.slice(i));
28
+ break;
29
+ }
22
30
  if (inspect && start > i) ast.push(template.slice(i, start));
23
31
  const end = template.indexOf('}', start + 1);
24
- if (end === -1) throw new Error('unterminated expression in uri-template.');
32
+ const nestedStart = template.indexOf('{', start + 1);
33
+ if (end === -1 || (nestedStart !== -1 && nestedStart < end)) throw new SyntaxError(`at index ${start}: unterminated expression in uri-template.`);
25
34
  let expression = template.slice(start + 1, end);
26
- if (expression.length === 0) throw new Error('empty expression.');
35
+ if (expression.length === 0) throw new SyntaxError(`at index ${start + 1}: empty expression.`);
27
36
  const first = expression[0];
28
37
  const operator = operators.has(first) ? ((expression = expression.slice(1)), first) : '';
29
- if (expression.length === 0) throw new Error('expression missing variable names.');
38
+ if (expression.length === 0) throw new SyntaxError(`at index ${start + 2}: expression missing variable names.`);
30
39
  const varspecs = expression.split(',').map((key) => {
31
40
  const colon = key.indexOf(':');
32
41
  const limit = colon !== -1 ? Number(key.slice(colon + 1)) : null;
33
- if (isDefined(limit) && (limit % 1 !== 0 || isNaN(limit) || limit < 1 || limit > 9999)) throw new Error('invalid limit modifier.');
42
+ if (isDefined(limit) && (limit % 1 !== 0 || isNaN(limit) || limit < 1 || limit > 9999)) throw new SyntaxError(`at index ${start}: invalid limit modifier.`);
34
43
  if (limit) key = key.slice(0, colon);
35
44
  const explode = key.endsWith('*');
36
- if (explode && limit) throw new Error('invalid modifier having both explode and limit.');
37
45
  if (explode) key = key.slice(0, -1);
38
- if (!/^(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+)*$/.test(key)) throw new Error('invalid variable name in uri-template.');
46
+ if (!/^(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+)*$/.test(key)) throw new SyntaxError(`at index ${operator ? start + 2 : start + 1}: invalid variable name in uri-template.`);
39
47
  return Object.assign({ key }, limit ? { limit } : explode ? { explode } : {});
40
48
  });
41
49
  if (inspect) ast.push({ [operator]: varspecs });
@@ -52,17 +60,19 @@ function parseTemplate(template, validate) {
52
60
  if (first + expression) {
53
61
  const values = [];
54
62
  const operator = operators.has(first) ? first : ((expression = first + expression), '');
63
+ let defined = expression.split(/,/g).length;
55
64
  expression.split(/,/g).forEach((variable) => {
56
65
  const match = /(?<key>[^:\*]*)(?::(?<length>\d+)|(?<explode>\*))?/.exec(variable).groups;
57
66
  const key = match.key;
58
67
  const value = vars[key];
68
+ if (!isDefined(value)) defined--;
59
69
  if (isDefined(value) && value !== '') {
60
70
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
61
71
  let string = value.toString();
62
72
  if (match.length) string = string.substring(0, parseInt(match.length));
63
73
  values.push(encodeValue(operator, string, keyOperators.has(operator) ? key : ''));
64
74
  } else {
65
- if (validate && match.length) throw new Error('invalid limit modifier on objects.');
75
+ if (validate && match.length) throw new SyntaxError('invalid limit modifier on objects.');
66
76
  if (match.explode) {
67
77
  if (Array.isArray(value)) {
68
78
  value.filter(isDefined).forEach((item) => values.push(encodeValue(operator, item, keyOperators.has(operator) ? key : '')));
@@ -97,6 +107,7 @@ function parseTemplate(template, validate) {
97
107
  }
98
108
  }
99
109
  });
110
+ if (!defined && !validate) values.push(`{${expression}}`);
100
111
  return (operator === '+' || values.length === 0 ? '' : operator) + values.join(separator(operator));
101
112
  }
102
113
  return encodeReserved(literal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "url-templates",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A url-template validator, expander and inspector as defined by RFC 6570",
5
5
  "keywords": [
6
6
  "url",
package/readme.md CHANGED
@@ -40,7 +40,7 @@ It returns `true` or throws an `error`.
40
40
  ```js title="js"
41
41
  const { inspect } = require('url-templates');
42
42
  try {
43
- console.dir(inspect('/search{?q*,lang:2}'), { depth: null });
43
+ console.dir(inspect('/search{?q*,lang:2}'), { depth: null });
44
44
  // [ '/search', { '?': [ { key: 'q', explode: true }, { key: 'lang', limit: 2 } ] } ]
45
45
  } catch (error) {
46
46
  console.error('invalid:', error.message);
@@ -69,9 +69,36 @@ If valid returns the `expand(vars)` function which returns the expanded `url-tem
69
69
  ```js title="js"
70
70
  const { compile } = require('url-templates');
71
71
  console.log(compile('/broken{').expand({})); // returns '/broken{'; invalid parts left for postprocessing
72
- console.log(compile('/good{id}').expand({id:42})); // returns '/good42';
72
+ console.log(compile('/good{id}').expand({ id: 42 })); // returns '/good42';
73
+ console.log(compile('/undefined{id}').expand({ id: undefined })); // returns '/undefined{id}';
73
74
  ```
74
75
 
75
76
  **Note:**
76
- Returns a usable expander without validation (for cases where validation is done elsewhere, or for the cases where some sort of postprocessing will follow).
77
+ Returns a usable expander without validation for cases where validation is done elsewhere, or for the cases where some sort of postprocessing will follow. A good example of postprocessing is described next:
77
78
 
79
+ ### Multi pass expansion without validation
80
+
81
+ **Example 1**
82
+
83
+ ```js title="js"
84
+ const { compile } = require('url-templates');
85
+ const vars1 = { anotherPattern: '{foo}', andAnotherPattern: '{bar,baz}' };
86
+ const vars2 = { foo: 1, bar: 2, baz: 3 };
87
+ const firstPass = decodeURIComponent(compile('[{anotherPattern},{andAnotherPattern}]').expand(vars1));
88
+ console.log(firstPass); // returns '[{foo},{bar,baz}]';
89
+ console.log(compile(firstPass).expand(vars2)); // returns '[1,2,3]';
90
+ ```
91
+
92
+ **Example 2**
93
+
94
+ ```js title="js"
95
+ const { compile } = require('url-templates');
96
+ const vars1 = { foo: 1 };
97
+ const vars2 = { bar: 2, baz: 3 };
98
+ const firstPass = decodeURIComponent(compile('[{foo},{bar,baz}]').expand(vars1));
99
+ console.log(firstPass); // returns '[1,{bar,baz}]';
100
+ console.log(compile(firstPass).expand(vars2)); // returns '[1,2,3]';
101
+ ```
102
+
103
+ **Important Note:**
104
+ The first pass will preserve the `{bar,baz}` expression only if the supplied variable has **none** of its members. This method can also be used to preserve quantifiers like `{1,4}` in regular expresions.
File without changes