Skip to content

Commit d9b89bd

Browse files
committed
Add implementation for exists, and coalescing, and test suites for each
1 parent 75e096b commit d9b89bd

File tree

4 files changed

+194
-15
lines changed

4 files changed

+194
-15
lines changed

constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export const Sync = Symbol.for('json_logic_sync')
55
export const Compiled = Symbol.for('json_logic_compiled')
66
export const EfficientTop = Symbol.for('json_logic_efficientTop')
7+
export const Unfound = Symbol.for('json_logic_unfound')
78

89
/**
910
* Checks if an item is synchronous.

defaultMethods.js

+55-15
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
'use strict'
33

44
import asyncIterators from './async_iterators.js'
5-
import { Sync, isSync } from './constants.js'
5+
import { Sync, isSync, Unfound } from './constants.js'
66
import declareSync from './utilities/declareSync.js'
77
import { build, buildString } from './compiler.js'
88
import chainingSupported from './utilities/chainingSupported.js'
@@ -161,6 +161,43 @@ const defaultMethods = {
161161
xor: ([a, b]) => a ^ b,
162162
// Why "executeInLoop"? Because if it needs to execute to get an array, I do not want to execute the arguments,
163163
// Both for performance and safety reasons.
164+
'??': {
165+
method: (arr, _1, _2, engine) => {
166+
// See "executeInLoop" above
167+
const executeInLoop = Array.isArray(arr)
168+
if (!executeInLoop) arr = engine.run(arr, _1, { above: _2 })
169+
170+
let item
171+
for (let i = 0; i < arr.length; i++) {
172+
item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i]
173+
if (item !== null && item !== undefined) return item
174+
}
175+
176+
if (item === undefined) return null
177+
return item
178+
},
179+
asyncMethod: async (arr, _1, _2, engine) => {
180+
// See "executeInLoop" above
181+
const executeInLoop = Array.isArray(arr)
182+
if (!executeInLoop) arr = await engine.run(arr, _1, { above: _2 })
183+
184+
let item
185+
for (let i = 0; i < arr.length; i++) {
186+
item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i]
187+
if (item !== null && item !== undefined) return item
188+
}
189+
190+
if (item === undefined) return null
191+
return item
192+
},
193+
deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState),
194+
compile: (data, buildState) => {
195+
if (!chainingSupported) return false
196+
if (Array.isArray(data) && data.length) return `(${data.map((i) => buildString(i, buildState)).join(' ?? ')})`
197+
return `(${buildString(data, buildState)}).reduce((a,b) => a ?? b, null)`
198+
},
199+
traverse: false
200+
},
164201
or: {
165202
method: (arr, _1, _2, engine) => {
166203
// See "executeInLoop" above
@@ -191,11 +228,8 @@ const defaultMethods = {
191228
deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState),
192229
compile: (data, buildState) => {
193230
if (!buildState.engine.truthy.IDENTITY) return false
194-
if (Array.isArray(data)) {
195-
return `(${data.map((i) => buildString(i, buildState)).join(' || ')})`
196-
} else {
197-
return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)`
198-
}
231+
if (Array.isArray(data) && data.length) return `(${data.map((i) => buildString(i, buildState)).join(' || ')})`
232+
return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)`
199233
},
200234
traverse: false
201235
},
@@ -228,11 +262,8 @@ const defaultMethods = {
228262
deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState),
229263
compile: (data, buildState) => {
230264
if (!buildState.engine.truthy.IDENTITY) return false
231-
if (Array.isArray(data)) {
232-
return `(${data.map((i) => buildString(i, buildState)).join(' && ')})`
233-
} else {
234-
return `(${buildString(data, buildState)}).reduce((a,b) => a&&b, true)`
235-
}
265+
if (Array.isArray(data) && data.length) return `(${data.map((i) => buildString(i, buildState)).join(' && ')})`
266+
return `(${buildString(data, buildState)}).reduce((a,b) => a&&b, true)`
236267
}
237268
},
238269
substr: ([string, from, end]) => {
@@ -267,12 +298,20 @@ const defaultMethods = {
267298
}
268299
}
269300
},
270-
// Adding this to spec something out, not to merge it quite yet
301+
exists: {
302+
method: (key, context, above, engine) => {
303+
const result = defaultMethods.val.method(key, context, above, engine, Unfound)
304+
return result !== Unfound
305+
},
306+
traverse: true,
307+
deterministic: false
308+
},
271309
val: {
272-
method: (args, context, above, engine) => {
310+
method: (args, context, above, engine, /** @type {null | Symbol} */ unFound = null) => {
273311
if (Array.isArray(args) && args.length === 1) args = args[0]
274312
// A unary optimization
275313
if (!Array.isArray(args)) {
314+
if (unFound && !(context && args in context)) return unFound
276315
const result = context[args]
277316
if (typeof result === 'undefined') return null
278317
return result
@@ -295,11 +334,12 @@ const defaultMethods = {
295334
}
296335
// This block handles traversing the path
297336
for (let i = start; i < args.length; i++) {
337+
if (unFound && !(result && args[i] in result)) return unFound
298338
if (result === null || result === undefined) return null
299339
result = result[args[i]]
300340
}
301-
if (typeof result === 'undefined') return null
302-
if (typeof result === 'function' && !engine.allowFunctions) return null
341+
if (typeof result === 'undefined') return unFound
342+
if (typeof result === 'function' && !engine.allowFunctions) return unFound
303343
return result
304344
},
305345
optimizeUnary: true,

suites/coalesce.json

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
[
2+
"Test Specification for ??",
3+
{
4+
"description": "Coalesces a string alone",
5+
"rule": { "??": ["hello"] },
6+
"data": null,
7+
"result": "hello"
8+
},
9+
{
10+
"description": "Coalesces a number alone",
11+
"rule": { "??": [1] },
12+
"data": null,
13+
"result": 1
14+
},
15+
{
16+
"description": "Coalesces a boolean alone",
17+
"rule": { "??": [true] },
18+
"data": null,
19+
"result": true
20+
},
21+
{
22+
"description": "Coalesces an object from context alone",
23+
"rule": { "??": [{ "val": "person" }]},
24+
"data": { "person": { "name": "John" } },
25+
"result": { "name": "John" }
26+
},
27+
{
28+
"description": "Empty behavior",
29+
"rule": { "??": [] },
30+
"data": null,
31+
"result": null
32+
},
33+
{
34+
"description": "Coalesces a string with nulls before",
35+
"rule": { "??": [null, "hello"] },
36+
"data": null,
37+
"result": "hello"
38+
},
39+
{
40+
"description": "Coalesces a string with multiple nulls before",
41+
"rule": { "??": [null, null, null, "hello"] },
42+
"data": null,
43+
"result": "hello"
44+
},
45+
{
46+
"description": "Coalesces a string with nulls after",
47+
"rule": { "??": ["hello", null] },
48+
"data": null,
49+
"result": "hello"
50+
},
51+
{
52+
"description": "Coalesces a string with nulls both before and after",
53+
"rule": { "??": [null, "hello", null] },
54+
"data": null,
55+
"result": "hello"
56+
},
57+
{
58+
"description": "Coalesces a number with nulls both before and after",
59+
"rule": { "??": [null, 1, null] },
60+
"data": null,
61+
"result": 1
62+
},
63+
{
64+
"description": "Uses the first non-null value",
65+
"rule": { "??": [null, 1, "hello"] },
66+
"data": null,
67+
"result": 1
68+
},
69+
{
70+
"description": "Uses the first non-null value, even if it is false",
71+
"rule": { "??": [null, false, "hello"] },
72+
"data": null,
73+
"result": false
74+
},
75+
{
76+
"description": "Uses the first non-null value from context",
77+
"rule": { "??": [{ "val": ["person", "name"] }, { "val": "name" }] },
78+
"data": { "person": { "name": "John" }, "name": "Jane" },
79+
"result": "John"
80+
},
81+
{
82+
"description": "Uses the first non-null value from context (with person undefined)",
83+
"rule": { "??": [{ "val": ["person", "name"] }, { "val": "name" }] },
84+
"data": { "name": "Jane" },
85+
"result": "Jane"
86+
}
87+
]

suites/exists.json

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
[
2+
"Test Specification for exists",
3+
{
4+
"description": "Checks if a normal key exists",
5+
"rule": { "exists": "hello" },
6+
"data": { "hello" : 1 },
7+
"result": true
8+
},
9+
{
10+
"description": "Checks if a normal key exists (array)",
11+
"rule": { "exists": ["hello"] },
12+
"data": { "hello" : 1 },
13+
"result": true
14+
},
15+
{
16+
"description": "Checks if a normal key exists (false)",
17+
"rule": { "exists": "hello" },
18+
"data": { "world" : 1 },
19+
"result": false
20+
},
21+
{
22+
"description": "Checks if an empty key exists (true)",
23+
"rule": { "exists": [""] },
24+
"data": { "" : 1 },
25+
"result": true
26+
},
27+
{
28+
"description": "Checks if an empty key exists (false)",
29+
"rule": { "exists": [""] },
30+
"data": { "hello" : 1 },
31+
"result": false
32+
},
33+
{
34+
"description": "Checks if a nested key exists",
35+
"rule": { "exists": ["hello", "world"] },
36+
"data": { "hello" : { "world": false } },
37+
"result": true
38+
},
39+
{
40+
"description": "Checks if a nested key exists (false)",
41+
"rule": { "exists": ["hello", "world"] },
42+
"data": { "hello" : { "x": false } },
43+
"result": false
44+
},
45+
{
46+
"description": "Checks if a null value exists",
47+
"rule": { "exists": "hello" },
48+
"data": { "hello" : null },
49+
"result": true
50+
}
51+
]

0 commit comments

Comments
 (0)