diff --git a/defaultMethods.js b/defaultMethods.js index 8be6055..b168a0e 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -8,6 +8,7 @@ import { build, buildString } from './compiler.js' import chainingSupported from './utilities/chainingSupported.js' import InvalidControlInput from './errors/InvalidControlInput.js' import YieldingIterators from './yieldingIterators.js' +import { splitPath } from './utilities/splitPath.js' function isDeterministic (method, engine, buildState) { if (Array.isArray(method)) { @@ -161,7 +162,8 @@ const defaultMethods = { get: { method: ([data, key, defaultValue], context, above, engine) => { const notFound = defaultValue === undefined ? null : defaultValue - const subProps = String(key).split('.') + + const subProps = splitPath(String(key)) for (let i = 0; i < subProps.length; i++) { if (data === null || data === undefined) { return notFound @@ -203,7 +205,7 @@ const defaultMethods = { } return null } - const subProps = String(key).split('.') + const subProps = splitPath(String(key)) for (let i = 0; i < subProps.length; i++) { if (context === null || context === undefined) { return notFound @@ -810,7 +812,7 @@ defaultMethods.get.compile = function (data, buildState) { if (key && typeof key === 'object') return false key = key.toString() - const pieces = key.split('.') + const pieces = splitPath(key) if (!chainingSupported) { return `(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce( (text, i) => { @@ -854,7 +856,7 @@ defaultMethods.var.compile = function (data, buildState) { buildState.useContext = true return false } - const pieces = key.split('.') + const pieces = splitPath(key) const [top] = pieces buildState.varTop.add(top) // support older versions of node diff --git a/general.test.js b/general.test.js index c5711de..19efb34 100644 --- a/general.test.js +++ b/general.test.js @@ -117,4 +117,21 @@ describe('Various Test Cases', () => { it('get operator w/ object key as var', async () => { for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { get: [{ var: 'selected' }, { var: 'key' }] }, { selected: { b: 2 }, key: 'b' }, 2) }) + + 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) + }) + + 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) + }) + + 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) + }) + + 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) + }) }) diff --git a/index.js b/index.js index a907693..22b77f9 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,9 @@ import EngineObject from './structures/EngineObject.js' import Constants from './constants.js' import defaultMethods from './defaultMethods.js' import { asLogicSync, asLogicAsync } from './asLogic.js' +import { splitPath } from './utilities/splitPath.js' +export { splitPath } export { LogicEngine } export { AsyncLogicEngine } export { Compiler } @@ -20,4 +22,4 @@ export { defaultMethods } export { asLogicSync } export { asLogicAsync } -export default { LogicEngine, AsyncLogicEngine, Compiler, YieldStructure, EngineObject, Constants, defaultMethods, asLogicSync, asLogicAsync } +export default { LogicEngine, AsyncLogicEngine, Compiler, YieldStructure, EngineObject, Constants, defaultMethods, asLogicSync, asLogicAsync, splitPath } diff --git a/package.json b/package.json index 7743979..f34a66f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-logic-engine", - "version": "1.2.11", + "version": "1.3.0", "description": "Construct complex rules with JSON & process them.", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/utilities/splitPath.js b/utilities/splitPath.js new file mode 100644 index 0000000..0dc4374 --- /dev/null +++ b/utilities/splitPath.js @@ -0,0 +1,39 @@ + +/** + * 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 = '\\') { + const parts = [] + let current = '' + + for (let i = 0; i < str.length; i++) { + const char = str[i] + if (char === escape) { + if (str[i + 1] === separator) { + current += separator + i++ + } else if (str[i + 1] === escape) { + current += escape + i++ + } else current += escape + } else if (char === separator) { + parts.push(current) + current = '' + } else current += char + } + parts.push(current) + + return parts +}