Skip to content

Commit

Permalink
Implement Reserved Expansion
Browse files Browse the repository at this point in the history
  • Loading branch information
kjellmorten committed Sep 24, 2017
1 parent 5cdedc0 commit 938d429
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 54 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand Down
48 changes: 5 additions & 43 deletions lib/compile-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,57 +98,19 @@ 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)

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 = [
Expand Down
13 changes: 13 additions & 0 deletions lib/generate-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]}
Expand Down
27 changes: 20 additions & 7 deletions lib/generate.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
const expandParam = (params, useKeys = false, delimiter = ',') => ({
const expandParam = (
params,
useKeys = false,
delimiter = ',',
encodeReserved = true
) => ({
param,
key,
filters,
explode = false,
optional = false
}) => {
let value = params[param]
const encode = (encodeReserved) ? encodeURIComponent : encodeURI

// Required parameters
if (value === undefined || value === null) {
Expand All @@ -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 '?':
Expand All @@ -54,29 +63,33 @@ 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) {
params = params || {}

const uri = compiled.map((segment) => {
if (typeof segment === 'string') {
return segment
return encodeURI(segment)
}
if (Array.isArray(segment)) {
return expandList(params, segment)
}
return expandParam(params)(segment)
}).join('')

return encodeURI(uri)
return uri
}

module.exports = generate
9 changes: 9 additions & 0 deletions lib/utils/splitComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/splitComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const split = (component) => {
}

function splitComponent (component) {
const match = /^\{([?&/#.;]?).+\}$/.exec(component)
const match = /^\{([?&/#.;+]?).+\}$/.exec(component)
if (!match) {
return []
}
Expand Down
2 changes: 1 addition & 1 deletion tests/filterFunctions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions tests/template-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

0 comments on commit 938d429

Please sign in to comment.