Comparing version 0.0.3 to 0.9.0
@@ -92,3 +92,4 @@ /* | ||
"asty", | ||
"cache-lru" | ||
"cache-lru", | ||
"source-code-error" | ||
], | ||
@@ -95,0 +96,0 @@ browserifyOptions: { |
{ | ||
"name": "ael", | ||
"version": "0.0.3", | ||
"version": "0.9.0", | ||
"description": "Advanced Expression Language", | ||
@@ -25,3 +25,4 @@ "keywords": [ "expression", "language", "evaluation" ], | ||
"asty": "1.8.11", | ||
"cache-lru": "1.1.11" | ||
"cache-lru": "1.1.11", | ||
"source-code-error": "1.0.2" | ||
}, | ||
@@ -28,0 +29,0 @@ "devDependencies": { |
228
README.md
@@ -13,2 +13,13 @@ | ||
About | ||
----- | ||
Advanced Expression Language (AEL) is a JavaScript library for use | ||
in the Browser and Node.js to parse/compile and execute/evaluate | ||
JavaScript-style expressions. The expressions are based on conditional, | ||
logical, bitwise, relational, arithmetical, functional, selective and | ||
literal constructs and hence can express arbitrary complex matchings and | ||
lookups. The result can be an arbitrary value, but usually is just a | ||
boolean one. | ||
Installation | ||
@@ -21,93 +32,164 @@ ------------ | ||
About | ||
Usage | ||
----- | ||
Advanced Expression Language (AEL) is a library in JavaScript for use | ||
in the Browser and Node.js to parse and evaluate JavaScript-style | ||
expressions. | ||
``` | ||
$ cat sample.js | ||
const AEL = require("..") | ||
Expression Language | ||
------------------- | ||
const ael = new AEL({ trace: (msg) => console.log(msg) }) | ||
### By Example | ||
const expr = `foo.quux =~ /ux$/ && foo.bar.a == 1` | ||
FIXME | ||
const data = { | ||
foo: { | ||
bar: { a: 1, b: 2, c: 3 }, | ||
baz: [ "a", "b", "c", "d", "e" ], | ||
quux: "quux" | ||
} | ||
} | ||
### By Grammar | ||
try { | ||
const result = ael.evaluate(expr, data) | ||
console.log("RESULT", result) | ||
} | ||
catch (ex) { | ||
console.log("ERROR", ex.toString()) | ||
} | ||
FIXME | ||
$ node sample.js | ||
AEL: compile: +---(expression string)--------------------------------------------------------------------------------- | ||
AEL: compile: | foo.quux =~ /ux$/ && foo.bar.a == 1 | ||
AEL: compile: +---(abstract syntax tree)------------------------------------------------------------------------------ | ||
AEL: compile: | Logical (op: "&&", expr: "foo.quux =~ /ux$/ && foo.bar.a == 1") [1,1] | ||
AEL: compile: | ├── Relational (op: "=~") [1,1] | ||
AEL: compile: | │ ├── Select [1,1] | ||
AEL: compile: | │ │ ├── Variable (id: "foo") [1,1] | ||
AEL: compile: | │ │ └── SelectItem [1,4] | ||
AEL: compile: | │ │ └── Identifier (id: "quux") [1,5] | ||
AEL: compile: | │ └── LiteralRegExp (value: /ux$/) [1,13] | ||
AEL: compile: | └── Relational (op: "==") [1,22] | ||
AEL: compile: | ├── Select [1,22] | ||
AEL: compile: | │ ├── Variable (id: "foo") [1,22] | ||
AEL: compile: | │ ├── SelectItem [1,25] | ||
AEL: compile: | │ │ └── Identifier (id: "bar") [1,26] | ||
AEL: compile: | │ └── SelectItem [1,29] | ||
AEL: compile: | │ └── Identifier (id: "a") [1,30] | ||
AEL: compile: | └── LiteralNumber (value: 1) [1,35] | ||
AEL: execute: +---(evaluation recursion tree)------------------------------------------------------------------------- | ||
AEL: execute: | Logical { | ||
AEL: execute: | Relational { | ||
AEL: execute: | Select { | ||
AEL: execute: | Variable { | ||
AEL: execute: | }: {"bar":{"a":1,"b":2,"c":3},"baz":["a","b... | ||
AEL: execute: | Identifier { | ||
AEL: execute: | }: "quux" | ||
AEL: execute: | }: "quux" | ||
AEL: execute: | LiteralRegExp { | ||
AEL: execute: | }: {} | ||
AEL: execute: | }: true | ||
AEL: execute: | Relational { | ||
AEL: execute: | Select { | ||
AEL: execute: | Variable { | ||
AEL: execute: | }: {"bar":{"a":1,"b":2,"c":3},"baz":["a","b... | ||
AEL: execute: | Identifier { | ||
AEL: execute: | }: "bar" | ||
AEL: execute: | Identifier { | ||
AEL: execute: | }: "a" | ||
AEL: execute: | }: 1 | ||
AEL: execute: | LiteralNumber { | ||
AEL: execute: | }: 1 | ||
AEL: execute: | }: true | ||
AEL: execute: | }: true | ||
``` | ||
expr ::= conditional | ||
| logical | ||
| bitwise | ||
| relational | ||
| arithmentical | ||
| function-call | ||
| attribute-ref | ||
| query-parameter | ||
| literal | ||
| parenthesis | ||
| sub-query | ||
conditional ::= expr "?" expr ":" expr | ||
| expr "?:" expr | ||
logical ::= expr ("&&" | "||") expr | ||
| "!" expr | ||
bitwise ::= expr ("&" | "|" | "<<" | ">>") expr | ||
| "~" expr | ||
relational ::= expr ("==" | "!=" | "<=" | ">=" | "<" | ">" | "=~" | "!~") expr | ||
arithmethical ::= expr ("+" | "-" | "*" | "/" | "%" | "**") expr | ||
function-call ::= id "(" (expr ("," expr)*)? ")" | ||
attribute-ref ::= "@" (id | string) | ||
query-parameter ::= "{" id "}" | ||
id ::= /[a-zA-Z_][a-zA-Z0-9_-]*/ | ||
literal ::= string | regexp | number | value | ||
string ::= /"(\\"|.)*"/ | /'(\\'|.)*'/ | ||
regexp ::= /`(\\`|.)*`/ | ||
number ::= /\d+(\.\d+)?$/ | ||
value ::= "true" | "false" | "null" | "NaN" | "undefined" | ||
parenthesis ::= "(" expr ")" | ||
sub-query ::= path // <-- ESSENTIAL RECURSION !! | ||
Expression Language | ||
------------------- | ||
Application Programming Interface (API) | ||
--------------------------------------- | ||
The following BNF-style grammar shows the supported expression language: | ||
### AEL API | ||
``` | ||
// top-level | ||
expr ::= conditional | ||
| logical | ||
| bitwise | ||
| relational | ||
| arithmentical | ||
| functional | ||
| selective | ||
| variable | ||
| literal | ||
| parenthesis | ||
- `new AEL(): AEL`:<br/> | ||
Create a new AEL instance. | ||
// expressions | ||
conditional ::= expr "?" expr ":" expr | ||
| expr "?:" expr | ||
logical ::= expr ("&&" | "||") expr | ||
| "!" expr | ||
bitwise ::= expr ("&" | "^" | "|" | "<<" | ">>") expr | ||
| "~" expr | ||
relational ::= expr ("==" | "!=" | "<=" | ">=" | "<" | ">" | "=~" | "!~") expr | ||
arithmethical ::= expr ("+" | "-" | "*" | "/" | "%" | "**") expr | ||
functional ::= expr "?."? "(" (expr ("," expr)*)? ")" | ||
selective ::= expr "?."? "." ud | ||
| expr "?."? "[" expr "]" | ||
variable ::= id | ||
literal ::= array | object | string | regexp | number | value | ||
parenthesis ::= "(" expr ")" | ||
- `AEL#cache(num: Number): AEL`:<br/> | ||
Set the upper limit for the internal query cache to `num`, i.e., | ||
up to `num` ASTs of parsed queries will be cached. Set `num` to | ||
`0` to disable the cache at all. Returns the API itself. | ||
// literals | ||
id ::= /[a-zA-Z_][a-zA-Z0-9_-]*/ | ||
array ::= "[" (expr ("," expr)*)? "]" | ||
object ::= "{" (key ":" expr ("," key ":" expr)*)? "}" | ||
key ::= "[" expr "]" | ||
| id | ||
string ::= /"(\\"|.)*"/ | ||
| /'(\\'|.)*'/ | ||
regexp ::= /`(\\`|.)*`/ | ||
number ::= /[+-]?/ number-value | ||
number-value ::= "0b" /[01]+/ | ||
| "0o" /[0-7]+/ | ||
| "0x" /[0-9a-fA-F]+/ | ||
| /[0-9]*\.[0-9]+([eE][+-]?[0-9]+)?/ | ||
| /[0-9]+/ | ||
value ::= "true" | "false" | "null" | "NaN" | "undefined" | ||
``` | ||
- `AEL#compile(selector: String, trace?: Boolean): AELQuery { | ||
Compile `selector` DSL into an internal query object for subsequent | ||
processing by `AEL#execute`. | ||
If `trace` is `true` the compiling is dumped to the console. | ||
Returns the query object. | ||
Application Programming Interface (API) | ||
--------------------------------------- | ||
- `AEL#execute(node: Object, query: AELQuery, params?: Object, trace?: Boolean): Object[]`:<br/> | ||
Execute the previously compiled `query` (see `compile` above) at `node`. | ||
The optional `params` object can provide parameters for the `{name}` query constructs. | ||
If `trace` is `true` the execution is dumped to the console. | ||
Returns an array of zero or more matching AST nodes. | ||
The following TypeScript definition shows the supported Application Programming Interface (API): | ||
- `AEL#evaluate(node: Object, selector: String, params?: Object, trace?: Boolean): Object[]`: <br/> | ||
Just the convenient combination of `compile` and `execute`: | ||
`execute(node, compile(selector, trace), params, trace)`. | ||
Use this as the standard query method except you need more control. | ||
The optional `params` object can provide parameters for the `{name}` query constructs. | ||
If `trace` is `true` the compiling and execution is dumped to the console. | ||
Returns an array of zero or more matching AST nodes. | ||
```ts | ||
declare module "AEL" { | ||
class AEL { | ||
/* create AEL instance */ | ||
public constructor( | ||
options?: { | ||
cache?: number, /* number of LRU-cached ASTs (default: 0) */ | ||
trace?: ( /* optional tracing callback (default: null) */ | ||
msg: string /* tracing message */ | ||
) => void | ||
} | ||
) | ||
Example | ||
------- | ||
/* individual step 1: compile (and cache) expression into AST */ | ||
compile( | ||
expr: string /* expression string */ | ||
): any /* abstract syntax tree */ | ||
``` | ||
$ cat sample.js | ||
FIXME | ||
/* individual step 2: execute AST */ | ||
execute( | ||
ast: any, /* abstract syntax tree */ | ||
vars: object /* expression variables */ | ||
): void | ||
$ node sample.js | ||
FIXME | ||
/* all-in-one step: evaluate (compile and execute) expression */ | ||
evaluate( | ||
expr: string, /* expression string */ | ||
vars: object /* expression variables */ | ||
): any | ||
} | ||
export = AEL | ||
} | ||
``` | ||
@@ -114,0 +196,0 @@ |
const AEL = require("..") | ||
let ael = new AEL() | ||
let ast = ael.compile(`foo.bar.quux()`, true) | ||
let result = ael.execute(ast, { foo: { bar: { quux: () => 42 } } }, true) | ||
console.log(result) | ||
const ael = new AEL({ trace: (msg) => console.log(msg) }) | ||
const expr = `foo.quux =~ /ux$/ && foo.bar.a == 1` | ||
const data = { | ||
foo: { | ||
bar: { a: 1, b: 2, c: 3 }, | ||
baz: [ "a", "b", "c", "d", "e" ], | ||
quux: "quux" | ||
} | ||
} | ||
try { | ||
const result = ael.evaluate(expr, data) | ||
console.log("RESULT", result) | ||
} | ||
catch (ex) { | ||
console.log("ERROR", ex.toString()) | ||
} | ||
@@ -25,12 +25,27 @@ /* | ||
import util from "./ael-util.js" | ||
import AELTrace from "./ael-trace.js" | ||
/* load internal depdendencies */ | ||
import util from "./ael-util.js" | ||
import AELTrace from "./ael-trace.js" | ||
import AELError from "./ael-error.js" | ||
/* the exported class */ | ||
export default class AELEval extends AELTrace { | ||
constructor (vars, trace) { | ||
super() | ||
this.vars = vars | ||
this.trace = trace | ||
constructor (expr, vars, trace) { | ||
super(trace) | ||
this.expr = expr | ||
this.vars = vars | ||
} | ||
/* raise an error */ | ||
error (N, origin, message) { | ||
let pos = N.pos() | ||
return new AELError(message, { | ||
origin: origin, | ||
code: this.expr, | ||
line: pos.line, | ||
column: pos.column | ||
}) | ||
} | ||
/* evaluate an arbitrary node */ | ||
eval (N) { | ||
@@ -49,2 +64,5 @@ switch (N.type()) { | ||
case "Variable": return this.evalVariable(N) | ||
case "LiteralArray": return this.evalLiteralArray(N) | ||
case "LiteralObject": return this.evalLiteralObject(N) | ||
case "LiteralObjectItem": return this.evalLiteralObjectItem(N) | ||
case "LiteralString": return this.evalLiteralString(N) | ||
@@ -55,6 +73,7 @@ case "LiteralRegExp": return this.evalLiteralRegExp(N) | ||
default: | ||
throw new Error("invalid AST node") | ||
throw this.error(N, "eval", "invalid AST node (should not happen)") | ||
} | ||
} | ||
/* evaluate conditional binary operator */ | ||
evalConditionalBinary (N) { | ||
@@ -69,2 +88,3 @@ this.traceBegin(N) | ||
/* evaluate conditional ternary operator */ | ||
evalConditionalTernary (N) { | ||
@@ -81,2 +101,3 @@ this.traceBegin(N) | ||
/* evaluate logical operator */ | ||
evalLogical (N) { | ||
@@ -101,2 +122,3 @@ this.traceBegin(N) | ||
/* evaluate bitwise operator */ | ||
evalBitwise (N) { | ||
@@ -117,2 +139,3 @@ this.traceBegin(N) | ||
/* evaluate relational operator */ | ||
evalRelational (N) { | ||
@@ -124,10 +147,122 @@ this.traceBegin(N) | ||
switch (N.get("op")) { | ||
case "==": result = v1 === v2; break | ||
case "!=": result = v1 !== v2; break | ||
case "<=": result = util.coerce(v1, "number") <= util.coerce(v2, "number"); break | ||
case ">=": result = util.coerce(v1, "number") >= util.coerce(v2, "number"); break | ||
case "<": result = util.coerce(v1, "number") < util.coerce(v2, "number"); break | ||
case ">": result = util.coerce(v1, "number") > util.coerce(v2, "number"); break | ||
case "=~": result = util.coerce(v1, "string").match(util.coerce(v2, "regexp")) !== null; break | ||
case "!~": result = util.coerce(v1, "string").match(util.coerce(v2, "regexp")) === null; break | ||
case "==": | ||
switch (util.typePair(v1, v2)) { | ||
case "array:array": | ||
case "array:object": | ||
case "array:scalar": | ||
case "object:array": | ||
case "object:object": | ||
case "object:scalar": | ||
v1 = util.coerce(v1, "array") | ||
v2 = util.coerce(v2, "array") | ||
result = v1.length === v2.length | ||
&& v1.filter((x) => !v2.includes(x)).length === 0 | ||
&& v2.filter((x) => !v1.includes(x)).length === 0 | ||
break | ||
default: | ||
result = v1 === v2 | ||
} | ||
break | ||
case "!=": | ||
switch (util.typePair(v1, v2)) { | ||
case "array:array": | ||
case "array:object": | ||
case "array:scalar": | ||
case "object:array": | ||
case "object:object": | ||
case "object:scalar": | ||
v1 = util.coerce(v1, "array") | ||
v2 = util.coerce(v2, "array") | ||
result = v1.length !== v2.length | ||
|| v1.filter((x) => !v2.includes(x)).length > 0 | ||
|| v2.filter((x) => !v1.includes(x)).length > 0 | ||
break | ||
default: | ||
result = v1 !== v2 | ||
} | ||
break | ||
case "<=": | ||
switch (util.typePair(v1, v2)) { | ||
case "string:any": | ||
result = v1.localeCompare(util.coerce(v2, "string")) <= 0 | ||
break | ||
case "array:array": | ||
case "array:object": | ||
case "array:scalar": | ||
case "object:array": | ||
case "object:object": | ||
case "object:scalar": | ||
v1 = util.coerce(v1, "array") | ||
v2 = util.coerce(v2, "array") | ||
result = v1.filter((x) => !v2.includes(x)).length === 0 | ||
break | ||
default: | ||
result = util.coerce(v1, "number") <= util.coerce(v2, "number") | ||
} | ||
break | ||
case "<": | ||
switch (util.typePair(v1, v2)) { | ||
case "string:any": | ||
result = v1.localeCompare(util.coerce(v2, "string")) < 0 | ||
break | ||
case "array:array": | ||
case "array:object": | ||
case "array:scalar": | ||
case "object:array": | ||
case "object:object": | ||
case "object:scalar": | ||
v1 = util.coerce(v1, "array") | ||
v2 = util.coerce(v2, "array") | ||
result = v1.filter((x) => !v2.includes(x)).length === 0 | ||
&& v2.filter((x) => !v1.includes(x)).length > 0 | ||
break | ||
default: | ||
result = util.coerce(v1, "number") < util.coerce(v2, "number") | ||
} | ||
break | ||
case ">=": | ||
switch (util.typePair(v1, v2)) { | ||
case "string:any": | ||
result = v1.localeCompare(util.coerce(v2, "string")) >= 0 | ||
break | ||
case "array:array": | ||
case "array:object": | ||
case "array:scalar": | ||
case "object:array": | ||
case "object:object": | ||
case "object:scalar": | ||
v1 = util.coerce(v1, "array") | ||
v2 = util.coerce(v2, "array") | ||
result = v2.filter((x) => !v1.includes(x)).length === 0 | ||
break | ||
default: | ||
result = util.coerce(v1, "number") >= util.coerce(v2, "number") | ||
} | ||
break | ||
case ">": | ||
switch (util.typePair(v1, v2)) { | ||
case "string:any": | ||
result = v1.localeCompare(util.coerce(v2, "string")) > 0 | ||
break | ||
case "array:array": | ||
case "array:object": | ||
case "array:scalar": | ||
case "object:array": | ||
case "object:object": | ||
case "object:scalar": | ||
v1 = util.coerce(v1, "array") | ||
v2 = util.coerce(v2, "array") | ||
result = v2.filter((x) => !v1.includes(x)).length === 0 | ||
&& v1.filter((x) => !v2.includes(x)).length > 0 | ||
break | ||
default: | ||
result = util.coerce(v1, "number") > util.coerce(v2, "number") | ||
} | ||
break | ||
case "=~": | ||
result = util.coerce(v1, "string").match(util.coerce(v2, "regexp")) !== null | ||
break | ||
case "!~": | ||
result = util.coerce(v1, "string").match(util.coerce(v2, "regexp")) === null | ||
break | ||
} | ||
@@ -138,2 +273,3 @@ this.traceEnd(N, result) | ||
/* evaluate arithmetical operator */ | ||
evalArithmetical (N) { | ||
@@ -146,10 +282,99 @@ this.traceBegin(N) | ||
case "+": | ||
if (typeof v1 === "string") | ||
result = v1 + util.coerce(v2, "string") | ||
else | ||
result = util.coerce(v1, "number") + util.coerce(v2, "number") | ||
switch (util.typePair(v1, v2)) { | ||
case "string:any": | ||
result = v1 + util.coerce(v2, "string") | ||
break | ||
case "array:array": | ||
result = v1.concat(v2) | ||
break | ||
case "array:object": | ||
result = v1.concat(Object.keys(v2).filter((x) => util.truthy(x))) | ||
break | ||
case "array:scalar": | ||
result = v1.concat([ v2 ]) | ||
break | ||
case "object:array": | ||
result = { ...v1, ...v2.reduce((obj, key) => { obj[key] = true; return obj }, {}) } | ||
break | ||
case "object:object": | ||
result = { ...v1, ...v2 } | ||
break | ||
case "object:scalar": | ||
result = { ...v1, [util.coerce(v2, "string")]: true } | ||
break | ||
default: | ||
result = util.coerce(v1, "number") + util.coerce(v2, "number") | ||
} | ||
break | ||
case "-": result = util.coerce(v1, "number") + util.coerce(v2, "number"); break | ||
case "-": | ||
switch (util.typePair(v1, v2)) { | ||
case "string:any": { | ||
let i = v1.indexOf(v2) | ||
result = i >= 0 ? v1.splice(i, v2.length) : v1 | ||
break | ||
} | ||
case "array:array": | ||
result = v1.filter((x) => !v2.includes(x)) | ||
break | ||
case "array:object": | ||
result = v1.filter((x) => !util.truthy(v2[x])) | ||
break | ||
case "array:scalar": | ||
result = v1.filter((x) => x !== v2) | ||
break | ||
case "object:array": | ||
result = Object.keys(v1) | ||
.filter((key) => !v2.includes(key)) | ||
.reduce((obj, key) => { obj[key] = v1[key]; return obj }, {}) | ||
break | ||
case "object:object": | ||
result = Object.keys(v1) | ||
.filter((key) => !util.truthy(v2[key])) | ||
.reduce((obj, key) => { obj[key] = v1[key]; return obj }, {}) | ||
break | ||
case "object:scalar": | ||
result = Object.keys(v1) | ||
.filter((key) => key !== v2) | ||
.reduce((obj, key) => { obj[key] = v1[key]; return obj }, {}) | ||
break | ||
default: | ||
result = util.coerce(v1, "number") - util.coerce(v2, "number") | ||
} | ||
break | ||
case "/": | ||
switch (util.typePair(v1, v2)) { | ||
case "string:any": { | ||
let i = v1.indexOf(v2) | ||
result = i >= 0 ? v2 : "" | ||
break | ||
} | ||
case "array:array": | ||
result = v1.filter((x) => v2.includes(x)) | ||
break | ||
case "array:object": | ||
result = v1.filter((x) => util.truthy(v2[x])) | ||
break | ||
case "array:scalar": | ||
result = v1.filter((x) => x === v2) | ||
break | ||
case "object:array": | ||
result = Object.keys(v1) | ||
.filter((key) => v2.includes(key)) | ||
.reduce((obj, key) => { obj[key] = v1[key]; return obj }, {}) | ||
break | ||
case "object:object": | ||
result = Object.keys(v1) | ||
.filter((key) => util.truthy(v2[key])) | ||
.reduce((obj, key) => { obj[key] = v1[key]; return obj }, {}) | ||
break | ||
case "object:scalar": | ||
result = Object.keys(v1) | ||
.filter((key) => key === v2) | ||
.reduce((obj, key) => { obj[key] = v1[key]; return obj }, {}) | ||
break | ||
default: | ||
result = util.coerce(v1, "number") / util.coerce(v2, "number") | ||
} | ||
break | ||
case "*": result = util.coerce(v1, "number") * util.coerce(v2, "number"); break | ||
case "/": result = util.coerce(v1, "number") / util.coerce(v2, "number"); break | ||
case "%": result = util.coerce(v1, "number") % util.coerce(v2, "number"); break | ||
@@ -162,2 +387,3 @@ case "**": result = Math.pow(util.coerce(v1, "number"), util.coerce(v2, "number")); break | ||
/* evaluate unary operator */ | ||
evalUnary (N) { | ||
@@ -175,45 +401,47 @@ this.traceBegin(N) | ||
evalSelect (N) { | ||
/* evaluate selection */ | ||
evalSelect (N, provideParent = false) { | ||
this.traceBegin(N) | ||
let parent | ||
let result = this.eval(N.child(0)) | ||
for (const child of N.childs(1)) { | ||
if (typeof result !== "object") | ||
throw new Error("selector base object does not evaluate into an object") | ||
const selector = this.eval(child) | ||
const optional = child.get("optional") ?? false | ||
if ((result === null || result === undefined) && optional) { | ||
result = undefined | ||
break | ||
} | ||
if (result === null || typeof result !== "object") | ||
throw this.error(child, "evalSelect", "selector base object does not evaluate into a non-null object") | ||
const selector = this.eval(child.child(0)) | ||
const key = util.coerce(selector, "string") | ||
parent = result | ||
result = result[key] | ||
} | ||
this.traceEnd(N, result) | ||
return result | ||
return provideParent ? [ parent, result ] : result | ||
} | ||
/* evaluate function call */ | ||
evalFuncCall (N) { | ||
this.traceBegin(N) | ||
let S = N.child(0) | ||
this.traceBegin(S) | ||
let ctx = null | ||
let ctx | ||
let fn = null | ||
if (S.type() === "Variable") | ||
if (S.type() === "Select") | ||
[ ctx, fn ] = this.evalSelect(S, true) | ||
else | ||
fn = this.eval(S) | ||
else if (S.type() === "Select") { | ||
fn = this.eval(S.child(0)) | ||
for (const child of S.childs(1)) { | ||
if (typeof fn !== "object") | ||
throw new Error("selector base object does not evaluate into an object") | ||
const selector = this.eval(child) | ||
const key = util.coerce(selector, "string") | ||
ctx = fn | ||
fn = fn[key] | ||
} | ||
const optional = N.get("optional") ?? false | ||
let result | ||
if ((fn === null || fn === undefined) && optional) | ||
result = undefined | ||
else { | ||
if (typeof fn !== "function") | ||
throw this.error(S, "evalFuncCall", "object does not evaluate into a function") | ||
let args = [] | ||
N.childs().forEach((child) => { | ||
args.push(this.eval(child)) | ||
}) | ||
result = fn.apply(ctx, args) | ||
} | ||
this.traceEnd(S, fn) | ||
if (typeof fn !== "function") | ||
throw new Error("selector tail object does not evaluate into a function") | ||
let args = [] | ||
N.childs().forEach((child) => { | ||
args.push(this.eval(child)) | ||
}) | ||
let result = fn.apply(ctx, args) | ||
this.traceEnd(N, result) | ||
@@ -223,2 +451,3 @@ return result | ||
/* evaluate identifier */ | ||
evalIdentifier (N) { | ||
@@ -231,2 +460,3 @@ this.traceBegin(N) | ||
/* evaluate variable */ | ||
evalVariable (N) { | ||
@@ -236,3 +466,3 @@ this.traceBegin(N) | ||
if (typeof this.vars[id] === "undefined") | ||
throw new Error("invalid variable reference \"" + id + "\"") | ||
throw this.error(N, "evalVariable", "invalid variable reference") | ||
let result = this.vars[id] | ||
@@ -243,2 +473,35 @@ this.traceEnd(N, result) | ||
/* evaluate array literal */ | ||
evalLiteralArray (N) { | ||
this.traceBegin(N) | ||
let result = [] | ||
for (const child of N.childs()) | ||
result.push(this.eval(child)) | ||
this.traceEnd(N, result) | ||
return result | ||
} | ||
/* evaluate object literal */ | ||
evalLiteralObject (N) { | ||
this.traceBegin(N) | ||
let result = {} | ||
for (const child of N.childs()) { | ||
const sub = this.eval(child) | ||
result = { ...result, ...sub } | ||
} | ||
this.traceEnd(N, result) | ||
return result | ||
} | ||
/* evaluate object item */ | ||
evalLiteralObjectItem (N) { | ||
this.traceBegin(N) | ||
const key = this.eval(N.child(0)) | ||
const val = this.eval(N.child(1)) | ||
const result = { [key]: val } | ||
this.traceEnd(N, result) | ||
return result | ||
} | ||
/* evaluate string literal */ | ||
evalLiteralString (N) { | ||
@@ -251,2 +514,3 @@ this.traceBegin(N) | ||
/* evaluate regular expression literal */ | ||
evalLiteralRegExp (N) { | ||
@@ -259,2 +523,3 @@ this.traceBegin(N) | ||
/* evaluate number literal */ | ||
evalLiteralNumber (N) { | ||
@@ -267,2 +532,3 @@ this.traceBegin(N) | ||
/* evaluate special value literal */ | ||
evalLiteralValue (N) { | ||
@@ -269,0 +535,0 @@ this.traceBegin(N) |
@@ -25,7 +25,11 @@ /* | ||
/* eslint no-console: 0 */ | ||
/* load internal depdendencies */ | ||
import util from "./ael-util.js" | ||
/* the exported class */ | ||
export default class AELTrace { | ||
constructor (trace = null) { | ||
this.trace = trace | ||
} | ||
/* determine output prefix based on tree depth */ | ||
@@ -42,6 +46,6 @@ prefixOf (N) { | ||
traceBegin (N) { | ||
if (!this.trace) | ||
if (this.trace === null) | ||
return | ||
let prefix = this.prefixOf(N) | ||
console.log("AEL: execute: | " + prefix + N.type() + " {") | ||
this.trace("AEL: execute: | " + prefix + N.type() + " {") | ||
} | ||
@@ -51,3 +55,3 @@ | ||
traceEnd (N, val) { | ||
if (!this.trace) | ||
if (this.trace === null) | ||
return | ||
@@ -64,5 +68,5 @@ let prefix = this.prefixOf(N) | ||
result = result.substr(0, 40) + "..." | ||
console.log("AEL: execute: | " + prefix + "}: " + result) | ||
this.trace("AEL: execute: | " + prefix + "}: " + result) | ||
} | ||
} | ||
@@ -57,2 +57,4 @@ /* | ||
result = value.length > 0 | ||
else | ||
result = Object.keys(value).length > 0 | ||
} | ||
@@ -88,2 +90,14 @@ break | ||
break | ||
case "array": | ||
if (typeof value === "object" && !(value instanceof Array) && value !== null) | ||
value = Object.keys(value) | ||
else if (!(typeof value === "object" && value instanceof Array)) | ||
value = [ String(value) ] | ||
break | ||
case "object": | ||
if (typeof value === "object" && value instanceof Array) | ||
value = value.reduce((obj, el) => { obj[el] = true; return obj }, {}) | ||
else if (!(typeof value === "object")) | ||
value = { [String(value)]: true } | ||
break | ||
} | ||
@@ -97,2 +111,29 @@ } | ||
} | ||
/* determine type pair */ | ||
static typePair (v1, v2) { | ||
if (typeof v1 === "string") | ||
return "string:any" | ||
else if (typeof v1 === "object" && v1 !== null) { | ||
if (v1 instanceof Array) { | ||
if (typeof v2 === "object" && v2 instanceof Array && v2 !== null) | ||
return "array:array" | ||
else if (typeof v2 === "object" && !(v2 instanceof Array) && v2 !== null) | ||
return "array:object" | ||
else | ||
return "array:scalar" | ||
} | ||
else { | ||
if (typeof v2 === "object" && v2 instanceof Array && v2 !== null) | ||
return "object:array" | ||
else if (typeof v2 === "object" && !(v2 instanceof Array) && v2 !== null) | ||
return "object:object" | ||
else | ||
return "object:scalar" | ||
} | ||
} | ||
else | ||
return "any:any" | ||
} | ||
} | ||
@@ -32,2 +32,3 @@ /* | ||
import AELEval from "./ael-eval.js" | ||
import AELError from "./ael-error.js" | ||
@@ -45,17 +46,18 @@ /* get expression parser (by loading and on-the-fly compiling PEG.js grammar) */ | ||
/* create a new AEL instance */ | ||
constructor () { | ||
constructor (options = {}) { | ||
/* provide parameter defaults */ | ||
this.options = { | ||
cache: 100, | ||
trace: null, | ||
...options | ||
} | ||
/* create LRU cache */ | ||
this._cache = new CacheLRU() | ||
this._cache.limit(this.options.cache) | ||
} | ||
/* configure the LRU cache limit */ | ||
cache (entries) { | ||
if (arguments.length !== 1) | ||
throw new Error("AEL#cache: invalid number of arguments") | ||
this._cache.limit(entries) | ||
return this | ||
} | ||
/* individual step 1: compile expression into AST */ | ||
compile (expr, trace) { | ||
compile (expr) { | ||
/* sanity check usage */ | ||
if (arguments.length < 1) | ||
@@ -65,10 +67,13 @@ throw new Error("AEL#compile: too less arguments") | ||
throw new Error("AEL#compile: too many arguments") | ||
if (trace === undefined) | ||
trace = false | ||
/* tracing operation */ | ||
if (this.options.trace !== null) | ||
this.options.trace("AEL: compile: +---(expression string)-----------------" + | ||
"----------------------------------------------------------------\n" + | ||
expr.replace(/\n$/, "").replace(/^/mg, "AEL: compile: | ")) | ||
/* try to fetch pre-compiled AST */ | ||
let ast = this._cache.get(expr) | ||
if (ast === undefined) { | ||
if (trace) | ||
console.log("AEL: compile: +---(expression)------------------------" + | ||
"----------------------------------------------------------------\n" + | ||
expr.replace(/\n$/, "").replace(/^/mg, "AEL: compile: | ")) | ||
/* compile AST from scratch */ | ||
const asty = new ASTY() | ||
@@ -81,12 +86,26 @@ let result = PEGUtil.parse(AELParser, expr, { | ||
}) | ||
if (result.error !== null) | ||
throw new Error("AEL: compile: expression parsing failed:\n" + | ||
PEGUtil.errorMessage(result.error, true).replace(/^/mg, "ERROR: ")) | ||
if (result.error !== null) { | ||
const message = "parsing failed: " + | ||
`"${result.error.location.prolog}" "${result.error.location.token}" "${result.error.location.epilog}": ` + | ||
result.error.message | ||
throw new AELError(message, { | ||
origin: "parser", | ||
code: expr, | ||
line: result.error.line, | ||
column: result.error.column | ||
}) | ||
} | ||
ast = result.ast | ||
if (trace) | ||
console.log("AEL: compile: +---(AST)-------------------------------" + | ||
"----------------------------------------------------------------\n" + | ||
ast.dump().replace(/\n$/, "").replace(/^/mg, "AEL: compile: | ")) | ||
ast.set("expr", expr) | ||
/* cache AST for subsequent usages */ | ||
this._cache.set(expr, ast) | ||
} | ||
/* tracing operation */ | ||
if (this.options.trace !== null) | ||
this.options.trace("AEL: compile: +---(abstract syntax tree)--------------" + | ||
"----------------------------------------------------------------\n" + | ||
ast.dump().replace(/\n$/, "").replace(/^/mg, "AEL: compile: | ")) | ||
return ast | ||
@@ -96,3 +115,4 @@ } | ||
/* individual step 2: execute AST */ | ||
execute (ast, vars, trace) { | ||
execute (ast, vars) { | ||
/* sanity check usage */ | ||
if (arguments.length < 1) | ||
@@ -102,16 +122,21 @@ throw new Error("AEL#execute: too less arguments") | ||
throw new Error("AEL#execute: too many arguments") | ||
/* provide defaults */ | ||
if (vars === undefined) | ||
vars = {} | ||
if (trace === undefined) | ||
trace = false | ||
if (trace) | ||
console.log("AEL: execute: +---(result)----------------------------" + | ||
/* tracing operation */ | ||
if (this.options.trace !== null) | ||
this.options.trace("AEL: execute: +---(evaluation recursion tree)---------" + | ||
"----------------------------------------------------------------") | ||
const evaluator = new AELEval(vars, trace) | ||
const result = evaluator.eval(ast) | ||
return result | ||
/* evaluate the AST */ | ||
const expr = ast.get("expr") | ||
const evaluator = new AELEval(expr, vars, this.options.trace) | ||
return evaluator.eval(ast) | ||
} | ||
/* all-in-one step */ | ||
evaluate (expr, vars, trace) { | ||
evaluate (expr, vars) { | ||
/* sanity check usage */ | ||
if (arguments.length < 1) | ||
@@ -121,8 +146,10 @@ throw new Error("AEL#evaluate: too less arguments") | ||
throw new Error("AEL#evaluate: too many arguments") | ||
/* provide defaults */ | ||
if (vars === undefined) | ||
vars = {} | ||
if (trace === undefined) | ||
trace = false | ||
const ast = this.compile(expr, trace) | ||
return this.execute(ast, vars, trace) | ||
/* compile and evaluate expression */ | ||
const ast = this.compile(expr) | ||
return this.execute(ast, vars) | ||
} | ||
@@ -129,0 +156,0 @@ } |
@@ -38,5 +38,3 @@ /* | ||
const ael = new AEL() | ||
it("API availability", () => { | ||
expect(ael).to.respondTo("cache") | ||
expect(ael).to.respondTo("compile") | ||
@@ -46,4 +44,3 @@ expect(ael).to.respondTo("execute") | ||
}) | ||
it("simple expressions", () => { | ||
it("literal expressions", () => { | ||
expect(ael.evaluate("true")).to.be.equal(true) | ||
@@ -53,9 +50,10 @@ expect(ael.evaluate("42")).to.be.equal(42) | ||
}) | ||
it("variable expressions", () => { | ||
expect(ael.evaluate("foo + bar", { foo: "foo", bar: "bar" })).to.be.equal("foobar") | ||
it("conditional expressions", () => { | ||
expect(ael.evaluate("a > 0 ? b : c", { a: 1, b: 2, c: 3 })).to.be.equal(2) | ||
expect(ael.evaluate("a > 0 ? b > 1 ? true : false : c", { a: 1, b: 2, c: 3 })).to.be.equal(true) | ||
}) | ||
it("function calls", () => { | ||
expect(ael.evaluate("foo() + bar.baz()", { foo: () => 42, bar: { baz: () => 7 } }), true).to.be.equal(49) | ||
it("boolean expressions", () => { | ||
expect(ael.evaluate("a && b || c && d", { a: true, b: true, c: false, d: false })).to.be.equal(true) | ||
}) | ||
}) | ||
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
High entropy strings
Supply chain riskContains high entropy strings. This could be a sign of encrypted data, leaked secrets or obfuscated code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
395033
17
7110
235
6
12
+ Addedsource-code-error@1.0.2
+ Added@babel/code-frame@7.12.11(transitive)
+ Added@babel/helper-validator-identifier@7.25.9(transitive)
+ Added@babel/highlight@7.25.9(transitive)
+ Addedansi-regex@5.0.1(transitive)
+ Addedansi-styles@3.2.14.3.0(transitive)
+ Addedchalk@2.4.24.1.0(transitive)
+ Addedcolor-convert@1.9.32.0.1(transitive)
+ Addedcolor-name@1.1.31.1.4(transitive)
+ Addedescape-string-regexp@1.0.5(transitive)
+ Addedhas-flag@3.0.04.0.0(transitive)
+ Addedjs-tokens@4.0.0(transitive)
+ Addedpicocolors@1.1.1(transitive)
+ Addedsource-code-error@1.0.2(transitive)
+ Addedstrip-ansi@6.0.0(transitive)
+ Addedsupports-color@5.5.07.2.0(transitive)