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.
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
+