UNPKG

@pawel-up/jexl

Version:

Javascript Expression Language: Powerful context-based expression parser and evaluator

864 lines (646 loc) โ€ข 30.8 kB
# Jexl > **A modern, TypeScript-first fork of the original Jexl library, brought to the XXI century.** > > Maintained by [Pawel Uchida-Psztyc (@jarrodek)](https://github.com/jarrodek) with modern tooling, enhanced type safety, and improved developer experience. > > Original library created by [Tom Shawver](https://github.com/TomFrost). Javascript Expression Language: Powerful context-based expression parser and evaluator ## Why This Fork? This modernized version of Jexl brings several key improvements over the original: ### ๐Ÿš€ **Modern TypeScript Support** - **Full TypeScript rewrite** with comprehensive type definitions - **Enhanced type safety** with `unknown` types instead of unsafe `any` - **Better IDE support** with full IntelliSense and autocomplete - **Type-safe transform and function definitions** - write your custom transforms with proper typing ### ๐Ÿ›  **Modern Development Experience** - **ESM-first** with proper ES module support - **Modern build tooling** with up-to-date dependencies - **Comprehensive test coverage** with modern testing framework - **Clean, maintainable codebase** following current best practices ### ๐Ÿ“ฆ **Developer-Friendly** - **Flexible function signatures** - define transforms with specific parameter types - **Namespace support** - organize functions and transforms with dot notation (e.g., `Utils.String.upper`, `Api.User.getName()`) - **Better error handling** and debugging experience - **Consistent API** that works seamlessly in both Node.js and modern browsers - **Tree-shakeable** for optimal bundle sizes ### ๐Ÿ”ง **Enhanced Type Safety Example** ```typescript // Before: Unsafe any types jexl.addTransform('multiply', (val: any, factor: any) => val * factor) // Now: Type-safe transforms jexl.addTransform('multiply', (val: number, factor: number) => val * factor) jexl.addTransform('upperCase', (val: string) => val.toUpperCase()) jexl.addTransform('formatDate', (val: Date, format: string) => /* ... */) ``` ## Quick start Use it with promises or synchronously: ```javascript const context = { name: { first: 'Sterling', last: 'Archer' }, assoc: [ { first: 'Lana', last: 'Kane' }, { first: 'Cyril', last: 'Figgis' }, { first: 'Pam', last: 'Poovey' } ], age: 36 } // Filter an array asynchronously... await const res = jexl.eval('assoc[.first == "Lana"].last', context) console.log(res) // Output: Kane // Do math await jexl.eval('age * (3 - 1)', context) // 72 // Concatenate await jexl.eval('name.first + " " + name["la" + "st"]', context) // "Sterling Archer" // Compound await jexl.eval( 'assoc[.last == "Figgis"].first == "Cyril" && assoc[.last == "Poovey"].first == "Pam"', context ) // true // Use array indexes await jexl.eval('assoc[1]', context) // { first: 'Cyril', last: 'Figgis' } // Use conditional logic await jexl.eval('age > 62 ? "retired" : "working"', context) // "working" // Transform jexl.addTransform('upper', (val) => val.toUpperCase()) await jexl.eval('"duchess"|upper + " " + name.last|upper', context) // "DUCHESS ARCHER" // Namespace transforms (NEW!) jexl.addTransform('String.upper', (val) => val.toUpperCase()) jexl.addTransform('String.repeat', (val, times) => val.repeat(times)) await jexl.eval('"hi"|String.repeat(3)|String.upper', context) // "HIHIHI" // Transform asynchronously, with arguments jexl.addTransform('getStat', async (val, stat) => dbSelectByLastName(val, stat)) try { const res = await jexl.eval('name.last|getStat("weight")', context) console.log(res) // Output: 184 } catch (e) { console.log('Database Error', e.stack) } // Functions too, sync or async, args or no args jexl.addFunction('getOldestAgent', () => db.getOldestAgent()) await jexl.eval('age == getOldestAgent().age', context) // false // Namespace functions (NEW!) jexl.addFunction('Math.max', (a, b) => Math.max(a, b)) jexl.addFunction('Utils.String.slugify', (text) => text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) await jexl.eval('Math.max(age, 25)', context) // 36 // Add your own (a)synchronous operators // Here's a case-insensitive string equality jexl.addBinaryOp( '_=', 20, (left, right) => left.toLowerCase() === right.toLowerCase() ) await jexl.eval('"Guest" _= "gUeSt"') // true // Compile your expression once, evaluate many times! const { expr } = jexl const danger = expr`"Danger " + place` // Also: jexl.compile('"Danger " + place') await danger.eval({ place: 'zone' }) // Danger zone await danger.eval({ place: 'ZONE!!!' }) // Danger ZONE!!! (Doesn't recompile the expression!) ``` ## Migration from Original Jexl Upgrading from the original Jexl library is straightforward: ### Package Installation ```bash # Remove old package npm uninstall jexl # Install modern version npm install @pawel-up/jexl ``` ### Import Changes ```typescript // Before const jexl = require('jexl') // or import jexl from 'jexl' // Now import { Jexl } from '@pawel-up/jexl' ``` ### Enhanced Transform and Function Definitions The API remains the same, but you can now use proper TypeScript types and namespace organization: ```typescript // Before: No type safety jexl.addTransform('multiply', (val, factor) => val * factor) // Now: Full type safety (optional but recommended) jexl.addTransform('multiply', (val: number, factor: number): number => val * factor) // NEW: Namespace support for better organization jexl.addTransform('Math.multiply', (val: number, factor: number): number => val * factor) jexl.addTransform('String.upper', (val: string): string => val.toUpperCase()) jexl.addFunction('Utils.String.slugify', (text: string): string => text .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') ) ``` ### Backwards Compatibility - โœ… **99% API compatible** - existing code mostly works without changes. We removed the `evalSync()` methods. - โœ… **Same expression syntax** - no need to update your expressions - โœ… **Same behavior** - results are identical to the original library - โœ… **Same performance** - optimized for modern JavaScript engines ## Installation Jexl works on the backend, and on the frontend if bundled using a bundler like Parcel or Webpack. Install from npm: ```sh npm install @pawel-up/jexl --save ``` and use it: ```javascript import { Jexl } from '@pawel-up/jexl' ``` ## All the details ### Unary Operators | Operation | Symbol | | --------- | :----: | | Negate | ! | ### Binary Operators | Operation | Symbol | | ---------------- | :----------: | | Add, Concat | + | | Subtract | - | | Multiply | \* | | Divide | / | | Divide and floor | // | | Modulus | % | | Power of | ^ | | Logical AND | && | | Logical OR | &#124;&#124; | ### Comparisons | Comparison | Symbol | | -------------------------- | :----: | | Equal | == | | Not equal | != | | Greater than | > | | Greater than or equal | >= | | Less than | < | | Less than or equal | <= | | Element in array or string | in | #### A note about `in` The `in` operator can be used to check for a substring: `"Cad" in "Ron Cadillac"`, and it can be used to check for an array element: `"coarse" in ['fine', 'medium', 'coarse']`. However, the `==` operator is used behind-the-scenes to search arrays, so it should not be used with arrays of objects. The following expression returns false: `{a: 'b'} in [{a: 'b'}]`. ### Ternary operator Conditional expressions check to see if the first segment evaluates to a truthy value. If so, the consequent segment is evaluated. Otherwise, the alternate is. If the consequent section is missing, the test result itself will be used instead. | Expression | Result | | --------------------------------- | ------ | | "" ? "Full" : "Empty" | Empty | | "foo" in "foobar" ? "Yes" : "No" | Yes | | {agent: "Archer"}.agent ?: "Kane" | Archer | ### Native Types | Type | Examples | | -------- | :----------------------------: | | Booleans | `true`, `false` | | Strings | "Hello \"user\"", 'Hey there!' | | Numerics | 6, -7.2, 5, -3.14159 | | Objects | {hello: "world!"} | | Arrays | ['hello', 'world!'] | ### Groups Parentheses work just how you'd expect them to: | Expression | Result | | ----------------------------------- | :----- | | (83 + 1) / 2 | 42 | | 1 < 3 && (4 > 2 &#124;&#124; 2 > 4) | true | ### Identifiers Access variables in the context object by just typing their name. Objects can be traversed with dot notation, or by using brackets to traverse to a dynamic property name. Example context: ```javascript { name: { first: "Malory", last: "Archer" }, exes: [ "Nikolai Jakov", "Len Trexler", "Burt Reynolds" ], lastEx: 2 } ``` | Expression | Result | | ----------------- | ------------- | | name.first | Malory | | name['la' + 'st'] | Archer | | exes[2] | Burt Reynolds | | exes[lastEx - 1] | Len Trexler | ### Collections Collections, or arrays of objects, can be filtered by including a filter expression in brackets. Properties of each collection can be referenced by prefixing them with a leading dot. The result will be an array of the objects for which the filter expression resulted in a truthy value. Example context: ```javascript { employees: [ {first: 'Sterling', last: 'Archer', age: 36}, {first: 'Malory', last: 'Archer', age: 75}, {first: 'Lana', last: 'Kane', age: 33}, {first: 'Cyril', last: 'Figgis', age: 45}, {first: 'Cheryl', last: 'Tunt', age: 28} ], retireAge: 62 } ``` | Expression | Result | | --------------------------------------------- | ------------------------------------------------------------------------------------- | | employees[.first == 'Sterling'] | [{first: 'Sterling', last: 'Archer', age: 36}] | | employees[.last == 'Tu' + 'nt'].first | Cheryl | | employees[.age >= 30 && .age < 40] | [{first: 'Sterling', last: 'Archer', age: 36},{first: 'Lana', last: 'Kane', age: 33}] | | employees[.age >= 30 && .age < 40][.age < 35] | [{first: 'Lana', last: 'Kane', age: 33}] | | employees[.age >= retireAge].first | Malory | ### Transforms The power of Jexl is in transforming data, synchronously or asynchronously. Transform functions take one or more arguments: The value to be transformed, followed by anything else passed to it in the expression. They must return either the transformed value, or a Promise that resolves with the transformed value. Add them with `jexl.addTransform(name, function)`. ```javascript jexl.addTransform('split', (val, char) => val.split(char)) jexl.addTransform('lower', (val) => val.toLowerCase()) ``` | Expression | Result | | ---------------------------------------- | --------------------- | | `"Pam Poovey" \| lower \| split[' '](1)` | poovey | | `"password==guest" \| split('=' + '=')` | ['password', 'guest'] | #### Advanced Transforms Using Transforms, Jexl can support additional string formats like embedded JSON, YAML, XML, and more. The following, with the help of the [xml2json](https://github.com/buglabs/node-xml2json) module, allows XML to be traversed just as easily as plain javascript objects: ```javascript const xml2json = require('xml2json') jexl.addTransform('xml', (val) => xml2json.toJson(val, { object: true })) const context = { xmlDoc: ` <Employees> <Employee> <FirstName>Cheryl</FirstName> <LastName>Tunt</LastName> </Employee> <Employee> <FirstName>Cyril</FirstName> <LastName>Figgis</LastName> </Employee> </Employees>`, } var expr = 'xmlDoc|xml.Employees.Employee[.LastName == "Figgis"].FirstName' jexl.eval(expr, context).then(console.log) // Output: Cyril ``` ### Namespace Functions and Transforms Jexl supports organizing functions and transforms into namespaces using dot notation. This allows you to create hierarchical structures and avoid naming conflicts in large applications. #### Namespace Functions Functions can be organized into namespaces by using dot-separated names when registering them: ```javascript // Register namespace functions jexl.addFunction('Math.max', (a, b) => Math.max(a, b)) jexl.addFunction('Utils.String.slugify', (text) => text .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') ) jexl.addFunction('Api.User.getName', async (userId) => { const user = await fetchUser(userId) return user.name }) // Use namespace functions in expressions await jexl.eval('Math.max(10, 25)') // 35 await jexl.eval('Utils.String.slugify("Hello World!")') // "hello-world" await jexl.eval('Api.User.getName(123)') // "John Doe" ``` #### Namespace Transforms Transforms can also be organized into namespaces and used with the pipe operator: ```javascript // Register namespace transforms jexl.addTransform('String.upper', (val) => val.toUpperCase()) jexl.addTransform('String.lower', (val) => val.toLowerCase()) jexl.addTransform('String.repeat', (val, times) => val.repeat(times)) jexl.addTransform('Utils.Text.capitalize', (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()) jexl.addTransform('Format.Date.iso', (date) => new Date(date).toISOString()) // Use namespace transforms in expressions await jexl.eval('"hello world"|String.upper') // "HELLO WORLD" await jexl.eval('"HELLO WORLD"|String.lower') // "hello world" await jexl.eval('"hi"|String.repeat(3)') // "hihihi" await jexl.eval('"hello world"|Utils.Text.capitalize') // "Hello world" await jexl.eval('"2024-01-01"|Format.Date.iso') // "2024-01-01T00:00:00.000Z" ``` #### Deeply Nested Namespaces Namespaces can be nested to any depth: ```javascript // Deep namespace registration jexl.addFunction('Company.Departments.HR.getEmployeeCount', () => 150) jexl.addTransform('Data.Validation.Email.isValid', (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) // Usage in expressions await jexl.eval('Company.Departments.HR.getEmployeeCount()') // 150 await jexl.eval('"user@example.com"|Data.Validation.Email.isValid') // true ``` #### Chaining Namespace Transforms Namespace transforms can be chained just like regular transforms: ```javascript // Register multiple namespace transforms jexl.addTransform('String.trim', (val) => val.trim()) jexl.addTransform('String.upper', (val) => val.toUpperCase()) jexl.addTransform('String.split', (val, separator) => val.split(separator)) // Chain namespace transforms await jexl.eval('" hello world "|String.trim|String.upper|String.split(" ")') // Result: ["HELLO", "WORLD"] ``` #### Namespace Transforms with Arguments Namespace transforms support arguments just like regular transforms: ```javascript // Transforms with multiple arguments jexl.addTransform('String.padStart', (val, length, padString) => val.padStart(length, padString)) jexl.addTransform('Array.slice', (arr, start, end) => arr.slice(start, end)) // Usage with arguments await jexl.eval('"5"|String.padStart(3, "0")') // "005" await jexl.eval('[1,2,3,4,5]|Array.slice(1, 4)') // [2,3,4] ``` #### Mixed Namespace and Regular Functions/Transforms You can mix namespace and regular functions/transforms in the same expression: ```javascript // Register both types jexl.addFunction('min', Math.min) jexl.addFunction('Utils.Math.average', (arr) => arr.reduce((a, b) => a + b) / arr.length) jexl.addTransform('sort', (arr) => [...arr].sort()) jexl.addTransform('Array.reverse', (arr) => [...arr].reverse()) // Mix in expressions await jexl.eval('min(Utils.Math.average([1,2,3]), 5)') // 2 await jexl.eval('[3,1,4,2]|sort|Array.reverse') // [4,3,2,1] ``` #### Namespace Organization Best Practices Consider organizing your functions and transforms by domain or functionality: ```javascript // By domain jexl.addFunction('User.authenticate', authenticateUser) jexl.addFunction('User.authorize', authorizeUser) jexl.addFunction('Product.getPrice', getProductPrice) jexl.addFunction('Order.calculate', calculateOrder) // By data type jexl.addTransform('String.capitalize', capitalizeString) jexl.addTransform('String.truncate', truncateString) jexl.addTransform('Array.unique', uniqueArray) jexl.addTransform('Array.flatten', flattenArray) jexl.addTransform('Date.format', formatDate) jexl.addTransform('Date.addDays', addDaysToDate) // By utility category jexl.addFunction('Validation.isEmail', isValidEmail) jexl.addFunction('Validation.isPhoneNumber', isValidPhoneNumber) jexl.addTransform('Format.currency', formatCurrency) jexl.addTransform('Format.percentage', formatPercentage) ``` #### Migration from Non-Namespaced Functions/Transforms Existing non-namespaced functions and transforms continue to work unchanged: ```javascript // Existing code continues to work jexl.addFunction('myFunc', () => 'old way') jexl.addTransform('myTransform', (val) => val + ' transformed') await jexl.eval('myFunc()') // "old way" await jexl.eval('"test"|myTransform') // "test transformed" // New namespace versions can coexist jexl.addFunction('Utils.myFunc', () => 'new way') jexl.addTransform('Utils.myTransform', (val) => val + ' namespace transformed') await jexl.eval('Utils.myFunc()') // "new way" await jexl.eval('"test"|Utils.myTransform') // "test namespace transformed" ``` ### Functions While Transforms are the preferred way to change one value into another value, Jexl also allows top-level expression functions to be defined. Use these to provide access to functions that either don't require an input, or require multiple equally-important inputs. They can be added with `jexl.addFunction(name, function)`. Like transforms, functions can return a value, or a Promise that resolves to the resulting value. ```javascript jexl.addFunction('min', Math.min) jexl.addFunction('expensiveQuery', async () => db.runExpensiveQuery()) ``` | Expression | Result | | --------------------------------------------- | ------------------------- | | min(4, 2, 19) | 2 | | counts.missions &#124;&#124; expensiveQuery() | Query only runs if needed | ### Context Variable contexts are straightforward Javascript objects that can be accessed in the expression, but they have a hidden feature: they can include a Promise object, and when that property is used, Jexl will wait for the Promise to resolve and use that value! ## API Reference ### Jexl Instance #### jexl.Jexl A reference to the Jexl constructor. To maintain separate instances of Jexl with each maintaining its own set of transforms, simply re-instantiate with `new jexl.Jexl()`. #### jexl.addBinaryOp(_{string} operator_, _{number} precedence_, _{function} fn_, _{boolean} [manualEval]_) Adds a binary operator to the Jexl instance. A binary operator is one that considers the values on both its left and right, such as "+" or "==", in order to calculate a result. The precedence determines the operator's position in the order of operations (please refer to `lib/grammar.js` to see the precedence of existing operators). The provided function will be called with two arguments: a left value and a right value. It should return either the resulting value, or a Promise that resolves to the resulting value. If `manualEval` is true, the `left` and `right` arguments will be wrapped in objects with an `eval` function. Calling `left.eval()` or `right.eval()` will return a promise that resolves to that operand's actual value. This is useful to conditionally evaluate operands, and is how `&&` and `||` work. #### jexl.addUnaryOp(_{string} operator_, _{function} fn_) Adds a unary operator to the Jexl instance. A unary operator is one that considers only the value on its right, such as "!", in order to calculate a result. The provided function will be called with one argument: the value to the operator's right. It should return either the resulting value, or a Promise that resolves to the resulting value. #### jexl.addFunction(_{string} name_, \_{function} func) Adds an expression function to this Jexl instance. The `name` can be a simple identifier (e.g., `'myFunction'`) or a namespace path using dot notation (e.g., `'Utils.String.slugify'`). See the **Functions** and **Namespace Functions and Transforms** sections above for information on the structure of an expression function. #### jexl.addFunctions(_{{}} map_) Adds multiple functions from a supplied map of function name to expression function. #### jexl.addTransform(_{string} name_, _{function} transform_) Adds a transform function to this Jexl instance. The `name` can be a simple identifier (e.g., `'myTransform'`) or a namespace path using dot notation (e.g., `'String.upper'`). See the **Transforms** and **Namespace Functions and Transforms** sections above for information on the structure of a transform function. #### jexl.addTransforms(_{{}} map_) Adds multiple transforms from a supplied map of transform name to transform function. #### jexl.compile(_{string} expression_) Constructs an Expression object around the given Jexl expression string. Expression objects allow a Jexl expression to be compiled only once but evaluated many times. See the Expression API below. Note that the only difference between this function and `jexl.createExpression` is that this function will immediately compile the expression, and throw any errors associated with invalid expression syntax. #### jexl.createExpression(_{string} expression_) Constructs an Expression object around the given Jexl expression string. Expression objects allow a Jexl expression to be compiled only once but evaluated many times. See the Expression API below. #### jexl.getTransform(_{string} name_) **Returns `{function|undefined}`.** Gets a previously set transform function, or `undefined` if no function of that name exists. #### jexl.eval(_{string} expression_, _{{}} [context]_) **Returns `{Promise<*>}`.** Evaluates an expression. The context map is optional. #### jexl.expr: _tagged template literal_ A convenient bit of syntactic sugar for `jexl.createExpression` ```javascript const someNumber = 10 const expression = jexl.expr`5 + ${someNumber}` console.log(await expression.eval()) // 15 ``` Note that `expr` will stay bound to its associated Jexl instance even if it's pulled out of context: ```javascript const { expr } = jexl jexl.addTransform('double', (val) => val * 2) const expression = expr`2|double` console.log(await expression.eval()) // 4 ``` #### jexl.removeOp(_{string} operator_) Removes a binary or unary operator from the Jexl instance. For example, "^" can be passed to eliminate the "power of" operator. ### Expression Expression objects are created via `jexl.createExpression`, `jexl.compile`, or `jexl.expr`, and are a convenient way to ensure jexl expressions compile only once, even if they're evaluated multiple times. #### expression.compile() **Returns self `{Expression}`.** Forces the expression to compile, even if it was compiled before. Note that each compile will happen with the latest grammar and transforms from the associated Jexl instance. #### expression.eval(_{{}} [context]_) **Returns `{Promise<*>}`.** Evaluates the expression. The context map is optional. ## Expression Validation This modernized version of Jexl includes a powerful `Validator` class that helps you validate expressions before evaluation, providing comprehensive error reporting and analysis. ### Validator Class The `Validator` class provides static methods to validate Jexl expressions, offering detailed feedback about syntax errors, semantic issues, and potential problems. #### Key Features - ๐Ÿ” **Comprehensive Validation**: Checks syntax, semantics, and context usage - ๐Ÿšจ **Detailed Error Reporting**: Provides specific error messages with position information - โš ๏ธ **Warning System**: Identifies potential issues and performance concerns - โ„น๏ธ **Informational Messages**: Offers suggestions for improvements - ๐Ÿงน **Automatic Whitespace Trimming**: Cleans expressions before validation - ๐Ÿ”ง **Context-Agnostic Mode**: Validates expressions without requiring specific context - **JavaScript-aligned Logic**: Correctly distinguishes between `null` and `undefined` and handles `&&`/`||` operator precedence just like in JS. - **Powerful Operators**: Full support for unary minus and plus on expressions (e.g., `-myFunc()`, `+variable`). - **Robust Parsing**: Handles complex cases like negative numbers in function arguments (`myFunc(5, -3)`), ternary operators after function calls, and leading decimal numbers (`.5`). - **Clear Error Handling**: Provides precise errors for ambiguous syntax like `2++2`. #### Basic Usage ```typescript import { Validator, Jexl } from '@pawel-up/jexl' const jexl = new Jexl() const validator = new Validator(jexl.grammar) // Validate a simple expression const result = validator.validate('name.first + " " + name.last') console.log(result.isValid) // true console.log(result.errors) // [] // Validate with context const context = { name: { first: 'John', last: 'Doe' } } const result2 = validator.validate('name.middle', context) console.log(result2.isValid) // false console.log(result2.errors[0].message) // "Property 'middle' not found in context object 'name'" ``` #### Validation Result The `validate` method returns a `ValidationResult` object with the following properties: ```typescript interface ValidationResult { isValid: boolean // Overall validation status errors: ValidationIssue[] // Critical errors that prevent execution warnings: ValidationIssue[] // Non-critical issues that might cause problems info: ValidationIssue[] // Informational suggestions trimmedExpression: string // Expression after automatic whitespace trimming } interface ValidationIssue { severity: 'error' | 'warning' | 'info' message: string position?: number // Character position in expression line?: number // Line number (for multi-line expressions) column?: number // Column number } ``` #### Automatic Whitespace Trimming The Validator automatically trims leading and trailing whitespace from expressions before validation: ```typescript // These expressions are equivalent after trimming validator.validate('name.first') validator.validate(' name.first ') validator.validate('\t\nname.first\n\t') ``` #### Context-Agnostic Validation Use `allowUndefinedContext: true` to validate expressions without providing specific context: ```typescript // Validate syntax only, ignore missing context const result = validator.validate('user.profile.email', undefined, { allowUndefinedContext: true, }) console.log(result.isValid) // true (if syntax is correct) console.log(result.warnings) // May contain warnings about undefined context ``` #### Validation Examples ```typescript // Syntax error const syntaxError = validator.validate('name.first +') console.log(syntaxError.errors[0].message) // "Unexpected end of expression" // Context validation const context = { user: { name: 'John' } } const contextError = validator.validate('user.age', context) console.log(contextError.errors[0].message) // "Property 'age' not found in context object 'user'" // Performance warning const perfWarning = validator.validate('users[.name == "John" && .active == true]') console.log(perfWarning.warnings[0].message) // "Complex filter expression may impact performance" // Style suggestion const styleInfo = validator.validate('name["first"]') console.log(styleInfo.info[0].message) // "Consider using dot notation: name.first" ``` #### Advanced Usage ```typescript // Validate multiple expressions const expressions = ['user.name', 'user.age > 18', 'users[.active == true].length'] expressions.forEach((expr, index) => { const result = validator.validate(expr, context) if (!result.isValid) { console.log(`Expression ${index + 1} has errors:`, result.errors) } if (result.warnings.length > 0) { console.log(`Expression ${index + 1} has warnings:`, result.warnings) } }) // Custom validation with options const result = validator.validate(' user.email ', undefined, { allowUndefinedContext: true, }) console.log(`Original: "${result.trimmedExpression}"`) console.log(`Trimmed: "${result.trimmedExpression}"`) console.log(`Valid: ${result.isValid}`) ``` #### Validator API Reference ##### validate(expression, context?, options?) **Parameters:** - `expression` _{string}_: The Jexl expression to validate - `context` _{object}_: Optional context object for validation - `options` _{object}_: Optional validation options - `allowUndefinedContext` _{boolean}_: Allow undefined context properties (default: false) **Returns:** `ValidationResult` object with validation details ##### ValidationResult Properties - `isValid` _{boolean}_: True if expression has no errors - `errors` _{ValidationIssue[]}_: Array of critical validation errors - `warnings` _{ValidationIssue[]}_: Array of non-critical warnings - `info` _{ValidationIssue[]}_: Array of informational suggestions - `trimmedExpression` _{string}_: Expression after whitespace trimming ## Other implementations - [PyJEXL](https://github.com/mozilla/pyjexl) - A Python-based JEXL parser and evaluator. - [JexlApex](https://github.com/the-last-byte/JexlApex) - A Salesforce Apex JEXL parser and evaluator. ## License Jexl is licensed under the MIT license. Please see `LICENSE.txt` for full details. ## Credits **Current Maintainer:** [Pawel Uchida-Psztyc (@jarrodek)](https://github.com/jarrodek) - Modernized the library with TypeScript, enhanced type safety, and modern tooling. **Original Creator:** [Tom Shawver (@TomFrost)](https://github.com/TomFrost) - Created the original Jexl library in 2015. **Contributors:** Thanks to [all the contributors](https://github.com/TomFrost/Jexl/graphs/contributors) who helped make the original Jexl library great. The original Jexl was created at [TechnologyAdvice](http://technologyadvice.com) in Nashville, TN.