Skip to content

Commit 2d8e1eb

Browse files
authored
Merge branch 'json-logic:master' into master
2 parents b4b3bc7 + f9f565c commit 2d8e1eb

16 files changed

+1045
-47
lines changed

asLogic.test.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,14 @@ describe('asLogicAsync', () => {
7070

7171
it('Should not allow you to create a builder with methods that do not exist', async () => {
7272
const builder = asLogicAsync(module)
73-
expect(builder({
74-
hello: { '+': [1, 2] }
75-
})).rejects.toThrow()
73+
try {
74+
await builder({
75+
hello: { '+': [1, 2] }
76+
})
77+
} catch (e) {
78+
return
79+
}
80+
throw new Error('Should have thrown an error')
7681
})
7782

7883
it('Should let you select logic kept', async () => {

asyncLogic.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,13 @@ class AsyncLogicEngine {
6969
* @param {*} above The context above (can be used for handlebars-style data traversal.)
7070
* @returns {Promise<*>}
7171
*/
72-
async _parse (logic, context, above) {
73-
const [func] = Object.keys(logic)
72+
async _parse (logic, context, above, func, length) {
7473
const data = logic[func]
7574

7675
if (this.isData(logic, func)) return logic
77-
if (!this.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
76+
77+
// eslint-disable-next-line no-throw-literal
78+
if (!this.methods[func] || length > 1) throw { type: 'Unknown Operator', key: func }
7879

7980
// A small but useful micro-optimization for some of the most common functions.
8081
// Later on, I could define something to shut this off if var / val are redefined.
@@ -182,7 +183,13 @@ class AsyncLogicEngine {
182183
return res
183184
}
184185

185-
if (logic && typeof logic === 'object' && Object.keys(logic).length > 0) return this._parse(logic, data, above)
186+
if (logic && typeof logic === 'object' && Object.keys(logic).length > 0) {
187+
const keys = Object.keys(logic)
188+
if (keys.length > 0) {
189+
const func = keys[0]
190+
return this._parse(logic, data, above, func, keys.length)
191+
}
192+
}
186193

187194
return logic
188195
}

async_optimizer.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,14 @@ export function optimize (logic, engine, above = []) {
8585
const keys = Object.keys(logic)
8686
const methodName = keys[0]
8787

88+
if (keys.length === 0) return logic
89+
8890
const isData = engine.isData(logic, methodName)
8991
if (isData) return () => logic
9092

93+
// eslint-disable-next-line no-throw-literal
94+
if (keys.length > 1) throw { type: 'Unknown Operator' }
95+
9196
// If we have a deterministic function, we can just return the result of the evaluation,
9297
// basically inlining the operation.
9398
const deterministic = !engine.disableInline && isDeterministic(logic, engine, { engine })
@@ -113,6 +118,9 @@ export function optimize (logic, engine, above = []) {
113118
}
114119
return result
115120
}
121+
122+
// eslint-disable-next-line no-throw-literal
123+
throw { type: 'Unknown Operator', key: methodName }
116124
}
117125

118126
return logic

compiler.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ export function isDeterministic (method, engine, buildState) {
8686

8787
if (engine.isData(method, func)) return true
8888
if (lower === undefined) return true
89-
if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
89+
90+
// eslint-disable-next-line no-throw-literal
91+
if (!engine.methods[func]) throw { type: 'Unknown Operator', key: func }
9092

9193
if (engine.methods[func].lazy) {
9294
return typeof engine.methods[func].deterministic === 'function'
@@ -169,14 +171,16 @@ function buildString (method, buildState = {}) {
169171
return result
170172
}
171173

172-
const func = method && Object.keys(method)[0]
173-
174174
if (method && typeof method === 'object') {
175+
const keys = Object.keys(method)
176+
const func = keys[0]
177+
175178
if (!func) return pushValue(method)
176-
if (!engine.methods[func]) {
179+
if (!engine.methods[func] || keys.length > 1) {
177180
// Check if this is supposed to be "data" rather than a function.
178181
if (engine.isData(method, func)) return pushValue(method, true)
179-
throw new Error(`Method '${func}' was not found in the Logic Engine.`)
182+
// eslint-disable-next-line no-throw-literal
183+
throw { type: 'Unknown Operator', key: func }
180184
}
181185

182186
if (

defaultMethods.js

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ function isDeterministic (method, engine, buildState) {
2020
const lower = method[func]
2121

2222
if (engine.isData(method, func) || func === undefined) return true
23-
if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
23+
// eslint-disable-next-line no-throw-literal
24+
if (!engine.methods[func]) throw { type: 'Unknown Operator', key: func }
2425

2526
if (engine.methods[func].lazy) {
2627
return typeof engine.methods[func].deterministic === 'function'
@@ -45,7 +46,8 @@ function isSyncDeep (method, engine, buildState) {
4546
const func = Object.keys(method)[0]
4647
const lower = method[func]
4748
if (engine.isData(method, func) || func === undefined) return true
48-
if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
49+
// eslint-disable-next-line no-throw-literal
50+
if (!engine.methods[func]) throw { type: 'Unknown Operator', key: func }
4951
if (engine.methods[func].lazy) return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync]
5052
return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync] && isSyncDeep(lower, engine, buildState)
5153
}
@@ -147,15 +149,32 @@ const defaultMethods = {
147149
// eslint-disable-next-line no-throw-literal
148150
throw { type }
149151
},
150-
max: (data) => Math.max(...data),
151-
min: (data) => Math.min(...data),
152+
max: (data) => {
153+
if (!data.length || typeof data[0] !== 'number') throw INVALID_ARGUMENTS
154+
let max = data[0]
155+
for (let i = 1; i < data.length; i++) {
156+
if (typeof data[i] !== 'number') throw INVALID_ARGUMENTS
157+
if (data[i] > max) max = data[i]
158+
}
159+
return max
160+
},
161+
min: (data) => {
162+
if (!data.length || typeof data[0] !== 'number') throw INVALID_ARGUMENTS
163+
let min = data[0]
164+
for (let i = 1; i < data.length; i++) {
165+
if (typeof data[i] !== 'number') throw INVALID_ARGUMENTS
166+
if (data[i] < min) min = data[i]
167+
}
168+
return min
169+
},
152170
in: ([item, array]) => (array || []).includes(item),
153171
preserve: {
154172
lazy: true,
155173
method: declareSync((i) => i, true),
156174
[Sync]: () => true
157175
},
158176
if: {
177+
[OriginalImpl]: true,
159178
method: (input, context, above, engine) => {
160179
if (!Array.isArray(input)) throw INVALID_ARGUMENTS
161180

@@ -228,7 +247,7 @@ const defaultMethods = {
228247
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
229248
method: (arr, context, above, engine) => {
230249
if (!Array.isArray(arr)) throw INVALID_ARGUMENTS
231-
if (!arr.length) return false
250+
if (!arr.length) return null
232251

233252
let item
234253
for (let i = 0; i < arr.length; i++) {
@@ -240,7 +259,7 @@ const defaultMethods = {
240259
},
241260
asyncMethod: async (arr, _1, _2, engine) => {
242261
if (!Array.isArray(arr)) throw INVALID_ARGUMENTS
243-
if (!arr.length) return false
262+
if (!arr.length) return null
244263

245264
let item
246265
for (let i = 0; i < arr.length; i++) {
@@ -254,7 +273,7 @@ const defaultMethods = {
254273
compile: (data, buildState) => {
255274
let res = buildState.compile``
256275
if (Array.isArray(data)) {
257-
if (!data.length) return buildState.compile`false`
276+
if (!data.length) return buildState.compile`null`
258277
for (let i = 0; i < data.length; i++) res = buildState.compile`${res} engine.truthy(prev = ${data[i]}) ? prev : `
259278
res = buildState.compile`${res} prev`
260279
return res
@@ -386,7 +405,7 @@ const defaultMethods = {
386405
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
387406
method: (arr, context, above, engine) => {
388407
if (!Array.isArray(arr)) throw INVALID_ARGUMENTS
389-
if (!arr.length) return false
408+
if (!arr.length) return null
390409

391410
let item
392411
for (let i = 0; i < arr.length; i++) {
@@ -397,7 +416,7 @@ const defaultMethods = {
397416
},
398417
asyncMethod: async (arr, _1, _2, engine) => {
399418
if (!Array.isArray(arr)) throw INVALID_ARGUMENTS
400-
if (!arr.length) return false
419+
if (!arr.length) return null
401420
let item
402421
for (let i = 0; i < arr.length; i++) {
403422
item = await engine.run(arr[i], _1, { above: _2 })
@@ -410,7 +429,7 @@ const defaultMethods = {
410429
compile: (data, buildState) => {
411430
let res = buildState.compile``
412431
if (Array.isArray(data)) {
413-
if (!data.length) return buildState.compile`false`
432+
if (!data.length) return buildState.compile`null`
414433
for (let i = 0; i < data.length; i++) res = buildState.compile`${res} !engine.truthy(prev = ${data[i]}) ? prev : `
415434
res = buildState.compile`${res} prev`
416435
return res
@@ -866,6 +885,7 @@ function createArrayIterativeMethod (name, useTruthy = false) {
866885
})
867886
)
868887
},
888+
[OriginalImpl]: true,
869889
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
870890
method: (input, context, above, engine) => {
871891
if (!Array.isArray(input)) throw INVALID_ARGUMENTS
@@ -933,21 +953,6 @@ Object.keys(defaultMethods).forEach((item) => {
933953
: defaultMethods[item].deterministic
934954
})
935955

936-
// @ts-ignore Allow custom attribute
937-
defaultMethods.min.compile = function (data, buildState) {
938-
if (!Array.isArray(data)) return false
939-
return `Math.min(${data
940-
.map((i) => buildString(i, buildState))
941-
.join(', ')})`
942-
}
943-
// @ts-ignore Allow custom attribute
944-
defaultMethods.max.compile = function (data, buildState) {
945-
if (!Array.isArray(data)) return false
946-
return `Math.max(${data
947-
.map((i) => buildString(i, buildState))
948-
.join(', ')})`
949-
}
950-
951956
// @ts-ignore Allow custom attribute
952957
defaultMethods.if.compile = function (data, buildState) {
953958
if (!Array.isArray(data)) return false

logic.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,13 @@ class LogicEngine {
6060
* @param {*} above The context above (can be used for handlebars-style data traversal.)
6161
* @returns {{ result: *, func: string }}
6262
*/
63-
_parse (logic, context, above, func) {
63+
_parse (logic, context, above, func, length) {
6464
const data = logic[func]
6565

6666
if (this.isData(logic, func)) return logic
6767

68-
if (!this.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
68+
// eslint-disable-next-line no-throw-literal
69+
if (!this.methods[func] || length > 1) throw { type: 'Unknown Operator', key: func }
6970

7071
// A small but useful micro-optimization for some of the most common functions.
7172
// Later on, I could define something to shut this off if var / val are redefined.
@@ -161,7 +162,7 @@ class LogicEngine {
161162
const keys = Object.keys(logic)
162163
if (keys.length > 0) {
163164
const func = keys[0]
164-
return this._parse(logic, data, above, func)
165+
return this._parse(logic, data, above, func, keys.length)
165166
}
166167
}
167168

optimizer.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,27 @@ function checkIdioms (logic, engine, above) {
115115
}
116116
}
117117

118+
if ((logic.if || logic['?:']) && engine.methods.if[OriginalImpl] && Array.isArray(logic.if || logic['?:']) && (logic.if || logic['?:']).length === 3) {
119+
const [condition, truthy, falsy] = logic.if || logic['?:']
120+
const C = optimize(condition, engine, above)
121+
const T = optimize(truthy, engine, above)
122+
const F = optimize(falsy, engine, above)
123+
124+
if (typeof C === 'function' && typeof T === 'function' && typeof F === 'function') return (data, abv) => engine.truthy(C(data, abv)) ? T(data, abv) : F(data, abv)
125+
if (typeof C === 'function' && typeof T === 'function') return (data, abv) => engine.truthy(C(data, abv)) ? T(data, abv) : F
126+
if (typeof C === 'function' && typeof F === 'function') return (data, abv) => engine.truthy(C(data, abv)) ? T : F(data, abv)
127+
if (typeof C === 'function') return (data, abv) => engine.truthy(C(data, abv)) ? T : F
128+
129+
// Otherwise, C is not a function, and we can just return the result of the evaluation.
130+
return engine.truthy(C) ? T : F
131+
}
132+
133+
if (logic.filter && engine.methods.filter[OriginalImpl] && Array.isArray(logic.filter) && logic.filter.length === 2) {
134+
const [collection, filter] = logic.filter
135+
const filterF = optimize(filter, engine, above)
136+
if (typeof filterF !== 'function') return engine.truthy(filterF) ? optimize(collection, engine, above) : []
137+
}
138+
118139
// Hyper-Optimizations for Comparison Operators.
119140
for (const comparison in comparisons) {
120141
if (logic[comparison] && Array.isArray(logic[comparison]) && engine.methods[comparison][OriginalImpl]) {
@@ -195,9 +216,14 @@ export function optimize (logic, engine, above = []) {
195216
const keys = Object.keys(logic)
196217
const methodName = keys[0]
197218

219+
if (keys.length === 0) return logic
220+
198221
const isData = engine.isData(logic, methodName)
199222
if (isData) return () => logic
200223

224+
// eslint-disable-next-line no-throw-literal
225+
if (keys.length > 1) throw { type: 'Unknown Operator' }
226+
201227
// If we have a deterministic function, we can just return the result of the evaluation,
202228
// basically inlining the operation.
203229
const deterministic = !engine.disableInline && isDeterministic(logic, engine, { engine })
@@ -207,6 +233,9 @@ export function optimize (logic, engine, above = []) {
207233
if (deterministic) return result()
208234
return result
209235
}
236+
237+
// eslint-disable-next-line no-throw-literal
238+
throw { type: 'Unknown Operator', key: methodName }
210239
}
211240

212241
return logic

0 commit comments

Comments
 (0)