url-templates 1.0.0
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.
- package/#/tests/0/.gitkeep +0 -0
- package/LICENSE +21 -0
- package/index.d.ts +36 -0
- package/index.js +112 -0
- package/package.json +29 -0
- package/readme.md +77 -0
|
File without changes
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SorinGFS
|
|
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.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a template.
|
|
3
|
+
* Returns true or throws a detailed error.
|
|
4
|
+
*
|
|
5
|
+
* @throws {TypeError | Error}
|
|
6
|
+
*/
|
|
7
|
+
declare function isUrlTemplate(template: string): boolean;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Inspect a template.
|
|
11
|
+
* Returns the parsed AST.
|
|
12
|
+
*/
|
|
13
|
+
declare function inspect(template: string): any[];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse and validate a template.
|
|
17
|
+
* Returns an object with an expand() method.
|
|
18
|
+
*
|
|
19
|
+
* @throws {TypeError | Error}
|
|
20
|
+
*/
|
|
21
|
+
declare function parseTemplate(template: string): { expand(vars?: any): string };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Compile a template without validation.
|
|
25
|
+
* Returns an object with an expand() method.
|
|
26
|
+
*/
|
|
27
|
+
declare function compile(template: string): { expand(vars?: any): string };
|
|
28
|
+
|
|
29
|
+
declare const urlTemplates: {
|
|
30
|
+
isUrlTemplate: typeof isUrlTemplate;
|
|
31
|
+
inspect: typeof inspect;
|
|
32
|
+
parseTemplate: typeof parseTemplate;
|
|
33
|
+
compile: typeof compile;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export = urlTemplates;
|
package/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// RFC 6570 fully compiant uri-template validator and loader
|
|
3
|
+
const keyOperators = new Set(['?', '&', ';']);
|
|
4
|
+
const operators = new Set(['+', '#', '.', '/', '?', '&', ';']);
|
|
5
|
+
const separator = (operator) => (operator === '' || operator === '+' || operator == '#' ? ',' : operator === '?' ? '&' : operator);
|
|
6
|
+
const isDefined = (value) => value !== undefined && value !== null;
|
|
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('');
|
|
9
|
+
const encodeValue = (operator, value, key) => {
|
|
10
|
+
value = operator === '+' || operator === '#' ? encodeReserved(value) : encodeUnreserved(value);
|
|
11
|
+
return key ? encodeReserved(key) + '=' + value : value;
|
|
12
|
+
};
|
|
13
|
+
const ast = [];
|
|
14
|
+
// url-template validator
|
|
15
|
+
function isUrlTemplate(template, inspect) {
|
|
16
|
+
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.');
|
|
18
|
+
for (let i = 0; i < template.length; ) {
|
|
19
|
+
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; }
|
|
22
|
+
if (inspect && start > i) ast.push(template.slice(i, start));
|
|
23
|
+
const end = template.indexOf('}', start + 1);
|
|
24
|
+
if (end === -1) throw new Error('unterminated expression in uri-template.');
|
|
25
|
+
let expression = template.slice(start + 1, end);
|
|
26
|
+
if (expression.length === 0) throw new Error('empty expression.');
|
|
27
|
+
const first = expression[0];
|
|
28
|
+
const operator = operators.has(first) ? ((expression = expression.slice(1)), first) : '';
|
|
29
|
+
if (expression.length === 0) throw new Error('expression missing variable names.');
|
|
30
|
+
const varspecs = expression.split(',').map((key) => {
|
|
31
|
+
const colon = key.indexOf(':');
|
|
32
|
+
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.');
|
|
34
|
+
if (limit) key = key.slice(0, colon);
|
|
35
|
+
const explode = key.endsWith('*');
|
|
36
|
+
if (explode && limit) throw new Error('invalid modifier having both explode and limit.');
|
|
37
|
+
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.');
|
|
39
|
+
return Object.assign({ key }, limit ? { limit } : explode ? { explode } : {});
|
|
40
|
+
});
|
|
41
|
+
if (inspect) ast.push({ [operator]: varspecs });
|
|
42
|
+
i = end + 1;
|
|
43
|
+
}
|
|
44
|
+
return inspect ? ast : true;
|
|
45
|
+
}
|
|
46
|
+
// url-template filler with optional validation
|
|
47
|
+
function parseTemplate(template, validate) {
|
|
48
|
+
if (validate) isUrlTemplate(template);
|
|
49
|
+
return {
|
|
50
|
+
expand: (vars = {}) =>
|
|
51
|
+
template.replace(/([^\{\}]+)|\{([^\{\}])([^\{\}]*)\}/g, (match, literal, first, expression) => {
|
|
52
|
+
if (first + expression) {
|
|
53
|
+
const values = [];
|
|
54
|
+
const operator = operators.has(first) ? first : ((expression = first + expression), '');
|
|
55
|
+
expression.split(/,/g).forEach((variable) => {
|
|
56
|
+
const match = /(?<key>[^:\*]*)(?::(?<length>\d+)|(?<explode>\*))?/.exec(variable).groups;
|
|
57
|
+
const key = match.key;
|
|
58
|
+
const value = vars[key];
|
|
59
|
+
if (isDefined(value) && value !== '') {
|
|
60
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
61
|
+
let string = value.toString();
|
|
62
|
+
if (match.length) string = string.substring(0, parseInt(match.length));
|
|
63
|
+
values.push(encodeValue(operator, string, keyOperators.has(operator) ? key : ''));
|
|
64
|
+
} else {
|
|
65
|
+
if (validate && match.length) throw new Error('invalid limit modifier on objects.');
|
|
66
|
+
if (match.explode) {
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
value.filter(isDefined).forEach((item) => values.push(encodeValue(operator, item, keyOperators.has(operator) ? key : '')));
|
|
69
|
+
} else {
|
|
70
|
+
Object.keys(value).forEach((key) => {
|
|
71
|
+
if (isDefined(value[key])) values.push(encodeValue(operator, value[key], key));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
const array = [];
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
value.filter(isDefined).forEach((item) => array.push(encodeValue(operator, item)));
|
|
78
|
+
} else {
|
|
79
|
+
Object.keys(value).forEach((key) => {
|
|
80
|
+
if (isDefined(value[key])) {
|
|
81
|
+
array.push(encodeUnreserved(key));
|
|
82
|
+
array.push(encodeValue(operator, value[key].toString()));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (keyOperators.has(operator) && Object.keys(value).length) values.push(encodeUnreserved(key) + '=' + array.join());
|
|
87
|
+
else if (array.length !== 0) values.push(array.join());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
if (operator === ';') {
|
|
92
|
+
if (isDefined(value)) values.push(encodeUnreserved(key));
|
|
93
|
+
} else if (value === '' && (operator === '&' || operator === '?')) {
|
|
94
|
+
values.push(encodeUnreserved(key) + '=');
|
|
95
|
+
} else if (value === '') {
|
|
96
|
+
values.push('');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return (operator === '+' || values.length === 0 ? '' : operator) + values.join(separator(operator));
|
|
101
|
+
}
|
|
102
|
+
return encodeReserved(literal);
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// export
|
|
107
|
+
module.exports = {
|
|
108
|
+
parseTemplate: (template) => parseTemplate(template, true),
|
|
109
|
+
isUrlTemplate: (template) => isUrlTemplate(template),
|
|
110
|
+
inspect: (template) => isUrlTemplate(template, true),
|
|
111
|
+
compile: (template) => parseTemplate(template),
|
|
112
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "url-templates",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A url-template validator, expander and inspector as defined by RFC 6570",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"url",
|
|
7
|
+
"uri",
|
|
8
|
+
"template",
|
|
9
|
+
"url-template",
|
|
10
|
+
"uri-template",
|
|
11
|
+
"rfc6570",
|
|
12
|
+
"validation"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/SorinGFS/url-templates#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/SorinGFS/url-templates/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/SorinGFS/url-templates.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "SorinGFS",
|
|
24
|
+
"type": "commonjs",
|
|
25
|
+
"main": "index.js",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
title: URL Templates
|
|
4
|
+
|
|
5
|
+
description: A URL Template validator, expander and inspector
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
A URL Template validator, expander and inspector — fully RFC 6570 compliant.
|
|
12
|
+
Passes all the 252 tests from the [uritemplate-test](https://github.com/uri-templates/uritemplate-test) suite. Minimal, zero dependency, sync API; exposes AST for linters/interfaces and both validated and non-validated expansion paths.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash title="console"
|
|
17
|
+
npm i url-templates
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
This library provides the following `url-template` functions:
|
|
23
|
+
|
|
24
|
+
### Validation
|
|
25
|
+
|
|
26
|
+
```js title="js"
|
|
27
|
+
const { isUrlTemplate } = require('url-templates');
|
|
28
|
+
try {
|
|
29
|
+
console.log('valid:', isUrlTemplate('/users/{id}')); // true
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('invalid:', error.message);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Note:**
|
|
36
|
+
It returns `true` or throws an `error`.
|
|
37
|
+
|
|
38
|
+
### Inspection
|
|
39
|
+
|
|
40
|
+
```js title="js"
|
|
41
|
+
const { inspect } = require('url-templates');
|
|
42
|
+
try {
|
|
43
|
+
console.dir(inspect('/search{?q*,lang:2}'), { depth: null });
|
|
44
|
+
// [ '/search', { '?': [ { key: 'q', explode: true }, { key: 'lang', limit: 2 } ] } ]
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('invalid:', error.message);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Note:**
|
|
51
|
+
Same as with `isUrlTemplate`, but if valid returns the parsed `AST` instead of `true`.
|
|
52
|
+
|
|
53
|
+
### Expansion with validation
|
|
54
|
+
|
|
55
|
+
```js title="js"
|
|
56
|
+
const { parseTemplate } = require('url-templates');
|
|
57
|
+
try {
|
|
58
|
+
console.log(parseTemplate('/items/{id}').expand({ id: 42 })); // '/items/42'
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('parse/validation error:', error.message);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Note:**
|
|
65
|
+
If valid returns the `expand(vars)` function which returns the expanded `url-template`. Otherwise, it throws an `error`. The `expand` function also throws `error` if `limit` is defined on `objects` (`isUrlTemplate` function cannot know that without runtime vars).
|
|
66
|
+
|
|
67
|
+
### Expansion without validation
|
|
68
|
+
|
|
69
|
+
```js title="js"
|
|
70
|
+
const { compile } = require('url-templates');
|
|
71
|
+
console.log(compile('/broken{').expand({})); // returns '/broken{'; invalid parts left for postprocessing
|
|
72
|
+
console.log(compile('/good{id}').expand({id:42})); // returns '/good42';
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**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
|
+
|