Skip to content

Commit

Permalink
refactor(core): Framework functions, add transforms to grammar
Browse files Browse the repository at this point in the history
  • Loading branch information
TomFrost committed May 7, 2020
1 parent b053ca3 commit eecff16
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 238 deletions.
2 changes: 1 addition & 1 deletion __tests__/lib/Lexer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright 2020 Tom Shawver
*/

const grammar = require('lib/grammar').elements
const grammar = require('lib/grammar').getGrammar()
const Lexer = require('lib/Lexer')

let inst
Expand Down
33 changes: 19 additions & 14 deletions __tests__/lib/evaluator/Evaluator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
const Lexer = require('lib/Lexer')
const Parser = require('lib/parser/Parser')
const Evaluator = require('lib/evaluator/Evaluator')
const grammar = require('lib/grammar').elements
const grammar = require('lib/grammar').getGrammar()
const PromiseSync = require('lib/PromiseSync')

const lexer = new Lexer(grammar)
Expand All @@ -19,7 +19,7 @@ const toTree = (exp) => {

describe('Evaluator', () => {
it('evaluates using an alternative Promise class', () => {
const e = new Evaluator(grammar, null, null, null, PromiseSync)
const e = new Evaluator(grammar, null, null, PromiseSync)
expect(e.eval(toTree('2 + 2'))).toHaveProperty('value', 4)
})
it('evaluates an arithmetic expression', async () => {
Expand Down Expand Up @@ -48,15 +48,16 @@ describe('Evaluator', () => {
})
it('evaluates an identifier chain', async () => {
const context = { foo: { baz: { bar: 'tek' } } }
const e = new Evaluator(grammar, null, context)
const e = new Evaluator(grammar, context)
return expect(e.eval(toTree('foo.baz.bar'))).resolves.toBe(
context.foo.baz.bar
)
})
it('applys transforms', async () => {
it('applies transforms', async () => {
const context = { foo: 10 }
const half = (val) => val / 2
const e = new Evaluator(grammar, { half: half }, context)
const g = { ...grammar, transforms: { half } }
const e = new Evaluator(g, context)
return expect(e.eval(toTree('foo|half + 3'))).resolves.toBe(8)
})
it('filters arrays', async () => {
Expand All @@ -65,7 +66,7 @@ describe('Evaluator', () => {
bar: [{ tek: 'hello' }, { tek: 'baz' }, { tok: 'baz' }]
}
}
const e = new Evaluator(grammar, null, context)
const e = new Evaluator(grammar, context)
return expect(e.eval(toTree('foo.bar[.tek == "baz"]'))).resolves.toEqual([
{ tek: 'baz' }
])
Expand All @@ -76,7 +77,7 @@ describe('Evaluator', () => {
bar: [{ tek: { hello: 'world' } }, { tek: { hello: 'universe' } }]
}
}
const e = new Evaluator(grammar, null, context)
const e = new Evaluator(grammar, context)
return expect(e.eval(toTree('foo.bar.tek.hello'))).resolves.toBe('world')
})
it('makes array elements addressable by index', async () => {
Expand All @@ -85,12 +86,12 @@ describe('Evaluator', () => {
bar: [{ tek: 'tok' }, { tek: 'baz' }, { tek: 'foz' }]
}
}
const e = new Evaluator(grammar, null, context)
const e = new Evaluator(grammar, context)
return expect(e.eval(toTree('foo.bar[1].tek'))).resolves.toBe('baz')
})
it('allows filters to select object properties', async () => {
const context = { foo: { baz: { bar: 'tek' } } }
const e = new Evaluator(grammar, null, context)
const e = new Evaluator(grammar, context)
return expect(e.eval(toTree('foo["ba" + "z"].bar'))).resolves.toBe(
context.foo.baz.bar
)
Expand All @@ -114,9 +115,13 @@ describe('Evaluator', () => {
return expect(e.eval(toTree('{}'))).resolves.toEqual({})
})
it('evaluates a transform with multiple args', async () => {
const e = new Evaluator(grammar, {
concat: (val, a1, a2, a3) => val + ': ' + a1 + a2 + a3
})
const g = {
...grammar,
transforms: {
concat: (val, a1, a2, a3) => val + ': ' + a1 + a2 + a3
}
}
const e = new Evaluator(g)
return expect(
e.eval(toTree('"foo"|concat("baz", "bar", "tek")'))
).resolves.toBe('foo: bazbartek')
Expand Down Expand Up @@ -167,7 +172,7 @@ describe('Evaluator', () => {
return expect(e.eval(toTree('"".length'))).resolves.toBe(0)
})
it('returns empty array when applying a filter to an undefined value', async () => {
const e = new Evaluator(grammar, null, { a: {}, d: 4 })
const e = new Evaluator(grammar, { a: {}, d: 4 })
return expect(e.eval(toTree('a.b[.c == d]'))).resolves.toHaveLength(0)
})
it('evaluates an expression with arbitrary whitespace', async () => {
Expand All @@ -182,7 +187,7 @@ describe('Evaluator', () => {
$bar: 8
}
const expr = '$+$foo+$foo$bar+$bar'
const e = new Evaluator(grammar, null, context)
const e = new Evaluator(grammar, context)
await expect(e.eval(toTree(expr))).resolves.toBe(26)
})
})
54 changes: 30 additions & 24 deletions __tests__/lib/parser/Parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

const Lexer = require('lib/Lexer')
const Parser = require('lib/parser/Parser')
const grammar = require('lib/grammar').elements
const grammar = require('lib/grammar').getGrammar()

let inst
const lexer = new Lexer(grammar)
Expand Down Expand Up @@ -232,47 +232,53 @@ describe('Parser', () => {
it('applies transforms and arguments', () => {
inst.addTokens(lexer.tokenize('foo|tr1|tr2.baz|tr3({bar:"tek"})'))
expect(inst.complete()).toEqual({
type: 'Transform',
type: 'FunctionCall',
name: 'tr3',
pool: 'transforms',
args: [
{
type: 'Identifier',
value: 'baz',
from: {
type: 'FunctionCall',
name: 'tr2',
pool: 'transforms',
args: [
{
type: 'FunctionCall',
name: 'tr1',
pool: 'transforms',
args: [
{
type: 'Identifier',
value: 'foo'
}
]
}
]
}
},
{
type: 'ObjectLiteral',
value: {
bar: { type: 'Literal', value: 'tek' }
}
}
],
subject: {
type: 'Identifier',
value: 'baz',
from: {
type: 'Transform',
name: 'tr2',
args: [],
subject: {
type: 'Transform',
name: 'tr1',
args: [],
subject: {
type: 'Identifier',
value: 'foo'
}
}
}
}
]
})
})
it('handles multiple arguments in transforms', () => {
inst.addTokens(lexer.tokenize('foo|bar("tek", 5, true)'))
expect(inst.complete()).toEqual({
type: 'Transform',
type: 'FunctionCall',
name: 'bar',
pool: 'transforms',
args: [
{ type: 'Identifier', value: 'foo' },
{ type: 'Literal', value: 'tek' },
{ type: 'Literal', value: 5 },
{ type: 'Literal', value: true }
],
subject: { type: 'Identifier', value: 'foo' }
]
})
})
it('applies filters to identifiers', () => {
Expand Down
12 changes: 5 additions & 7 deletions lib/Expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ const Parser = require('./parser/Parser')
const PromiseSync = require('./PromiseSync')

class Expression {
constructor(lang, exprStr) {
this._lang = lang
this._lexer = new Lexer(lang.grammar)
constructor(grammar, exprStr) {
this._grammar = grammar
this._exprStr = exprStr
this._ast = null
}
Expand All @@ -23,8 +22,8 @@ class Expression {
* @returns {Expression} this Expression instance, for convenience
*/
compile() {
const lexer = new Lexer(this._lang.grammar)
const parser = new Parser(this._lang.grammar)
const lexer = new Lexer(this._grammar)
const parser = new Parser(this._grammar)
const tokens = lexer.tokenize(this._exprStr)
parser.addTokens(tokens)
this._ast = parser.complete()
Expand Down Expand Up @@ -58,8 +57,7 @@ class Expression {
return promise.resolve().then(() => {
const ast = this._getAst()
const evaluator = new Evaluator(
this._lang.grammar,
this._lang.transforms,
this._grammar,
context,
undefined,
promise
Expand Down
45 changes: 17 additions & 28 deletions lib/Jexl.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

const Expression = require('./Expression')
const defaultGrammar = require('./grammar').elements
const { getGrammar } = require('./grammar')

/**
* Jexl is the Javascript Expression Language, capable of parsing and
Expand All @@ -16,9 +16,7 @@ class Jexl {
constructor() {
// Allow expr to be called outside of the jexl context
this.expr = this.expr.bind(this)
this._grammar = Object.assign({}, defaultGrammar)
this._lexer = null
this._transforms = {}
this._grammar = getGrammar()
}

/**
Expand Down Expand Up @@ -77,7 +75,7 @@ class Jexl {
* - {...*} args: The arguments for this transform
*/
addTransform(name, fn) {
this._transforms[name] = fn
this._grammar.transforms[name] = fn
}

/**
Expand All @@ -88,7 +86,7 @@ class Jexl {
addTransforms(map) {
for (let key in map) {
if (map.hasOwnProperty(key)) {
this._transforms[key] = map[key]
this._grammar.transforms[key] = map[key]
}
}
}
Expand All @@ -113,8 +111,7 @@ class Jexl {
* @returns {Expression} The Expression object representing the given string
*/
createExpression(expression) {
const lang = this._getLang()
return new Expression(lang, expression)
return new Expression(this._grammar, expression)
}

/**
Expand All @@ -123,7 +120,7 @@ class Jexl {
* @returns {function} The transform function
*/
getTransform(name) {
return this._transforms[name]
return this._grammar.transforms[name]
}

/**
Expand Down Expand Up @@ -151,6 +148,12 @@ class Jexl {
return exprObj.evalSync(context)
}

/**
* A JavaScript template literal to allow expressions to be defined by the
* syntax: expr`40 + 2`
* @param {Array<string>} strs
* @param {...any} args
*/
expr(strs, ...args) {
const exprStr = strs.reduce((acc, str, idx) => {
const arg = idx < args.length ? args[idx] : ''
Expand All @@ -166,11 +169,11 @@ class Jexl {
*/
removeOp(operator) {
if (
this._grammar[operator] &&
(this._grammar[operator].type === 'binaryOp' ||
this._grammar[operator].type === 'unaryOp')
this._grammar.elements[operator] &&
(this._grammar.elements[operator].type === 'binaryOp' ||
this._grammar.elements[operator].type === 'unaryOp')
) {
delete this._grammar[operator]
delete this._grammar.elements[operator]
}
}

Expand All @@ -182,21 +185,7 @@ class Jexl {
* @private
*/
_addGrammarElement(str, obj) {
this._grammar[str] = obj
}

/**
* Gets an object defining the dynamic language elements of this Jexl
* instance.
* @returns {{ grammar: object, transforms: object }} A language definition
* object
* @private
*/
_getLang() {
return {
grammar: this._grammar,
transforms: this._transforms
}
this._grammar.elements[str] = obj
}
}

Expand Down
6 changes: 3 additions & 3 deletions lib/Lexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ class Lexer {
token.value = parseFloat(element)
} else if (element === 'true' || element === 'false') {
token.value = element === 'true'
} else if (this._grammar[element]) {
token.type = this._grammar[element].type
} else if (this._grammar.elements[element]) {
token.type = this._grammar.elements[element].type
} else if (element.match(identRegex)) {
token.type = 'identifier'
} else {
Expand Down Expand Up @@ -185,7 +185,7 @@ class Lexer {
_getSplitRegex() {
if (!this._splitRegex) {
// Sort by most characters to least, then regex escape each
const elemArray = Object.keys(this._grammar)
const elemArray = Object.keys(this._grammar.elements)
.sort((a, b) => {
return b.length - a.length
})
Expand Down
Loading

0 comments on commit eecff16

Please sign in to comment.