logical-expression-parser
v2.1.0
Published
Logical expression parser and evaluator, suitable for permissions management
Maintainers
Readme
Logical Expression Parser
Parses logical expressions like REGISTED&(SPECIAL|INVITED) into an AST and evaluates them with your own token checker. Suitable for permissions management. Written in TypeScript, zero runtime dependencies, ships both CommonJS and ESM builds.
Supported operators
| Operator | Meaning | Precedence |
| -------- | -------- | ---------- |
| ! | Not | highest |
| & | And | middle |
| \| | Or | lowest |
| (...) | Grouping | — |
&binds tighter than|:A&B|Cmeans(A&B)|C.&and|are left-associative.- Whitespace between tokens is ignored.
- Literals are any runs of characters other than whitespace and the operators above (unicode included).
How it works
- You require permissions such as
REGISTED&(SPECIAL|INVITED). - The parser builds an AST and passes each literal (
REGISTED,SPECIAL,INVITED) to your token checking function. - The AST is evaluated with short-circuiting, so your checker is only called for tokens that can still change the result.
Example (CommonJS)
const { parse } = require('logical-expression-parser');
const REQUIREMENTS = 'REGISTED&(SPECIAL|INVITED)';
const LIST_A = ['REGISTED', 'INVITED'];
const LIST_B = ['SPECIAL', 'EXPERT'];
const RESULT_A = parse(REQUIREMENTS, token => LIST_A.includes(token));
const RESULT_B = parse(REQUIREMENTS, token => LIST_B.includes(token));
// RESULT_A: true
// RESULT_B: falseExample (ESM)
import { parse } from 'logical-expression-parser';
// same as aboveAST API
import { parseAst, evaluate } from 'logical-expression-parser';
const ast = parseAst('REGISTED&!(BANNED)');
// {
// type: 'and',
// left: { type: 'literal', value: 'REGISTED' },
// right: { type: 'not', operand: { type: 'literal', value: 'BANNED' } }
// }
const allowed = evaluate(ast, token => userPermissions.includes(token));Async Evaluation
For permission checks requiring database or network calls, use parseAsync or evaluateAsync:
import { parseAsync } from 'logical-expression-parser';
const allowed = await parseAsync('ADMIN | (SPECIAL & !BANNED)', async token => {
return await checkUserPermission(userId, token);
});Like the synchronous API, evaluation short-circuits so subsequent async checks are skipped once the outcome is determined.
Custom Operators
You can register custom binary and prefix unary operators with their own precedence, associativity, and evaluation logic using createParser:
import { createParser } from 'logical-expression-parser';
const parser = createParser({
customOperators: [
{
kind: 'binary',
symbol: '^',
precedence: 15, // between OR (10) and AND (20)
associativity: 'left',
evaluate: (left, right) => left !== right(),
evaluateAsync: async (left, right) => left !== (await right()),
},
{
kind: 'binary',
symbol: '->',
precedence: 5, // looser than OR
associativity: 'right',
// Short-circuiting: if left is false, right() is skipped
evaluate: (left, right) => (!left ? true : right()),
evaluateAsync: async (left, right) => (!left ? true : await right()),
},
{
kind: 'prefix',
symbol: '~',
precedence: 30, // same as '!'
evaluate: operand => !operand,
evaluateAsync: async operand => !(await operand),
},
],
});
const allowed = parser.parse('ADMIN ^ (USER -> BETA)', checker);Built-in Precedence Reference
|(OR):10&(AND):20!(NOT):30
Word operators (e.g. XOR, NAND) are also supported with word-boundary safety (they won't split literals like XOR_GATE).
Types
export type TokenChecker = (token: string) => boolean;
export type AsyncTokenChecker = (token: string) => boolean | Promise<boolean>;
export type LiteralNode = { readonly type: 'literal'; readonly value: string };
export type NotNode = { readonly type: 'not'; readonly operand: AstNode };
export type BinaryNode = {
readonly type: 'and' | 'or';
readonly left: AstNode;
readonly right: AstNode;
};
export type CustomBinaryNode = {
readonly type: 'custom_binary';
readonly operator: string;
readonly left: AstNode;
readonly right: AstNode;
};
export type CustomUnaryNode = {
readonly type: 'custom_unary';
readonly operator: string;
readonly operand: AstNode;
};
export type AstNode =
| LiteralNode
| NotNode
| BinaryNode
| CustomBinaryNode
| CustomUnaryNode;Errors
Malformed expressions — empty inputs, A&, &A, (), unbalanced parentheses, stray tokens — throw LEPSyntaxError, a SyntaxError subclass carrying the character offset. Invalid argument types (e.g. non-string expression or non-function checker) throw TypeError.
import { parse, LEPSyntaxError } from 'logical-expression-parser';
try {
parse('A&(B|C', checker);
} catch (error) {
if (error instanceof LEPSyntaxError) {
// error.offset === 6
}
}Development
Requires Node >= 18.
npm install
npm test # builds dist/, compiles tests, runs node --test with a c8 coverage report, smoke-checks both dist buildsSources live in src/, tests in test/; npm run build emits dist/cjs and dist/esm.
Releasing
Releases publish to npm from GitHub Actions when a v-tag is pushed:
- Bump
versioninpackage.jsonand commit. git tag v<version> && git push origin v<version>— the tag must matchversionexactly (enforced by the workflow).- The workflow builds, runs the full test suite, and publishes with provenance using npm Trusted Publishing (OIDC keyless publishing; configure your repository once under Package Settings → Trusted Publishing on npmjs.com).
