From 938d429b5d271902ae555af15f490a15107172a6 Mon Sep 17 00:00:00 2001 From: Kjell-Morten Date: Sun, 24 Sep 2017 15:28:46 +0200 Subject: [PATCH] Implement Reserved Expansion --- README.md | 13 +++++++-- lib/compile-test.js | 48 ++++---------------------------- lib/generate-test.js | 13 +++++++++ lib/generate.js | 27 +++++++++++++----- lib/utils/splitComponent-test.js | 9 ++++++ lib/utils/splitComponent.js | 2 +- tests/filterFunctions-test.js | 2 +- tests/template-test.js | 22 +++++++++++++++ 8 files changed, 82 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 69216a9..5509ddd 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ escape character in JavaScript.) Including several parameters in the query string is such a common case, that it has its own modifier. Prefixing a parameter with a question mark creates a -query component, where several parameters may be included, seperated by a comma. +query component, where several parameters may be included, separated by a comma. A query component will be expanded to a key-value list, prefixed by a question mark, and delimited by ampersands. @@ -100,8 +100,17 @@ parameter as an array of three values): - Label Expansion: `{.var}` -> `.value1.value2.value3` - Path Segments Expansion: `{/var}` -> `/value1/value2/value3` - Path-Style Paramter Expansion: `{;var}` -> `;var=value1,value2,value3` +- Reserved Expansion: `{+var}` -> `value1,value2,value3` -Note: For Label and Path Segment Expansion, the 'explode' flag is on by default. +For Label and Path Segment Expansion, the 'explode' flag is on by default. + +When expanding parameters, all uri reserved characters are encoded, except for +Fragment and Reserved expansion. With a parameter `path` set to +`sections/news`, the template `http://example.com/{path}` will result +in the uri `http://example.com/sections%2Fnews`. So to expand `path` as +an actual path, use the template `http://example.com/{+path}`, which will +expand to `http://example.com/sections/news`. See [Variable Expansion](https://tools.ietf.org/html/rfc6570#section-3.2.1) in RFC 6570 for +more on encoding. ### Filter functions diff --git a/lib/compile-test.js b/lib/compile-test.js index 96f1f26..cc73e96 100644 --- a/lib/compile-test.js +++ b/lib/compile-test.js @@ -98,41 +98,12 @@ test('should compile optional query component', (t) => { t.deepEqual(ret, expected) }) -test('should compile query continuation', (t) => { - const template = 'http://example.com/?id=ent1{&first,max}' - const expected = ['http://example.com/', '?id=ent1', ['&', {param: 'first'}, {param: 'max'}]] - - const ret = compile(template) - - t.deepEqual(ret, expected) -}) - -test('should compile path segment', (t) => { - const template = 'http://example.com{/user,folders}' - const expected = [ - 'http://example.com', - ['/', {param: 'user', explode: true}, {param: 'folders', explode: true}] - ] - - const ret = compile(template) - - t.deepEqual(ret, expected) -}) - -test('should compile fragment expansion', (t) => { - const template = 'http://example.com/{#anchor}' - const expected = ['http://example.com/', ['#', {param: 'anchor'}]] - - const ret = compile(template) - - t.deepEqual(ret, expected) -}) - -test('should compile dot prefix', (t) => { - const template = 'http://example.com/index{.postfix}' +test('should explode path and dot expansion', (t) => { + const template = 'http://example.com/{/folders}{.suffix}' const expected = [ - 'http://example.com/index', - ['.', {param: 'postfix', explode: true}] + 'http://example.com/', + ['/', {param: 'folders', explode: true}], + ['.', {param: 'suffix', explode: true}] ] const ret = compile(template) @@ -140,15 +111,6 @@ test('should compile dot prefix', (t) => { t.deepEqual(ret, expected) }) -test('should compile path-style parameter expansion', (t) => { - const template = '{;list}' - const expected = [[';', {param: 'list'}]] - - const ret = compile(template) - - t.deepEqual(ret, expected) -}) - test('should compile filter function', (t) => { const template = 'http://example.com/{folder|append(_archive)}' const expected = [ diff --git a/lib/generate-test.js b/lib/generate-test.js index 993c112..c3b8410 100644 --- a/lib/generate-test.js +++ b/lib/generate-test.js @@ -160,6 +160,19 @@ test('should generate path-style parameter expansion', (t) => { t.is(ret, expected) }) +test('should generate with reserved expension', (t) => { + const compiled = [ + 'http://example.com/', + ['+', {param: 'folders'}] + ] + const params = {folders: 'path/to/something/'} + const expected = 'http://example.com/path/to/something/' + + const ret = generate(compiled, params) + + t.is(ret, expected) +}) + test('should generate path-style parameter expansion with array', (t) => { const compiled = [[';', {param: 'first'}]] const params = {first: [1, 2]} diff --git a/lib/generate.js b/lib/generate.js index 0c5b85e..10b0d86 100644 --- a/lib/generate.js +++ b/lib/generate.js @@ -1,4 +1,9 @@ -const expandParam = (params, useKeys = false, delimiter = ',') => ({ +const expandParam = ( + params, + useKeys = false, + delimiter = ',', + encodeReserved = true +) => ({ param, key, filters, @@ -6,6 +11,7 @@ const expandParam = (params, useKeys = false, delimiter = ',') => ({ optional = false }) => { let value = params[param] + const encode = (encodeReserved) ? encodeURIComponent : encodeURI // Required parameters if (value === undefined || value === null) { @@ -29,17 +35,20 @@ const expandParam = (params, useKeys = false, delimiter = ',') => ({ // Array value if (Array.isArray(value)) { - value = value.join((explode) ? delimiter : ',') + value = value.map(encode).join((explode) ? delimiter : ',') + } else { + value = encode(value) } // Return value - return (useKeys) ? `${key || param}=${value}` : value + return (useKeys) ? `${encode(key || param)}=${value}` : value } const expandList = (params, list) => { const modifier = list[0] || '' let useKeys = false let delimiter = ',' + let encodeReserved = true switch (modifier) { case '?': @@ -54,13 +63,17 @@ const expandList = (params, list) => { case ';': delimiter = modifier useKeys = true + break + case '#': + case '+': + encodeReserved = false } const parts = list.slice(1) - .map(expandParam(params, useKeys, delimiter)) + .map(expandParam(params, useKeys, delimiter, encodeReserved)) .filter((part) => part !== null) - return modifier + parts.join(delimiter) + return ((modifier === '+') ? '' : modifier) + parts.join(delimiter) } function generate (compiled, params) { @@ -68,7 +81,7 @@ function generate (compiled, params) { const uri = compiled.map((segment) => { if (typeof segment === 'string') { - return segment + return encodeURI(segment) } if (Array.isArray(segment)) { return expandList(params, segment) @@ -76,7 +89,7 @@ function generate (compiled, params) { return expandParam(params)(segment) }).join('') - return encodeURI(uri) + return uri } module.exports = generate diff --git a/lib/utils/splitComponent-test.js b/lib/utils/splitComponent-test.js index b0e814d..86bd11e 100644 --- a/lib/utils/splitComponent-test.js +++ b/lib/utils/splitComponent-test.js @@ -96,6 +96,15 @@ test('should split with path-style modifier', (t) => { t.deepEqual(ret, expected) }) +test('should split with reserved expansion modifier', (t) => { + const component = '{+folders}' + const expected = ['+', 'folders'] + + const ret = splitComponent(component) + + t.deepEqual(ret, expected) +}) + test('should split with filter function', (t) => { const component = '{/section|max(3),user}' const expected = ['/', 'section|max(3)', 'user'] diff --git a/lib/utils/splitComponent.js b/lib/utils/splitComponent.js index 2d728d4..2584c7d 100644 --- a/lib/utils/splitComponent.js +++ b/lib/utils/splitComponent.js @@ -27,7 +27,7 @@ const split = (component) => { } function splitComponent (component) { - const match = /^\{([?&/#.;]?).+\}$/.exec(component) + const match = /^\{([?&/#.;+]?).+\}$/.exec(component) if (!match) { return [] } diff --git a/tests/filterFunctions-test.js b/tests/filterFunctions-test.js index 76c4306..61c97c7 100644 --- a/tests/filterFunctions-test.js +++ b/tests/filterFunctions-test.js @@ -16,7 +16,7 @@ test('should generate uri with filter functions', (t) => { test('should generate uri with escaped characters in filter args', (t) => { const template = 'http://example.com/{?keys=ids|wrap([, ", ", \\,\\{\\}])}' const params = {ids: ['ent1', 'ent5']} - const expected = 'http://example.com/?keys=%5B%22ent1%22,%22ent5%22,%7B%7D%5D' + const expected = 'http://example.com/?keys=%5B%22ent1%22%2C%22ent5%22%2C%7B%7D%5D' const compiled = compile(template) const uri = generate(compiled, params) diff --git a/tests/template-test.js b/tests/template-test.js index b6b2c0e..03d0cf2 100644 --- a/tests/template-test.js +++ b/tests/template-test.js @@ -45,3 +45,25 @@ test('should keep escaped curly brackets', (t) => { t.is(uri, expected) }) + +test('should escape reserved characters in params', (t) => { + const template = 'http://example.com/{section}/{?url}' + const params = {section: 'health/wellness', url: 'http://test-it.com/'} + const expected = 'http://example.com/health%2Fwellness/?url=http%3A%2F%2Ftest-it.com%2F' + + const compiled = compile(template) + const uri = generate(compiled, params) + + t.is(uri, expected) +}) + +test('should not escape reserved characters in fragments', (t) => { + const template = 'http://example.com/{#section}' + const params = {section: 'health/wellness'} + const expected = 'http://example.com/#health/wellness' + + const compiled = compile(template) + const uri = generate(compiled, params) + + t.is(uri, expected) +})