UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

424 lines (361 loc) 11.6 kB
// the parser is dynamically generated from generateParser.js at compile time import { parser } from "./parser.mjs"; import { hasOwnProperty, bool, num, numstr, mod, arr, str, flatten, code, } from "./utils.mjs"; import { UnknownFunctionError, UnknownPropertyError, UnknownOptionError, InternalError, } from "./errors.mjs"; // Shared utility functions const std = { isfn(fns, funcName) { return hasOwnProperty(fns, funcName) && typeof fns[funcName] === "function"; }, unknown(funcName) { throw new UnknownFunctionError(funcName); }, coerceArray: arr, coerceNumber: num, coerceNumberOrString: numstr, coerceBoolean: bool, isSubset(a, b) { const A = arr(a); const B = arr(b); return A.every((val) => B.includes(val)); }, warnDeprecated: (function () { const warnMax = 3; let warnedTimes = { ternary: 0, modulo: 0, }; return (cause, value) => { switch (cause) { case "ternary": if (warnedTimes.ternary++ >= warnMax) break; console.warn( "The use of ? and : as conditional operators has been deprecated " + "in Filtrex v3 in favor of the if..then..else ternary operator. " + "See issue #34 for more information.", ); break; case "modulo": if (warnedTimes.modulo++ >= warnMax) break; console.warn( "The use of '%' as a modulo operator has been deprecated in Filtrex v3 " + "in favor of the 'mod' operator. You can use it like this: '3 mod 2 == 1'. " + "See issue #48 for more information.", ); break; } return value; }; })(), buildString(quote, literal) { quote = String(quote)[0]; literal = String(literal); let built = ""; if (literal[0] !== quote || literal[literal.length - 1] !== quote) throw new InternalError( `Unexpected internal error: String literal doesn't begin/end with the right quotation mark.`, ); for (let i = 1; i < literal.length - 1; i++) { if (literal[i] === "\\") { i++; if (i >= literal.length - 1) throw new InternalError( `Unexpected internal error: Unescaped backslash at the end of string literal.`, ); if (literal[i] === "\\") built += "\\"; else if (literal[i] === quote) built += quote; else throw new InternalError( `Unexpected internal error: Invalid escaped character in string literal: ${literal[i]}`, ); } else if (literal[i] === quote) { throw new InternalError( `Unexpected internal error: String literal contains unescaped quotation mark.`, ); } else { built += literal[i]; } } return built; }, reduceRelation(arr) { const declarations = []; const comparisons = []; let previousExpression = flatten([arr[0]]).join(""); let j = 0; for (let i = 1; i < arr.length - 1; i += 2) { const expr = flatten([arr[i + 1]]).join(""); const tempVar = `tmp${j++}`; comparisons.push( `ops["${arr[i]}"](${previousExpression}, ${tempVar} = ${expr})`, ); previousExpression = tempVar; declarations.push(tempVar); } return `(function(){ var ${declarations.join(", ")}; return ${comparisons.join(" && ")};})()`; }, }; parser.yy = Object.create(std); /** * A custom prop function which doesn't throw an UnknownPropertyError * if the user tries to access a property of `undefined` and `null`, * but instead returns `unknown` or `null`. This effectively turns * `a of b` into `b.?a`. You can use this function using the following * code: * ``` * import { * compileExpression, * useOptionalChaining * } from 'filtrex' * * const expr = "foo of bar" * * const fn = compileExpression(expr, { * customProp: useOptionalChaining * }); * * fn({ bar: null }) // → null * ``` */ export function useOptionalChaining(name, get, obj, type) { if (obj === null || obj === undefined) return obj; return get(name); } /** * A custom prop function which treats dots inside a symbol * as property accessors. If you want to use the `foo.bar` * syntax to access properties instead of the default * `bar of foo`, you can use this function using the following * code: * ``` * import { * compileExpression, * useDotAccessOperator * } from 'filtrex' * * const expr = "foo.bar" * * const fn = compileExpression(expr, { * customProp: useDotAccessOperator * }); * * fn({ foo: { bar: 42 } }) // → 42 * ``` */ export function useDotAccessOperator(name, get, obj, type) { // ignore dots inside escaped symbol if (type === "single-quoted") return get(name); const parts = name.split("."); for (const propertyName of parts) { if (hasOwnProperty(obj ?? {}, propertyName)) { obj = obj[propertyName]; } else { throw new UnknownPropertyError(propertyName); } } return obj; } /** * A custom prop function which combines `useOptionalChaining` and `useDotAccessOperator`. * The user can use both `foo of bar` and `bar.foo`, both have optional chaining. * You can use this function using the following code: * ``` * import { * compileExpression, * useDotAccessOperatorAndOptionalChaining * } from 'filtrex' * * const expr = "foo.bar" * * const fn = compileExpression(expr, { * customProp: useDotAccessOperatorAndOptionalChaining * }); * * fn({ foo: null }) // → null * ``` */ export function useDotAccessOperatorAndOptionalChaining(name, get, obj, type) { if (obj === null || obj === undefined) return obj; // ignore dots inside escaped symbol if (type === "single-quoted") return get(name); const parts = name.split("."); for (const propertyName of parts) { if (obj === null || obj === undefined) { return obj; } else { obj = obj[propertyName]; } } return obj; } /** * A simple, safe, JavaScript expression engine, allowing end-users to enter arbitrary expressions without p0wning you. * * @example * // Input from user (e.g. search filter) * let expression = 'transactions <= 5 and abs(profit) > 20.5'; * * // Compile expression to executable function * let myfilter = compileExpression(expression); * * // Execute function * myfilter({transactions: 3, profit:-40.5}); // returns 1 * myfilter({transactions: 3, profit:-14.5}); // returns 0 * * @param expression * The expression to be parsed. Under the hood, the expression gets compiled to a clean and fast JavaScript function. * There are only 2 types: numbers and strings. Numbers may be floating point or integers. Boolean logic is applied * on the truthy value of values (e.g. any non-zero number is true, any non-empty string is true, otherwise false). * Examples of numbers: `43`, `-1.234`; example of a string: `"hello"`; example of external data variable: `foo`, `a.b.c`, * `'foo-bar'`. * You can use the following operations: * * `x + y` Add * * `x - y` Subtract * * `x * y` Multiply * * `x / y` Divide * * `x ^ y` Power * * `x mod y` Modulo * * `x == y` Equals * * `x < y` Less than * * `x <= y` Less than or equal to * * `x > y` Greater than * * `x >= y` Greater than or equal to * * `x == y <= z` Chained relation, equivalent to `(x == y and y <= z)` * * `x of y` Get property x of object y * * `x in (a, b, c)` Equivalent to `(x == a or x == b or x == c)` * * `x not in (a, b, c)` Equivalent to `(x != a and x != b and x != c)` * * `x or y` Boolean or * * `x and y` Boolean and * * `not x` Boolean not * * `if x then y else z` If boolean x, value y, else z * * `( x )` Explicity operator precedence * * `( x, y, z )` Array of elements x, y and z * * `abs(x)` Absolute value * * `ceil(x)` Round floating point up * * `floor(x)` Round floating point down * * `log(x)` Natural logarithm * * `log2(x)` Binary logarithm * * `log10(x)` Decadic logarithm * * `max(a, b, c...)` Max value (variable length of args) * * `min(a, b, c...)` Min value (variable length of args) * * `round(x)` Round floating point * * `sqrt(x)` Square root * * `exists(x)` True if `x` is neither `undefined` nor `null` * * `empty(x)` True if `x` doesn't exist, it is an empty string or empty array * * `myFooBarFunction(x)` Custom function defined in `options.extraFunctions` */ export function compileExpression(expression, options) { // Check and coerce arguments if (arguments.length > 2) throw new TypeError("Too many arguments."); options = typeof options === "object" ? options : {}; const knownOptions = [ "extraFunctions", "constants", "customProp", "operators", ]; let { extraFunctions, constants, customProp, operators } = options; for (const key of Object.keys(options)) if (!knownOptions.includes(key)) throw new UnknownOptionError(key); // Functions available to the expression let functions = { abs: Math.abs, ceil: Math.ceil, floor: Math.floor, log: Math.log, log2: Math.log2, log10: Math.log10, max: Math.max, min: Math.min, round: Math.round, sqrt: Math.sqrt, exists: (v) => v !== undefined && v !== null, empty: (v) => v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0), }; if (extraFunctions) { for (const name of Object.keys(extraFunctions)) { functions[name] = extraFunctions[name]; } } let defaultOperators = { "+": (a, b) => numstr(a) + numstr(b), "-": (a, b) => (b === undefined ? -num(a) : num(a) - num(b)), "*": (a, b) => num(a) * num(b), "/": (a, b) => num(a) / num(b), "^": (a, b) => Math.pow(num(a), num(b)), mod: (a, b) => mod(num(a), num(b)), "==": (a, b) => a === b, "!=": (a, b) => a !== b, "<": (a, b) => num(a) < num(b), "<=": (a, b) => num(a) <= num(b), ">=": (a, b) => num(a) >= num(b), ">": (a, b) => num(a) > num(b), "~=": (a, b) => RegExp(str(b)).test(str(a)), }; if (operators) { for (const name of Object.keys(operators)) { defaultOperators[name] = operators[name]; } } operators = defaultOperators; constants = constants ?? {}; // Compile the expression let js = flatten(parser.parse(expression)); js.unshift("return "); js.push(";"); // Metaprogramming functions function nakedProp(name, obj, type) { if (hasOwnProperty(obj ?? {}, name)) return obj[name]; throw new UnknownPropertyError(name); } function safeGetter(obj) { return function get(name) { if (hasOwnProperty(obj ?? {}, name)) return obj[name]; throw new UnknownPropertyError(name); }; } if (typeof customProp === "function") { nakedProp = (name, obj, type) => customProp(name, safeGetter(obj), obj, type); } function createCall(fns) { return function call({ name }, ...args) { if (hasOwnProperty(fns, name) && typeof fns[name] === "function") return fns[name](...args); throw new UnknownFunctionError(name); }; } function prop({ name, type }, obj) { if (type === "unescaped" && hasOwnProperty(constants, name)) return constants[name]; return nakedProp(name, obj, type); } // Patch together and return let func = new Function("call", "ops", "std", "prop", "data", js.join("")); return function (data) { try { return func(createCall(functions), operators, std, prop, data); } catch (e) { return e; } }; }