Skip to content

Commit 9f93d94

Browse files
Merge pull request #45 from json-logic/proposal/hard-errors
Create a branch that uses hard exceptions instead
2 parents 46e6e5f + bfe8e75 commit 9f93d94

14 files changed

+262
-185
lines changed

compatible.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ describe('All of the compatible tests', () => {
5656
if (Array.isArray(result)) result = result.map(i => (i || 0).toNumber ? Number(i) : i)
5757
expect(correction(result)).toStrictEqual(testCase.result)
5858
} catch (err) {
59+
if (err.message && err.message.includes('expect')) throw err
5960
expect(testCase.error).toStrictEqual(true)
6061
}
6162
})
@@ -70,6 +71,7 @@ describe('All of the compatible tests', () => {
7071
if (Array.isArray(result)) result = result.map(i => i.toNumber ? Number(i) : i)
7172
expect(correction(result)).toStrictEqual(testCase.result)
7273
} catch (err) {
74+
if (err.message && err.message.includes('expect')) throw err
7375
expect(testCase.error).toStrictEqual(true)
7476
}
7577
})

compiler.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import asyncIterators from './async_iterators.js'
1212
import { coerceArray } from './utilities/coerceArray.js'
1313
import { countArguments } from './utilities/countArguments.js'
14-
import { downgrade, precoerceNumber } from './utilities/downgrade.js'
14+
import { precoerceNumber } from './utilities/downgrade.js'
1515

