diff --git a/package.json b/package.json index a2012e4..ba4ca9f 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "build": "tsc && node build.mjs", "prepack": "yarn build", "test": "mocha --require esbuild-register --extension js,ts,cjs,mjs tests", - "coverage": "c8 --reporter=lcov --reporter=text-summary yarn test" + "coverage": "c8 --reporter=lcov --reporter=text-summary --exclude=.yarn yarn test" }, "prettier": { "semi": false, diff --git a/src/arrayHandling.ts b/src/arrayHandling.ts index 7a74961..bdcd065 100644 --- a/src/arrayHandling.ts +++ b/src/arrayHandling.ts @@ -19,14 +19,12 @@ import type { RealTestFunc, TestOptions } from "./index" const fillHashtags = (count: number): string => "#".repeat(count) /** - * Function that creates an array-like test node parser. - * It's split into a separate file for the sake of organization, and uses the proxy function to avoid circular dependencies. - * - * @param realTest The realTest function (internal). + * Handles `$any`, `$all`, and `$inarray`. Works with nested loops! + * @param realTest The realTest function. * @param input The state machine. * @param variables The variables. * @param op The operation being performed. - * @param options The test option. + * @param options The test options. * @internal */ export function handleArrayLogic( @@ -34,13 +32,22 @@ export function handleArrayLogic( input: any, variables: Variables, op: string, - options: TestOptions, + options: TestOptions ): boolean { + const inValue = input[op]["in"] + const depth = (options._currentLoopDepth || 0) + 1 + + if (inValue.includes("#")) { + throw new TypeError("Nested array nodes cannot use current iteration (`$.#`) as an `in` value", { + cause: options._path + }) + } + // find the array - const array = realTest(input[op]["in"], variables, { + const array = realTest(inValue, variables, { ...options, - _currentLoopDepth: (options._currentLoopDepth || 0) + 1, - _path: `${options._path}.${op}.in`, + _currentLoopDepth: depth, + _path: `${options._path}.${op}.in` }) as unknown as unknown[] const itemConditions = input[op]["?"] @@ -48,12 +55,11 @@ export function handleArrayLogic( for (const item of array) { const test = realTest(itemConditions, variables, { ...options, - _currentLoopDepth: (options._currentLoopDepth || 0) + 1, + _currentLoopDepth: depth, + _path: `${options._path}.${op}.?`, findNamedChild(reference, variables) { // NOTE: if we have a multi-layered loop, this should one-by-one fall back until the targeted loop is hit - const hashtags = fillHashtags( - (options._currentLoopDepth || 0) + 1, - ) + const hashtags = fillHashtags(depth) // a little future-proofing, as sometimes the $ is there, and other times it isn't. // we strip it out somewhere, but it shouldn't matter too much. @@ -64,8 +70,17 @@ export function handleArrayLogic( return item } + // handle properties of an object + if (typeof item === "object") { + const newReference = `$${reference.substring( + reference.indexOf("#.") + 1 + )}` + const found = options.findNamedChild(newReference, item) + if (found !== newReference) return found + } + return options.findNamedChild(reference, variables) - }, + } }) if (test && (op === "$inarray" || op === "$any")) { diff --git a/tests/inarray.data.json b/tests/inarray.data.json index b7d04b3..d374e3c 100644 --- a/tests/inarray.data.json +++ b/tests/inarray.data.json @@ -70,5 +70,86 @@ ] } } + ], + "Any3": [ + { + "$any": { + "in": "$Value.listvar", + "?": { + "$eq": [ + "$.#.prop", + 6 + ] + } + } + }, + { + "Value": { + "listvar": [ + { + "prop": 4 + }, + { + "prop": 6 + } + ] + } + } + ], + "Any4": [ + { + "$any": { + "in": "$Value.listvar", + "?": { + "$eq": [ + "$.#.prop", + 5 + ] + } + } + }, + { + "Value": { + "listvar": [ + { + "prop": 4 + }, + { + "prop": 6 + } + ] + } + } + ], + "Invalid_Crash_Nested": [ + { + "$any": { + "in": "$.MyList", + "?": { + "$any": { + "in": "$.#", + "?": { + "$eq": ["$.##.prop", 4] + } + } + } + } + }, + { + "Value": { + "MyList": [ + [ + { + "prop": 4 + } + ], + [ + { + "prop": 6 + } + ] + ] + } + } ] } diff --git a/tests/inarray.spec.ts b/tests/inarray.spec.ts index a8a7fde..6fbc1af 100644 --- a/tests/inarray.spec.ts +++ b/tests/inarray.spec.ts @@ -32,12 +32,31 @@ describe("$inarray", () => { describe("$any", () => { it("can find a string in a context array", () => { - const [sm, vars] = data.Inarray1 + const [sm, vars] = data.Any1 assert.strictEqual(test(sm, vars), true) }) it("returns false if the item isn't present", () => { - const [sm, vars] = data.Inarray2 + const [sm, vars] = data.Any2 + assert.strictEqual(test(sm, vars), false) + }) + + it("can find a property in a context array", () => { + const [sm, vars] = data.Any3 + assert.strictEqual(test(sm, vars), true) + }) + + it("returns false if a property isn't present", () => { + const [sm, vars] = data.Any4 assert.strictEqual(test(sm, vars), false) }) + + describe("nested", () => { + it("throws when trying to use current iteration as an `in` value", () => { + const [sm, vars] = data.Invalid_Crash_Nested + assert.throws(() => { + test(sm, vars) + }, /Nested array nodes cannot use current iteration \(`\$.#`\) as an `in` value/) + }) + }) })