diff --git a/web/src/beta/lib/core/mantle/evaluator/simple/expression/utils.ts b/web/src/beta/lib/core/mantle/evaluator/simple/expression/utils.ts deleted file mode 100644 index 6a22862b45..0000000000 --- a/web/src/beta/lib/core/mantle/evaluator/simple/expression/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -const usableChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - -export const generateRandomString = (len: number): string => { - return Array.from(window.crypto.getRandomValues(new Uint8Array(len))) - .map(n => usableChars[n % len]) - .join("") - .toLowerCase(); -}; diff --git a/web/src/beta/lib/core/mantle/evaluator/simple/expression/variableReplacer.ts b/web/src/beta/lib/core/mantle/evaluator/simple/expression/variableReplacer.ts index 9135ead911..57618be6ab 100644 --- a/web/src/beta/lib/core/mantle/evaluator/simple/expression/variableReplacer.ts +++ b/web/src/beta/lib/core/mantle/evaluator/simple/expression/variableReplacer.ts @@ -1,16 +1,19 @@ import { JSONPath } from "jsonpath-plus"; +import { generateRandomString } from "../utils"; + import { JPLiteral } from "./expression"; -import { generateRandomString } from "./utils"; export const VARIABLE_PREFIX = "czm_"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function replaceVariables(expression: string, feature?: any): [string, JPLiteral[]] { let exp = expression; let result = ""; const literalJP: JPLiteral[] = []; let i = exp.indexOf("${"); const featureDefined = typeof feature !== "undefined"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const jsonPathCache: Record = {}; const varExpRegex = /^\$./; while (i >= 0) { diff --git a/web/src/beta/lib/core/mantle/evaluator/simple/index.ts b/web/src/beta/lib/core/mantle/evaluator/simple/index.ts index 657256fd1f..2faf3e151d 100644 --- a/web/src/beta/lib/core/mantle/evaluator/simple/index.ts +++ b/web/src/beta/lib/core/mantle/evaluator/simple/index.ts @@ -15,6 +15,7 @@ import { import { ConditionalExpression } from "./conditionalExpression"; import { clearExpressionCaches, Expression } from "./expression"; import { evalTimeInterval } from "./interval"; +import { recursiveJSONParse } from "./utils"; export async function evalSimpleLayer( layer: LayerSimple, @@ -65,6 +66,7 @@ export function evalLayerAppearances( ); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function recursiveValEval(obj: any, layer: LayerSimple, feature?: Feature): any { return Object.fromEntries( Object.entries(obj).map(([k, v]) => { @@ -88,6 +90,7 @@ export function clearAllExpressionCaches( }); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function recursiveClear(obj: any, layer: LayerSimple | undefined, feature: Feature | undefined) { Object.entries(obj).forEach(([, v]) => { // if v is an object itself and not a null, recurse deeper @@ -110,15 +113,18 @@ function recursiveClear(obj: any, layer: LayerSimple | undefined, feature: Featu }); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function hasExpression(e: any): e is ExpressionContainer { return typeof e === "object" && e && "expression" in e; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function hasNonExpressionObject(v: any): boolean { return typeof v === "object" && v && !("expression" in v); } function evalExpression( + // eslint-disable-next-line @typescript-eslint/no-explicit-any expressionContainer: any, layer: LayerSimple, feature?: Feature, @@ -126,14 +132,15 @@ function evalExpression( try { if (hasExpression(expressionContainer)) { const styleExpression = expressionContainer.expression; + const parsedFeature = recursiveJSONParse(feature); if (typeof styleExpression === "undefined") { return undefined; } else if (typeof styleExpression === "object" && styleExpression.conditions) { - return new ConditionalExpression(styleExpression, feature, layer.defines).evaluate(); + return new ConditionalExpression(styleExpression, parsedFeature, layer.defines).evaluate(); } else if (typeof styleExpression === "boolean" || typeof styleExpression === "number") { - return new Expression(String(styleExpression), feature, layer.defines).evaluate(); + return new Expression(String(styleExpression), parsedFeature, layer.defines).evaluate(); } else if (typeof styleExpression === "string") { - return new Expression(styleExpression, feature, layer.defines).evaluate(); + return new Expression(styleExpression, parsedFeature, layer.defines).evaluate(); } return styleExpression; } diff --git a/web/src/beta/lib/core/mantle/evaluator/simple/utils.test.ts b/web/src/beta/lib/core/mantle/evaluator/simple/utils.test.ts new file mode 100644 index 0000000000..4d1269c589 --- /dev/null +++ b/web/src/beta/lib/core/mantle/evaluator/simple/utils.test.ts @@ -0,0 +1,56 @@ +import { expect, test, describe } from "vitest"; + +import { recursiveJSONParse } from "./utils"; // Import the function from your module/file + +describe("recursiveJSONParse", () => { + test("should parse nested JSON strings within an object", () => { + const obj = { + name: "John", + age: "25", + data: '{"city": "New York", "country": "USA"}', + }; + const expected = { + name: "John", + age: "25", + data: { + city: "New York", + country: "USA", + }, + }; + + const result = recursiveJSONParse(obj); + expect(result).toEqual(expected); + }); + + test("should handle non-object values and null", () => { + const str = "Test"; + const num = 123; + const bool = true; + const arr = ["one", "two"]; + const emptyObj = {}; + const nullValue = null; + + expect(recursiveJSONParse(str)).toBe(str); + expect(recursiveJSONParse(num)).toBe(num); + expect(recursiveJSONParse(bool)).toBe(bool); + expect(recursiveJSONParse(arr)).toEqual(arr); + expect(recursiveJSONParse(emptyObj)).toEqual(emptyObj); + expect(recursiveJSONParse(nullValue)).toBe(nullValue); + }); + + test("should handle invalid JSON strings", () => { + const obj = { + name: "John", + age: "25", + data: '{city: "New York", country: "USA"}', // Invalid JSON string + }; + const expected = { + name: "John", + age: "25", + data: '{city: "New York", country: "USA"}', // Invalid JSON string remains unchanged + }; + + const result = recursiveJSONParse(obj); + expect(result).toEqual(expected); + }); +}); diff --git a/web/src/beta/lib/core/mantle/evaluator/simple/utils.ts b/web/src/beta/lib/core/mantle/evaluator/simple/utils.ts new file mode 100644 index 0000000000..7bcc51b7f2 --- /dev/null +++ b/web/src/beta/lib/core/mantle/evaluator/simple/utils.ts @@ -0,0 +1,31 @@ +const usableChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + +export const generateRandomString = (len: number): string => { + return Array.from(window.crypto.getRandomValues(new Uint8Array(len))) + .map(n => usableChars[n % len]) + .join("") + .toLowerCase(); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const recursiveJSONParse = (obj: any): any => { + if (typeof obj !== "object" || obj === null) { + return obj; + } + + for (const key in obj) { + if (typeof obj[key] === "string") { + try { + if (obj[key].startsWith("{") && obj[key].endsWith("}")) { + obj[key] = JSON.parse(obj[key]); + } + } catch (error) { + console.error("Invalid JSON:", obj[key]); + } + } else if (typeof obj[key] === "object") { + obj[key] = recursiveJSONParse(obj[key]); + } + } + + return obj; +}; diff --git a/web/src/classic/core/mantle/evaluator/simple/expression/utils.ts b/web/src/classic/core/mantle/evaluator/simple/expression/utils.ts deleted file mode 100644 index 6a22862b45..0000000000 --- a/web/src/classic/core/mantle/evaluator/simple/expression/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -const usableChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - -export const generateRandomString = (len: number): string => { - return Array.from(window.crypto.getRandomValues(new Uint8Array(len))) - .map(n => usableChars[n % len]) - .join("") - .toLowerCase(); -}; diff --git a/web/src/classic/core/mantle/evaluator/simple/expression/variableReplacer.ts b/web/src/classic/core/mantle/evaluator/simple/expression/variableReplacer.ts index 9135ead911..c0cb1bfa6b 100644 --- a/web/src/classic/core/mantle/evaluator/simple/expression/variableReplacer.ts +++ b/web/src/classic/core/mantle/evaluator/simple/expression/variableReplacer.ts @@ -1,7 +1,8 @@ import { JSONPath } from "jsonpath-plus"; +import { generateRandomString } from "../utils"; + import { JPLiteral } from "./expression"; -import { generateRandomString } from "./utils"; export const VARIABLE_PREFIX = "czm_"; diff --git a/web/src/classic/core/mantle/evaluator/simple/index.ts b/web/src/classic/core/mantle/evaluator/simple/index.ts index 657256fd1f..973d6510b7 100644 --- a/web/src/classic/core/mantle/evaluator/simple/index.ts +++ b/web/src/classic/core/mantle/evaluator/simple/index.ts @@ -15,6 +15,7 @@ import { import { ConditionalExpression } from "./conditionalExpression"; import { clearExpressionCaches, Expression } from "./expression"; import { evalTimeInterval } from "./interval"; +import { recursiveJSONParse } from "./utils"; export async function evalSimpleLayer( layer: LayerSimple, @@ -126,14 +127,15 @@ function evalExpression( try { if (hasExpression(expressionContainer)) { const styleExpression = expressionContainer.expression; + const parsedFeature = recursiveJSONParse(feature); if (typeof styleExpression === "undefined") { return undefined; } else if (typeof styleExpression === "object" && styleExpression.conditions) { - return new ConditionalExpression(styleExpression, feature, layer.defines).evaluate(); + return new ConditionalExpression(styleExpression, parsedFeature, layer.defines).evaluate(); } else if (typeof styleExpression === "boolean" || typeof styleExpression === "number") { - return new Expression(String(styleExpression), feature, layer.defines).evaluate(); + return new Expression(String(styleExpression), parsedFeature, layer.defines).evaluate(); } else if (typeof styleExpression === "string") { - return new Expression(styleExpression, feature, layer.defines).evaluate(); + return new Expression(styleExpression, parsedFeature, layer.defines).evaluate(); } return styleExpression; } diff --git a/web/src/classic/core/mantle/evaluator/simple/utils.test.ts b/web/src/classic/core/mantle/evaluator/simple/utils.test.ts new file mode 100644 index 0000000000..4d1269c589 --- /dev/null +++ b/web/src/classic/core/mantle/evaluator/simple/utils.test.ts @@ -0,0 +1,56 @@ +import { expect, test, describe } from "vitest"; + +import { recursiveJSONParse } from "./utils"; // Import the function from your module/file + +describe("recursiveJSONParse", () => { + test("should parse nested JSON strings within an object", () => { + const obj = { + name: "John", + age: "25", + data: '{"city": "New York", "country": "USA"}', + }; + const expected = { + name: "John", + age: "25", + data: { + city: "New York", + country: "USA", + }, + }; + + const result = recursiveJSONParse(obj); + expect(result).toEqual(expected); + }); + + test("should handle non-object values and null", () => { + const str = "Test"; + const num = 123; + const bool = true; + const arr = ["one", "two"]; + const emptyObj = {}; + const nullValue = null; + + expect(recursiveJSONParse(str)).toBe(str); + expect(recursiveJSONParse(num)).toBe(num); + expect(recursiveJSONParse(bool)).toBe(bool); + expect(recursiveJSONParse(arr)).toEqual(arr); + expect(recursiveJSONParse(emptyObj)).toEqual(emptyObj); + expect(recursiveJSONParse(nullValue)).toBe(nullValue); + }); + + test("should handle invalid JSON strings", () => { + const obj = { + name: "John", + age: "25", + data: '{city: "New York", country: "USA"}', // Invalid JSON string + }; + const expected = { + name: "John", + age: "25", + data: '{city: "New York", country: "USA"}', // Invalid JSON string remains unchanged + }; + + const result = recursiveJSONParse(obj); + expect(result).toEqual(expected); + }); +}); diff --git a/web/src/classic/core/mantle/evaluator/simple/utils.ts b/web/src/classic/core/mantle/evaluator/simple/utils.ts new file mode 100644 index 0000000000..2d765dbaf7 --- /dev/null +++ b/web/src/classic/core/mantle/evaluator/simple/utils.ts @@ -0,0 +1,30 @@ +const usableChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + +export const generateRandomString = (len: number): string => { + return Array.from(window.crypto.getRandomValues(new Uint8Array(len))) + .map(n => usableChars[n % len]) + .join("") + .toLowerCase(); +}; + +export const recursiveJSONParse = (obj: any): any => { + if (typeof obj !== "object" || obj === null) { + return obj; + } + + for (const key in obj) { + if (typeof obj[key] === "string") { + try { + if (obj[key].startsWith("{") && obj[key].endsWith("}")) { + obj[key] = JSON.parse(obj[key]); + } + } catch (error) { + console.error("Invalid JSON:", obj[key]); + } + } else if (typeof obj[key] === "object") { + obj[key] = recursiveJSONParse(obj[key]); + } + } + + return obj; +};