1616
/**
1717
* Provides a simple way to compile logic into a function that can be run.
@@ -310,12 +310,12 @@ function processBuiltString (method, str, buildState) {
310310
str = str.replace(`__%%%${x}%%%__`, item)
311311
})
312312

313-
const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade, precoerceNumber) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { ${str.includes('prev') ? 'let prev;' : ''} const result = ${str}; return result }`
313+
const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { ${str.includes('prev') ? 'let prev;' : ''} const result = ${str}; return result }`
314314
// console.log(str)
315315
// console.log(final)
316316
// eslint-disable-next-line no-eval
317317
return Object.assign(
318-
(typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade, precoerceNumber), {
318+
(typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber), {
319319
[Sync]: !buildState.asyncDetected,
320320
aboveDetected: typeof str === 'string' && str.includes(', above')
321321
})

defaultMethods.js

Lines changed: 131 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { build, buildString } from './compiler.js'
88
import chainingSupported from './utilities/chainingSupported.js'
99
import InvalidControlInput from './errors/InvalidControlInput.js'
1010
import legacyMethods from './legacy.js'
11-
import { downgrade } from './utilities/downgrade.js'
11+
import { precoerceNumber } from './utilities/downgrade.js'
1212

1313
function isDeterministic (method, engine, buildState) {
1414
if (Array.isArray(method)) {
@@ -56,68 +56,65 @@ const oldAll = createArrayIterativeMethod('every', true)
5656
const defaultMethods = {
5757
'+': (data) => {
5858
if (!data) return 0
59-
if (typeof data === 'string') return +data
60-
if (typeof data === 'number') return +data
61-
if (typeof data === 'boolean') return +data
62-
if (typeof data === 'object' && !Array.isArray(data)) return Number.NaN
59+
if (typeof data === 'string') return precoerceNumber(+data)
60+
if (typeof data === 'number') return precoerceNumber(+data)
61+
if (typeof data === 'boolean') return precoerceNumber(+data)
62+
if (typeof data === 'object' && !Array.isArray(data)) throw new Error('NaN')
6363
let res = 0
6464
for (let i = 0; i < data.length; i++) {
65-
if (data[i] && typeof data[i] === 'object') return Number.NaN
65+
if (data[i] && typeof data[i] === 'object') throw new Error('NaN')
6666
res += +data[i]
6767
}
68+
if (Number.isNaN(res)) throw new Error('NaN')
6869
return res
6970
},
7071
'*': (data) => {
7172
let res = 1
7273
for (let i = 0; i < data.length; i++) {
73-
if (data[i] && typeof data[i] === 'object') return Number.NaN
74+
if (data[i] && typeof data[i] === 'object') throw new Error('NaN')
7475
res *= +data[i]
7576
}
77+
if (Number.isNaN(res)) throw new Error('NaN')
7678
return res
7779
},
7880
'/': (data) => {
79-
if (data[0] && typeof data[0] === 'object') return Number.NaN
81+
if (data[0] && typeof data[0] === 'object') throw new Error('NaN')
8082
let res = +data[0]
8183
for (let i = 1; i < data.length; i++) {
82-
if ((data[i] && typeof data[i] === 'object') || !data[i]) return Number.NaN
84+
if ((data[i] && typeof data[i] === 'object') || !data[i]) throw new Error('NaN')
8385
res /= +data[i]
8486
}
87+
if (Number.isNaN(res)) throw new Error('NaN')
8588
return res
8689
},
8790
'-': (data) => {
8891
if (!data) return 0
89-
if (typeof data === 'string') return -data
90-
if (typeof data === 'number') return -data
91-
if (typeof data === 'boolean') return -data
92-
if (typeof data === 'object' && !Array.isArray(data)) return Number.NaN
93-
if (data[0] && typeof data[0] === 'object') return Number.NaN
92+
if (typeof data === 'string') return precoerceNumber(-data)
93+
if (typeof data === 'number') return precoerceNumber(-data)
94+
if (typeof data === 'boolean') return precoerceNumber(-data)
95+
if (typeof data === 'object' && !Array.isArray(data)) throw new Error('NaN')
96+
if (data[0] && typeof data[0] === 'object') throw new Error('NaN')
9497
if (data.length === 1) return -data[0]
9598
let res = data[0]
9699
for (let i = 1; i < data.length; i++) {
97-
if (data[i] && typeof data[i] === 'object') return Number.NaN
100+
if (data[i] && typeof data[i] === 'object') throw new Error('NaN')
98101
res -= +data[i]
99102
}
103+
if (Number.isNaN(res)) throw new Error('NaN')
100104
return res
101105
},
102106
'%': (data) => {
103-
if (data[0] && typeof data[0] === 'object') return Number.NaN
107+
if (data[0] && typeof data[0] === 'object') throw new Error('NaN')
104108
let res = +data[0]
105109
for (let i = 1; i < data.length; i++) {
106-
if (data[i] && typeof data[i] === 'object') return Number.NaN
110+
if (data[i] && typeof data[i] === 'object') throw new Error('NaN')
107111
res %= +data[i]
108112
}
113+
if (Number.isNaN(res)) throw new Error('NaN')
109114
return res
110115
},
111116
error: (type) => {
112-
if (Array.isArray(type)) type = type[0]
113-
if (type === 'NaN') return Number.NaN
114-
return { error: type }
115-
},
116-
panic: (item) => {
117-
if (Array.isArray(item)) item = item[0]
118-
if (Number.isNaN(item)) throw new Error('NaN was returned from expression')
119-
if (item && item.error) throw item.error
120-
return item
117+
throw new Error(type)
121118
},
122119
max: (data) => Math.max(...data),
123120
min: (data) => Math.min(...data),
@@ -289,8 +286,98 @@ const defaultMethods = {
289286
},
290287
lazy: true
291288
},
292-
'??': defineCoalesce(),
293-
try: defineCoalesce(downgrade, true),
289+
'??': {
290+
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
291+
method: (arr, _1, _2, engine) => {
292+
// See "executeInLoop" above
293+
const executeInLoop = Array.isArray(arr)
294+
if (!executeInLoop) arr = engine.run(arr, _1, { above: _2 })
295+
296+
let item
297+
for (let i = 0; i < arr.length; i++) {
298+
item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i]
299+
if (item !== null && item !== undefined) return item
300+
}
301+
302+
if (item === undefined) return null
303+
return item
304+
},
305+
asyncMethod: async (arr, _1, _2, engine) => {
306+
// See "executeInLoop" above
307+
const executeInLoop = Array.isArray(arr)
308+
if (!executeInLoop) arr = await engine.run(arr, _1, { above: _2 })
309+
310+
let item
311+
for (let i = 0; i < arr.length; i++) {
312+
item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i]
313+
if (item !== null && item !== undefined) return item
314+
}
315+
316+
if (item === undefined) return null
317+
return item
318+
},
319+
deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState),
320+
compile: (data, buildState) => {
321+
if (!chainingSupported) return false
322+
323+
if (Array.isArray(data) && data.length) {
324+
return `(${data.map((i, x) => {
325+
const built = buildString(i, buildState)
326+
if (Array.isArray(i) || !i || typeof i !== 'object' || x === data.length - 1) return built
327+
return '(' + built + ')'
328+
}).join(' ?? ')})`
329+
}
330+
return `(${buildString(data, buildState)}).reduce((a,b) => (a) ?? b, null)`
331+
},
332+
lazy: true
333+
},
334+
try: {
335+
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
336+
method: (arr, _1, _2, engine) => {
337+
// See "executeInLoop" above
338+
const executeInLoop = Array.isArray(arr)
339+
if (!executeInLoop) arr = engine.run(arr, _1, { above: _2 })
340+
341+
let item
342+
let lastError
343+
for (let i = 0; i < arr.length; i++) {
344+
try {
345+
// Todo: make this message thing more robust.
346+
if (lastError) item = engine.run(arr[i], { error: lastError.message || lastError.constructor.name }, { above: [null, _1, _2] })
347+
else item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i]
348+
return item
349+
} catch (e) {
350+
if (Number.isNaN(e)) lastError = { message: 'NaN' }
351+
else lastError = e
352+
}
353+
}
354+
355+
throw lastError
356+
},
357+
asyncMethod: async (arr, _1, _2, engine) => {
358+
// See "executeInLoop" above
359+
const executeInLoop = Array.isArray(arr)
360+
if (!executeInLoop) arr = await engine.run(arr, _1, { above: _2 })
361+
362+
let item
363+
let lastError
364+
for (let i = 0; i < arr.length; i++) {
365+
try {
366+
// Todo: make this message thing more robust.
367+
if (lastError) item = await engine.run(arr[i], { error: lastError.message || lastError.constructor.name }, { above: [null, _1, _2] })
368+
else item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i]
369+
return item
370+
} catch (e) {
371+
if (Number.isNaN(e)) lastError = { message: 'NaN' }
372+
else lastError = e
373+
}
374+
}
375+
376+
throw lastError
377+
},
378+
deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState),
379+
lazy: true
380+
},
294381
and: {
295382
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
296383
method: (arr, _1, _2, engine) => {
@@ -712,64 +799,6 @@ const defaultMethods = {
712799
}
713800
}
714801

715-
/**
716-
* Defines separate coalesce methods
717-
*/
718-
function defineCoalesce (func, panic) {
719-
let downgrade
720-
if (func) downgrade = func
721-
else downgrade = (a) => a
722-
723-
return {
724-
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
725-
method: (arr, _1, _2, engine) => {
726-
// See "executeInLoop" above
727-
const executeInLoop = Array.isArray(arr)
728-
if (!executeInLoop) arr = engine.run(arr, _1, { above: _2 })
729-
730-
let item
731-
for (let i = 0; i < arr.length; i++) {
732-
item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i]
733-
if (downgrade(item) !== null && item !== undefined) return item
734-
}
735-
736-
if (item === undefined) return null
737-
if (panic) throw item
738-
return item
739-
},
740-
asyncMethod: async (arr, _1, _2, engine) => {
741-
// See "executeInLoop" above
742-
const executeInLoop = Array.isArray(arr)
743-
if (!executeInLoop) arr = await engine.run(arr, _1, { above: _2 })
744-
745-
let item
746-
for (let i = 0; i < arr.length; i++) {
747-
item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i]
748-
if (downgrade(item) !== null && item !== undefined) return item
749-
}
750-
751-
if (item === undefined) return null
752-
if (panic) throw item
753-
return item
754-
},
755-
deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState),
756-
compile: (data, buildState) => {
757-
if (!chainingSupported) return false
758-
const funcCall = func ? 'downgrade' : ''
759-
if (Array.isArray(data) && data.length) {
760-
return `(${data.map((i, x) => {
761-
const built = buildString(i, buildState)
762-
if (panic && x === data.length - 1) return `(typeof ((prev = ${built}) || 0).error !== 'undefined' || Number.isNaN(prev) ? (() => { throw prev.error })() : prev)`
763-
if (Array.isArray(i) || !i || typeof i !== 'object' || x === data.length - 1) return built
764-
return `${funcCall}(` + built + ')'
765-
}).join(' ?? ')})`
766-
}
767-
return `(${buildString(data, buildState)}).reduce((a,b) => ${funcCall}(a) ?? b, null)`
768-
},
769-
lazy: true
770-
}
771-
}
772-
773802
function createArrayIterativeMethod (name, useTruthy = false) {
774803
return {
775804
deterministic: (data, buildState) => {
@@ -898,15 +927,24 @@ defaultMethods.if.compile = function (data, buildState) {
898927
* Transforms the operands of the arithmetic operation to numbers.
899928
*/
900929
function numberCoercion (i, buildState) {
901-
if (Array.isArray(i)) return 'NaN'
902-
if (typeof i === 'string' || typeof i === 'number' || typeof i === 'boolean') return `(+${buildString(i, buildState)})`
903-
return `(+precoerceNumber(${buildString(i, buildState)}))`
930+
if (Array.isArray(i)) return 'precoerceNumber(NaN)'
931+
932+
if (typeof i === 'number' || typeof i === 'boolean') return '+' + buildString(i, buildState)
933+
if (typeof i === 'string') return '+' + precoerceNumber(+i)
934+
935+
// check if it's already a number once built
936+
const f = buildString(i, buildState)
937+
938+
// regex match
939+
if (/^-?\d+(\.\d*)?$/.test(f)) return '+' + f
940+
941+
return `(+precoerceNumber(${f}))`
904942
}
905943

906944
// @ts-ignore Allow custom attribute
907945
defaultMethods['+'].compile = function (data, buildState) {
908946
if (Array.isArray(data)) return `(${data.map(i => numberCoercion(i, buildState)).join(' + ')})`
909-
if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') return `(+${buildString(data, buildState)})`
947+
if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') return `precoerceNumber(+${buildString(data, buildState)})`
910948
return buildState.compile`(Array.isArray(prev = ${data}) ? prev.reduce((a,b) => (+a)+(+precoerceNumber(b)), 0) : +precoerceNumber(prev))`
911949
}
912950

@@ -933,11 +971,11 @@ defaultMethods['/'].compile = function (data, buildState) {
933971
if (Array.isArray(data)) {
934972
return `(${data.map((i, x) => {
935973
let res = numberCoercion(i, buildState)
936-
if (x) res = `(${res}||NaN)`
974+
if (x) res = `precoerceNumber(${res} || NaN)`
937975
return res
938976
}).join(' / ')})`
939977
}
940-
return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b) || NaN))`
978+
return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b || NaN)))`
941979
}
942980
// @ts-ignore Allow custom attribute
943981
defaultMethods['*'].compile = function (data, buildState) {
@@ -964,7 +1002,7 @@ defaultMethods['!!'].compile = function (data, buildState) {
9641002
defaultMethods.none.deterministic = defaultMethods.some.deterministic
9651003

9661004
// @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations
967-
defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = defaultMethods.error.optimizeUnary = defaultMethods.panic.optimizeUnary = true
1005+
defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = defaultMethods.error.optimizeUnary = true
9681006

9691007
export default {
9701008
...defaultMethods,

general.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,14 +279,14 @@ describe('Various Test Cases', () => {
279279

280280
it('should throw on a soft error when panic is used', async () => {
281281
const rule = {
282-
panic: { '+': 'hi' }
282+
try: { '+': 'hi' }
283283
}
284284

285285
for (const engine of normalEngines) await testEngine(engine, rule, {}, Error)
286286
for (const engine of permissiveEngines) await testEngine(engine, rule, {}, Error)
287287

288288
const rule2 = {
289-
panic: { error: 'Yeet' }
289+
try: { error: 'Yeet' }
290290
}
291291

292292
for (const engine of normalEngines) await testEngine(engine, rule2, {}, Error)

0 commit comments

Comments
 (0)