diff --git a/defaultMethods.js b/defaultMethods.js index 0318587..faaf130 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -203,6 +203,35 @@ const defaultMethods = { } } }, + // Adding this to spec something out, not to merge it. + val: { + method: (args, context, above) => { + let result = context + let start = 0 + if (Array.isArray(args[0]) && args[0].length === 1) { + start++ + const climb = +Math.abs(args[0][0]) + let pos = 0 + for (let i = 0; i < climb; i++) { + result = above[pos++] + if (i === above.length - 1 && Array.isArray(result)) { + above = result + result = result[0] + pos = 1 + } + } + } + + for (let i = start; i < args.length; i++) { + if (args[i] === null) continue + if (result === null || result === undefined) return null + result = result[args[i]] + } + if (typeof result === 'undefined') return null + return result + }, + deterministic: false + }, var: (key, context, above, engine) => { let b if (Array.isArray(key)) { @@ -539,7 +568,7 @@ function createArrayIterativeMethod (name, useTruthy = false) { } const method = build(mapper, mapState) - const aboveArray = method.aboveDetected ? buildState.compile`[{ item: null }, context, above]` : buildState.compile`null` + const aboveArray = method.aboveDetected ? buildState.compile`[{ item: null, index: x }, context, above]` : buildState.compile`null` if (async) { if (!isSyncDeep(mapper, buildState.engine, buildState)) { diff --git a/general.test.js b/general.test.js index 7ecbb93..e8fa44f 100644 --- a/general.test.js +++ b/general.test.js @@ -123,11 +123,11 @@ describe('Various Test Cases', () => { }) it('is able to handle simple path escaping', async () => { - for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { get: [{ var: 'selected' }, 'b\\.c'] }, { selected: { 'b.c': 2 } }, 2) + for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { get: [{ var: 'selected' }, 'b~2c'] }, { selected: { 'b.c': 2 } }, 2) }) it('is able to handle simple path escaping in a variable', async () => { - for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { get: [{ var: 'selected' }, { var: 'key' }] }, { selected: { 'b.c': 2 }, key: 'b\\.c' }, 2) + for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { get: [{ var: 'selected' }, { var: 'key' }] }, { selected: { 'b.c': 2 }, key: 'b~2c' }, 2) }) it('is able to avoid returning functions', async () => { @@ -154,7 +154,7 @@ describe('Various Test Cases', () => { }) it('is able to handle path escaping in a var call', async () => { - for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: 'hello\\.world' }, { 'hello.world': 2 }, 2) + for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: 'hello~2world' }, { 'hello.world': 2 }, 2) }) it('is able to access empty keys', async () => { @@ -162,16 +162,15 @@ describe('Various Test Cases', () => { }) it('is able to access dot keys', async () => { - for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: '\\.' }, { '.': 2 }, 2) + for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: '~2' }, { '.': 2 }, 2) }) it('is able to access "/" keys from above', async () => { - for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { map: [[1], { '+': [{ var: '' }, { var: '../../..\\/' }] }] }, { '': { '': { '/': 3 } } }, [4]) + for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { map: [[1], { '+': [{ var: '' }, { var: '../..////~1' }] }] }, { '': { '': { '/': 3 } } }, [4]) }) it('is able to handle path escaping with multiple escapes', async () => { for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: '\\foo' }, { '\\foo': 2 }, 2) - for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: '\\\\foo' }, { '\\foo': 2 }, 2) }) it('is able to access the index in the iterators', async () => { diff --git a/suites/scopes.json b/suites/scopes.json index 1a56d89..5fb0cb8 100644 --- a/suites/scopes.json +++ b/suites/scopes.json @@ -58,7 +58,7 @@ "rule": { "map": [ { "var": "arr" }, - { "+": [{ "var": "../../\\.\\./" }, { "var": "../../..\\/" }]} + { "+": [{ "var": "../../~2~2~1" }, { "var": "../..////~1" }]} ] }, "data": { "arr": [1,2,3], "../": 10, "": { "": { "/": 7 }} }, diff --git a/suites/val.json b/suites/val.json new file mode 100644 index 0000000..73de36e --- /dev/null +++ b/suites/val.json @@ -0,0 +1,72 @@ +[ + "Test Specification for val", + { + "description": "Fetches a value from an empty key", + "rule": { "val": "" }, + "data": { "" : 1 }, + "result": 1 + }, + { + "description": "Fetches a value from a nested empty key", + "rule": { "val": ["", ""] }, + "data": { "" : { "": 2 } }, + "result": 2 + }, + { + "description": "Fetches a value from a doubly nested empty key", + "rule": { "val": ["", "", ""] }, + "data": { "" : { "": { "": 3 } } }, + "result": 3 + }, + { + "description": "Fetches a value from a key that is purely a dot", + "rule": { "val": "." }, + "data": { "." : 20 }, + "result": 20 + }, + { + "description": "Fetches the entire context", + "rule": { "val": null }, + "data": { "": 21 }, + "result": { "": 21 } + }, + { + "description": "Fetches the entire context for a nested key", + "rule": { "val": "" }, + "data": { "": { "": 22 } }, + "result": { "": 22 } + }, + { + "description": "Using val in a map (remember that null gets the current context, not empty string)", + "rule": { "map": [[1,2,3], { "+": [{ "val": null }, 1] }] }, + "data": null, + "result": [2,3,4] + }, + { + "description": "Using val in a map (and remember [null] and null are the same)", + "rule": { "map": [[1,2,3], { "+": [{ "val": [null] }, 1] }] }, + "data": null, + "result": [2,3,4] + }, + "Testing out scopes", + { + "description": "Climb up to get adder", + "rule": { "map": [[1,2,3], { "+": [{ "val": null }, { "val": [[-2], "adder"] }] }] }, + "data": { "adder": 10 }, + "result": [11,12,13] + }, + { + "description": "Climb up to get index", + "rule": { "map": [[1,2,3], { "+": [{ "val": null }, { "val": [[-1], "index"] }] }] }, + "data": { "adder": 10 }, + "result": [1,3,5] + }, + { + "description": "Nested get adder", + "rule": { + "map": [["Test"], { "map": [[1,2,3], { "+": [{"val": null}, {"val": [[-4], "adder"]}] }]} ] + }, + "data": { "adder": 10 }, + "result": [[11,12,13]] + } +] \ No newline at end of file diff --git a/suites/vars.json b/suites/vars.json index 06ba8be..41b1983 100644 --- a/suites/vars.json +++ b/suites/vars.json @@ -2,80 +2,110 @@ "Test Specification for Handling esoteric path traversal", { "description": "Fetches a value from an empty key", - "rule": { "var": "." }, + "rule": { "var": "/" }, "data": { "" : 1 }, "result": 1 }, { "description": "Fetches a value from a nested empty key", - "rule": { "var": ".." }, + "rule": { "var": "//" }, "data": { "" : { "": 2 } }, "result": 2 }, { "description": "Fetches a value from a doubly nested empty key", - "rule": { "var": "..." }, + "rule": { "var": "///" }, "data": { "" : { "": { "": 3 } } }, "result": 3 }, { "description": "Fetches a value from a key that is purely a dot", - "rule": { "var": "\\." }, + "rule": { "var": "~2" }, "data": { "." : 20 }, "result": 20 }, { "description": "Fetches a value from a key with a dot in it", - "rule": { "var": "\\.key" }, + "rule": { "var": "~2key" }, "data": { ".key" : 4 }, "result": 4 }, { "description":"Fetches a value from a key with a dot in it (2)", - "rule": { "var": "hello\\.world" }, + "rule": { "var": "hello~2world" }, "data": { "hello.world" : 5 }, "result": 5 }, { "description": "Fetches a value from a key inside an empty key with a dot in it", - "rule": { "var": ".\\.key" }, + "rule": { "var": "//~2key" }, "data": { "": { ".key" : 6 } }, "result": 6 }, { "description": "Going a few levels deep", - "rule": { "var": "..\\.key." }, + "rule": { "var": "///~2key/" }, "data": { "": { "": { ".key": { "": 7 }} }}, "result": 7 }, { "description": "Escape / as well, which is useful for the scope proposal", - "rule": { "var": "\\/" }, + "rule": { "var": "~1" }, "data": { "/" : 8 }, "result": 8 }, - { - "description": "Though / doesn't inherently need to be escaped", - "rule": { "var": "/" }, - "data": { "/" : 9 }, - "result": 9 - }, { "description": "Dot then empty key", - "rule": { "var": "\\.." }, + "rule": { "var": "~2." }, "data": { "." : { "" : 10 } }, "result": 10 }, { "description": "Empty key then dot", - "rule": { "var": ".\\." }, + "rule": { "var": "//~2" }, "data": { "" : { "." : 11 } }, "result": 11 }, { "description": "Can use backslack in name, too", - "rule": { "var": "\\\\.Hello" }, + "rule": { "var": "\\.Hello" }, "data": { "\\" : { "Hello" : 12 } }, "result": 12 + }, + { + "description": "Can escape tilde", + "rule": { "var": "~0" }, + "data": { "~" : 13 }, + "result": 13 + }, + { + "description": "Fetches a value from an empty key, equivalence", + "rule": { "var": "." }, + "data": { "" : 1 }, + "result": 1 + }, + { + "description": "Fetches a value from a nested empty key, equivalence", + "rule": { "var": ".." }, + "data": { "" : { "": 2 } }, + "result": 2 + }, + { + "description": "Fetches a value from a doubly nested empty key, equivalence", + "rule": { "var": "..." }, + "data": { "" : { "": { "": 3 } } }, + "result": 3 + }, + { + "description": "Old format still works", + "rule": { "var": "hello.world" }, + "data": { "hello": { "world": 5 } }, + "result": 5 + }, + { + "description": "New format works too", + "rule": { "var": "hello/world" }, + "data": { "hello": { "world": 5 } }, + "result": 5 } ] \ No newline at end of file diff --git a/utilities/splitPath.js b/utilities/splitPath.js index c171c7b..1c21eed 100644 --- a/utilities/splitPath.js +++ b/utilities/splitPath.js @@ -20,38 +20,33 @@ export function splitPathMemoized (str) { return parts } +const chars = ['~', '/', '.'] + /** * Splits a path string into an array of parts. * - * @example splitPath('a.b.c') // ['a', 'b', 'c'] - * @example splitPath('a\\.b.c') // ['a.b', 'c'] - * @example splitPath('a\\\\.b.c') // ['a\\', 'b', 'c'] - * @example splitPath('a\\\\\\.b.c') // ['a\\.b', 'c'] - * @example splitPath('hello') // ['hello'] - * @example splitPath('hello\\') // ['hello\\'] - * @example splitPath('hello\\\\') // ['hello\\'] * * @param {string} str * @param {string} separator * @returns {string[]} */ -export function splitPath (str, separator = '.', escape = '\\', up = '/') { +export function splitPath (str) { const parts = [] let current = '' for (let i = 0; i < str.length; i++) { const char = str[i] - if (char === escape) { - if (str[i + 1] === separator || str[i + 1] === up) { - current += str[i + 1] + if (char === '~') { + if (str[i + 1] === '0' || str[i + 1] === '1' || str[i + 1] === '2') { + current += chars[+str[i + 1]] i++ - } else if (str[i + 1] === escape) { - current += escape + } else if (str[i + 1] === '~') { + current += '~' i++ // The following else might be something tweaked in a spec. - } else current += escape - } else if (char === separator) { - parts.push(current) + } else throw new Error('Invalid escape sequence') + } else if (char === '.' || char === '/') { + if (i) parts.push(current) current = '' } else current += char }