@nordcraft/core 1.0.95 → 1.0.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/api.js +30 -10
- package/dist/api/api.js.map +1 -1
- package/dist/component/ToddleComponent.js +3 -3
- package/dist/component/ToddleComponent.js.map +1 -1
- package/dist/component/component.types.d.ts +3 -3
- package/dist/component/component.types.js.map +1 -1
- package/dist/formula/andFormula.d.ts +3 -0
- package/dist/formula/andFormula.js +25 -0
- package/dist/formula/andFormula.js.map +1 -0
- package/dist/formula/applyFormula.d.ts +2 -0
- package/dist/formula/applyFormula.js +49 -0
- package/dist/formula/applyFormula.js.map +1 -0
- package/dist/formula/arrayFormula.d.ts +2 -0
- package/dist/formula/arrayFormula.js +5 -0
- package/dist/formula/arrayFormula.js.map +1 -0
- package/dist/formula/formula.d.ts +8 -6
- package/dist/formula/formula.js +45 -172
- package/dist/formula/formula.js.map +1 -1
- package/dist/formula/formulaTypes.d.ts +1 -0
- package/dist/formula/formulaUtils.d.ts +1 -1
- package/dist/formula/formulaUtils.js.map +1 -1
- package/dist/formula/functionFormula.d.ts +2 -0
- package/dist/formula/functionFormula.js +88 -0
- package/dist/formula/functionFormula.js.map +1 -0
- package/dist/formula/objectFormula.d.ts +2 -0
- package/dist/formula/objectFormula.js +8 -0
- package/dist/formula/objectFormula.js.map +1 -0
- package/dist/formula/orFormula.d.ts +3 -0
- package/dist/formula/orFormula.js +27 -0
- package/dist/formula/orFormula.js.map +1 -0
- package/dist/formula/pathFormula.d.ts +2 -0
- package/dist/formula/pathFormula.js +13 -0
- package/dist/formula/pathFormula.js.map +1 -0
- package/dist/formula/recordFormula.d.ts +2 -0
- package/dist/formula/recordFormula.js +8 -0
- package/dist/formula/recordFormula.js.map +1 -0
- package/dist/formula/switchFormula.d.ts +3 -0
- package/dist/formula/switchFormula.js +40 -0
- package/dist/formula/switchFormula.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/utils/measure.js +5 -0
- package/dist/utils/measure.js.map +1 -1
- package/package.json +1 -1
- package/src/api/api.test.ts +22 -22
- package/src/api/api.ts +36 -10
- package/src/component/ToddleComponent.ts +9 -7
- package/src/component/component.types.ts +5 -3
- package/src/formula/andFormula.test.ts +112 -0
- package/src/formula/andFormula.ts +33 -0
- package/src/formula/applyFormula.test.ts +151 -0
- package/src/formula/applyFormula.ts +72 -0
- package/src/formula/arrayFormula.test.ts +52 -0
- package/src/formula/arrayFormula.ts +14 -0
- package/src/formula/formula.ts +67 -206
- package/src/formula/formulaTypes.ts +5 -0
- package/src/formula/formulaUtils.ts +1 -1
- package/src/formula/functionFormula.test.ts +89 -0
- package/src/formula/functionFormula.ts +118 -0
- package/src/formula/objectFormula.test.ts +56 -0
- package/src/formula/objectFormula.ts +17 -0
- package/src/formula/orFormula.test.ts +113 -0
- package/src/formula/orFormula.ts +33 -0
- package/src/formula/pathFormula.test.ts +35 -0
- package/src/formula/pathFormula.ts +17 -0
- package/src/formula/recordFormula.test.ts +56 -0
- package/src/formula/recordFormula.ts +17 -0
- package/src/formula/switchFormula.test.ts +122 -0
- package/src/formula/switchFormula.ts +54 -0
- package/src/formula/testUtils.test.ts +68 -0
- package/src/types.ts +1 -1
- package/src/utils/measure.ts +6 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import type { FormulaHandler, Toddle } from '../types'
|
|
3
|
+
import { measure } from '../utils/measure'
|
|
4
|
+
import { isDefined } from '../utils/util'
|
|
5
|
+
import {
|
|
6
|
+
applyFormula,
|
|
7
|
+
isToddleFormula,
|
|
8
|
+
type FormulaContext,
|
|
9
|
+
type FunctionOperation,
|
|
10
|
+
} from './formula'
|
|
11
|
+
|
|
12
|
+
export const applyFunctionFormula = (
|
|
13
|
+
formula: FunctionOperation,
|
|
14
|
+
ctx: FormulaContext,
|
|
15
|
+
) => {
|
|
16
|
+
const stopMeasure = measure(`Formula: ${formula.name}`, {
|
|
17
|
+
formula,
|
|
18
|
+
component: ctx.component?.name,
|
|
19
|
+
})
|
|
20
|
+
const packageName = formula.package ?? ctx.package ?? undefined
|
|
21
|
+
const newFunc = (
|
|
22
|
+
ctx.toddle ??
|
|
23
|
+
((globalThis as any).toddle as Toddle<unknown, unknown> | undefined)
|
|
24
|
+
)?.getCustomFormula(formula.name, packageName)
|
|
25
|
+
if (isDefined(newFunc)) {
|
|
26
|
+
ctx.package = packageName
|
|
27
|
+
const args = (formula.arguments ?? []).reduce<Record<string, unknown>>(
|
|
28
|
+
(args, arg, i) => ({
|
|
29
|
+
...args,
|
|
30
|
+
[arg.name ?? `${i}`]: arg.isFunction
|
|
31
|
+
? (Args: any) =>
|
|
32
|
+
applyFormula(
|
|
33
|
+
arg.formula,
|
|
34
|
+
{
|
|
35
|
+
...ctx,
|
|
36
|
+
data: {
|
|
37
|
+
...ctx.data,
|
|
38
|
+
Args: ctx.data.Args
|
|
39
|
+
? { ...Args, '@toddle.parent': ctx.data.Args }
|
|
40
|
+
: Args,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
['arguments', i],
|
|
44
|
+
)
|
|
45
|
+
: applyFormula(arg.formula, ctx, ['arguments', i]),
|
|
46
|
+
}),
|
|
47
|
+
{},
|
|
48
|
+
)
|
|
49
|
+
try {
|
|
50
|
+
if (isToddleFormula(newFunc)) {
|
|
51
|
+
return applyFormula(
|
|
52
|
+
newFunc.formula,
|
|
53
|
+
{
|
|
54
|
+
...ctx,
|
|
55
|
+
data: { ...ctx.data, Args: args },
|
|
56
|
+
},
|
|
57
|
+
['formula'],
|
|
58
|
+
)
|
|
59
|
+
} else {
|
|
60
|
+
return newFunc.handler(args, {
|
|
61
|
+
root: ctx.root ?? document,
|
|
62
|
+
env: ctx.env,
|
|
63
|
+
} as any)
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
ctx.toddle.errors.push(e as Error)
|
|
67
|
+
if (ctx.env?.logErrors) {
|
|
68
|
+
console.error(e)
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
} finally {
|
|
72
|
+
stopMeasure()
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Lookup legacy formula
|
|
76
|
+
const legacyFunc: FormulaHandler | undefined = (
|
|
77
|
+
ctx.toddle ?? ((globalThis as any).toddle as Toddle<unknown, unknown>)
|
|
78
|
+
).getFormula(formula.name)
|
|
79
|
+
if (typeof legacyFunc === 'function') {
|
|
80
|
+
const args = (formula.arguments ?? []).map((arg, i) =>
|
|
81
|
+
arg.isFunction
|
|
82
|
+
? (Args: any) =>
|
|
83
|
+
applyFormula(
|
|
84
|
+
arg.formula,
|
|
85
|
+
{
|
|
86
|
+
...ctx,
|
|
87
|
+
data: {
|
|
88
|
+
...ctx.data,
|
|
89
|
+
Args: ctx.data.Args
|
|
90
|
+
? { ...Args, '@toddle.parent': ctx.data.Args }
|
|
91
|
+
: Args,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
['arguments', i],
|
|
95
|
+
)
|
|
96
|
+
: applyFormula(arg.formula, ctx, ['arguments', i]),
|
|
97
|
+
)
|
|
98
|
+
try {
|
|
99
|
+
return legacyFunc(args, ctx as any)
|
|
100
|
+
} catch (e) {
|
|
101
|
+
ctx.toddle.errors.push(e as Error)
|
|
102
|
+
if (ctx.env?.logErrors) {
|
|
103
|
+
console.error(e)
|
|
104
|
+
}
|
|
105
|
+
return null
|
|
106
|
+
} finally {
|
|
107
|
+
stopMeasure()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (ctx.env?.logErrors) {
|
|
112
|
+
console.error(
|
|
113
|
+
`Could not find formula ${formula.name} in package ${packageName ?? ''}`,
|
|
114
|
+
formula,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import type { ObjectOperation } from './formula'
|
|
3
|
+
import { valueFormula } from './formulaUtils'
|
|
4
|
+
import { applyObjectFormula } from './objectFormula'
|
|
5
|
+
import {
|
|
6
|
+
createTestFormulaContext,
|
|
7
|
+
createTestFormulaContextForAllPaths,
|
|
8
|
+
} from './testUtils.test'
|
|
9
|
+
|
|
10
|
+
describe('applyObjectFormula', () => {
|
|
11
|
+
it('returns an object with evaluated values', () => {
|
|
12
|
+
const formula: ObjectOperation = {
|
|
13
|
+
type: 'object',
|
|
14
|
+
arguments: [
|
|
15
|
+
{ name: 'a', formula: valueFormula(1) },
|
|
16
|
+
{ name: 'b', formula: valueFormula('hello') },
|
|
17
|
+
{ name: 'c', formula: valueFormula(true) },
|
|
18
|
+
],
|
|
19
|
+
}
|
|
20
|
+
const ctx = createTestFormulaContext()
|
|
21
|
+
expect(applyObjectFormula(formula, ctx)).toEqual({
|
|
22
|
+
a: 1,
|
|
23
|
+
b: 'hello',
|
|
24
|
+
c: true,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns an empty object if arguments is undefined', () => {
|
|
29
|
+
const formula: ObjectOperation = {
|
|
30
|
+
type: 'object',
|
|
31
|
+
arguments: undefined,
|
|
32
|
+
}
|
|
33
|
+
const ctx = createTestFormulaContext()
|
|
34
|
+
expect(applyObjectFormula(formula, ctx)).toEqual({})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('evaluates all formulas and reports results in "report" mode', () => {
|
|
38
|
+
const formula: ObjectOperation = {
|
|
39
|
+
type: 'object',
|
|
40
|
+
arguments: [
|
|
41
|
+
{ name: 'x', formula: valueFormula('foo') },
|
|
42
|
+
{ name: 'y', formula: valueFormula('bar') },
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
const results: Record<string, any> = {}
|
|
46
|
+
const ctx = createTestFormulaContextForAllPaths(
|
|
47
|
+
{},
|
|
48
|
+
(path, result) => (results[path.join('/')] = result),
|
|
49
|
+
)
|
|
50
|
+
expect(applyObjectFormula(formula, ctx)).toEqual({ x: 'foo', y: 'bar' })
|
|
51
|
+
expect(results).toMatchObject({
|
|
52
|
+
'arguments/0/formula': 'foo',
|
|
53
|
+
'arguments/1/formula': 'bar',
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyFormula,
|
|
3
|
+
type FormulaContext,
|
|
4
|
+
type ObjectOperation,
|
|
5
|
+
} from './formula'
|
|
6
|
+
|
|
7
|
+
export const applyObjectFormula = (
|
|
8
|
+
formula: ObjectOperation,
|
|
9
|
+
ctx: FormulaContext,
|
|
10
|
+
) => {
|
|
11
|
+
return Object.fromEntries(
|
|
12
|
+
formula.arguments?.map((entry, i) => [
|
|
13
|
+
entry.name,
|
|
14
|
+
applyFormula(entry.formula, ctx, ['arguments', i, 'formula']),
|
|
15
|
+
]) ?? [],
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { applyFormula, type OrOperation } from './formula'
|
|
3
|
+
import { valueFormula } from './formulaUtils'
|
|
4
|
+
import { applyEvaluateAllOrFormula, applyOrFormula } from './orFormula'
|
|
5
|
+
import {
|
|
6
|
+
createTestFormulaContext,
|
|
7
|
+
createTestFormulaContextForAllPaths,
|
|
8
|
+
} from './testUtils.test'
|
|
9
|
+
|
|
10
|
+
describe('applyOrFormula', () => {
|
|
11
|
+
it('returns true if any argument is truthy', () => {
|
|
12
|
+
const formula: OrOperation = {
|
|
13
|
+
type: 'or',
|
|
14
|
+
arguments: [
|
|
15
|
+
{ formula: valueFormula(false) },
|
|
16
|
+
{ formula: valueFormula(0) },
|
|
17
|
+
{ formula: valueFormula('yes') },
|
|
18
|
+
],
|
|
19
|
+
}
|
|
20
|
+
const ctx = createTestFormulaContext()
|
|
21
|
+
expect(applyOrFormula(formula, ctx)).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('returns false if all arguments are falsy', () => {
|
|
25
|
+
const formula: OrOperation = {
|
|
26
|
+
type: 'or',
|
|
27
|
+
arguments: [
|
|
28
|
+
{ formula: valueFormula(false) },
|
|
29
|
+
{ formula: valueFormula(null) },
|
|
30
|
+
{ formula: valueFormula(undefined) },
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
const ctx = createTestFormulaContext()
|
|
34
|
+
expect(applyOrFormula(formula, ctx)).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns true for first truthy argument', () => {
|
|
38
|
+
const formula: OrOperation = {
|
|
39
|
+
type: 'or',
|
|
40
|
+
arguments: [
|
|
41
|
+
{ formula: valueFormula(false) },
|
|
42
|
+
{ formula: valueFormula(true) },
|
|
43
|
+
{ formula: valueFormula(false) },
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
const ctx = createTestFormulaContext()
|
|
47
|
+
expect(applyOrFormula(formula, ctx)).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('applyEvaluateAllOrFormula', () => {
|
|
52
|
+
it('visits all formulas in "report" mode even if the first argument is truthy', () => {
|
|
53
|
+
const formula: OrOperation = {
|
|
54
|
+
type: 'or',
|
|
55
|
+
arguments: [
|
|
56
|
+
{ formula: valueFormula(true) },
|
|
57
|
+
{ formula: valueFormula('hello') },
|
|
58
|
+
{ formula: valueFormula(false) },
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
const results: Record<string, any> = {}
|
|
62
|
+
const ctx = createTestFormulaContextForAllPaths(
|
|
63
|
+
{},
|
|
64
|
+
(path, result) => (results[path.join('/')] = result),
|
|
65
|
+
)
|
|
66
|
+
// Call applyFormula directly since it will report the results
|
|
67
|
+
expect(applyFormula(formula, ctx, [])).toBe(true)
|
|
68
|
+
expect(results).toMatchObject({
|
|
69
|
+
'arguments/0/formula': true,
|
|
70
|
+
'arguments/1/formula': 'hello',
|
|
71
|
+
'arguments/2/formula': false,
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('returns true if any argument is truthy', () => {
|
|
76
|
+
const formula: OrOperation = {
|
|
77
|
+
type: 'or',
|
|
78
|
+
arguments: [
|
|
79
|
+
{ formula: valueFormula(false) },
|
|
80
|
+
{ formula: valueFormula(1) },
|
|
81
|
+
{ formula: valueFormula(false) },
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
const ctx = createTestFormulaContext()
|
|
85
|
+
expect(applyEvaluateAllOrFormula(formula, ctx)).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('returns false if all arguments are falsy', () => {
|
|
89
|
+
const formula: OrOperation = {
|
|
90
|
+
type: 'or',
|
|
91
|
+
arguments: [
|
|
92
|
+
{ formula: valueFormula(false) },
|
|
93
|
+
{ formula: valueFormula(null) },
|
|
94
|
+
{ formula: valueFormula(undefined) },
|
|
95
|
+
],
|
|
96
|
+
}
|
|
97
|
+
const ctx = createTestFormulaContext()
|
|
98
|
+
expect(applyEvaluateAllOrFormula(formula, ctx)).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('returns true if last argument is truthy', () => {
|
|
102
|
+
const formula: OrOperation = {
|
|
103
|
+
type: 'or',
|
|
104
|
+
arguments: [
|
|
105
|
+
{ formula: valueFormula(false) },
|
|
106
|
+
{ formula: valueFormula(false) },
|
|
107
|
+
{ formula: valueFormula('ok') },
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
const ctx = createTestFormulaContext()
|
|
111
|
+
expect(applyEvaluateAllOrFormula(formula, ctx)).toBe(true)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { toBoolean } from '../utils/util'
|
|
2
|
+
import { applyFormula, type FormulaContext, type OrOperation } from './formula'
|
|
3
|
+
|
|
4
|
+
export const applyOrFormula = (formula: OrOperation, ctx: FormulaContext) => {
|
|
5
|
+
for (let i = 0; i < (formula.arguments ?? []).length; i++) {
|
|
6
|
+
const arg = (formula.arguments ?? [])[i]
|
|
7
|
+
if (
|
|
8
|
+
toBoolean(applyFormula(arg?.formula, ctx, ['arguments', i, 'formula']))
|
|
9
|
+
) {
|
|
10
|
+
return true
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const applyEvaluateAllOrFormula = (
|
|
17
|
+
formula: OrOperation,
|
|
18
|
+
ctx: FormulaContext,
|
|
19
|
+
) => {
|
|
20
|
+
let orResult = false
|
|
21
|
+
for (let i = 0; i < (formula.arguments ?? []).length; i++) {
|
|
22
|
+
const arg = (formula.arguments ?? [])[i]
|
|
23
|
+
const argResult = applyFormula(arg?.formula, ctx, [
|
|
24
|
+
'arguments',
|
|
25
|
+
i,
|
|
26
|
+
'formula',
|
|
27
|
+
])
|
|
28
|
+
if (toBoolean(argResult)) {
|
|
29
|
+
orResult = true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return orResult
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import type { PathOperation } from './formula'
|
|
3
|
+
import { applyPathFormula } from './pathFormula'
|
|
4
|
+
|
|
5
|
+
describe('applyPathFormula', () => {
|
|
6
|
+
it('returns the value at the given path', () => {
|
|
7
|
+
const formula: PathOperation = { type: 'path', path: ['foo', 'bar'] }
|
|
8
|
+
const data: any = { foo: { bar: 42 } }
|
|
9
|
+
expect(applyPathFormula(formula, data)).toBe(42)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('returns undefined if the path does not exist', () => {
|
|
13
|
+
const formula: PathOperation = { type: 'path', path: ['foo', 'baz'] }
|
|
14
|
+
const data: any = { foo: { bar: 42 } }
|
|
15
|
+
expect(applyPathFormula(formula, data)).toBeUndefined()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('returns null if intermediate value is not an object', () => {
|
|
19
|
+
const formula: PathOperation = { type: 'path', path: ['foo', 'bar', 'baz'] }
|
|
20
|
+
const data: any = { foo: { bar: 42 } }
|
|
21
|
+
expect(applyPathFormula(formula, data)).toBeNull()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('returns the root value if path is empty', () => {
|
|
25
|
+
const formula: PathOperation = { type: 'path', path: [] }
|
|
26
|
+
const data: any = { foo: 1 }
|
|
27
|
+
expect(applyPathFormula(formula, data)).toEqual(data)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('works with numeric keys', () => {
|
|
31
|
+
const formula: PathOperation = { type: 'path', path: ['arr', 1] }
|
|
32
|
+
const data: any = { arr: [10, 20, 30] }
|
|
33
|
+
expect(applyPathFormula(formula, data)).toBe(20)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FormulaContext, PathOperation } from './formula'
|
|
2
|
+
|
|
3
|
+
export const applyPathFormula = (
|
|
4
|
+
formula: PathOperation,
|
|
5
|
+
data: FormulaContext['data'],
|
|
6
|
+
) => {
|
|
7
|
+
let input: any = data
|
|
8
|
+
for (const key of formula.path) {
|
|
9
|
+
if (input && typeof input === 'object') {
|
|
10
|
+
input = input[key]
|
|
11
|
+
} else {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return input
|
|
17
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import type { RecordOperation } from './formula'
|
|
3
|
+
import { valueFormula } from './formulaUtils'
|
|
4
|
+
import { applyRecordFormula } from './recordFormula'
|
|
5
|
+
import {
|
|
6
|
+
createTestFormulaContext,
|
|
7
|
+
createTestFormulaContextForAllPaths,
|
|
8
|
+
} from './testUtils.test'
|
|
9
|
+
|
|
10
|
+
describe('applyRecordFormula', () => {
|
|
11
|
+
it('returns an object with evaluated entries', () => {
|
|
12
|
+
const formula: RecordOperation = {
|
|
13
|
+
type: 'record',
|
|
14
|
+
entries: [
|
|
15
|
+
{ name: 'a', formula: valueFormula(1) },
|
|
16
|
+
{ name: 'b', formula: valueFormula('hello') },
|
|
17
|
+
{ name: 'c', formula: valueFormula(true) },
|
|
18
|
+
],
|
|
19
|
+
}
|
|
20
|
+
const ctx = createTestFormulaContext()
|
|
21
|
+
expect(applyRecordFormula(formula, ctx)).toEqual({
|
|
22
|
+
a: 1,
|
|
23
|
+
b: 'hello',
|
|
24
|
+
c: true,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns an empty object if entries is empty', () => {
|
|
29
|
+
const formula: RecordOperation = {
|
|
30
|
+
type: 'record',
|
|
31
|
+
entries: [],
|
|
32
|
+
}
|
|
33
|
+
const ctx = createTestFormulaContext()
|
|
34
|
+
expect(applyRecordFormula(formula, ctx)).toEqual({})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('evaluates all formulas and reports results in "report" mode', () => {
|
|
38
|
+
const formula: RecordOperation = {
|
|
39
|
+
type: 'record',
|
|
40
|
+
entries: [
|
|
41
|
+
{ name: 'x', formula: valueFormula('foo') },
|
|
42
|
+
{ name: 'y', formula: valueFormula('bar') },
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
const results: Record<string, any> = {}
|
|
46
|
+
const ctx = createTestFormulaContextForAllPaths(
|
|
47
|
+
{},
|
|
48
|
+
(path, result) => (results[path.join('/')] = result),
|
|
49
|
+
)
|
|
50
|
+
expect(applyRecordFormula(formula, ctx)).toEqual({ x: 'foo', y: 'bar' })
|
|
51
|
+
expect(results).toMatchObject({
|
|
52
|
+
'entries/0/formula': 'foo',
|
|
53
|
+
'entries/1/formula': 'bar',
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyFormula,
|
|
3
|
+
type FormulaContext,
|
|
4
|
+
type RecordOperation,
|
|
5
|
+
} from './formula'
|
|
6
|
+
|
|
7
|
+
export const applyRecordFormula = (
|
|
8
|
+
formula: RecordOperation,
|
|
9
|
+
ctx: FormulaContext,
|
|
10
|
+
) => {
|
|
11
|
+
return Object.fromEntries(
|
|
12
|
+
(formula.entries ?? []).map((entry, i) => [
|
|
13
|
+
entry.name,
|
|
14
|
+
applyFormula(entry.formula, ctx, ['entries', i, 'formula']),
|
|
15
|
+
]),
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { applyFormula, type SwitchOperation } from './formula'
|
|
3
|
+
import { valueFormula } from './formulaUtils'
|
|
4
|
+
import {
|
|
5
|
+
applyEvaluateAllSwitchFormula,
|
|
6
|
+
applySwitchFormula,
|
|
7
|
+
} from './switchFormula'
|
|
8
|
+
import {
|
|
9
|
+
createTestFormulaContext,
|
|
10
|
+
createTestFormulaContextForAllPaths,
|
|
11
|
+
} from './testUtils.test'
|
|
12
|
+
|
|
13
|
+
describe('applySwitchFormula', () => {
|
|
14
|
+
it('returns the first matching case value', () => {
|
|
15
|
+
const formula: SwitchOperation = {
|
|
16
|
+
type: 'switch',
|
|
17
|
+
cases: [
|
|
18
|
+
{ condition: valueFormula(false), formula: valueFormula('no') },
|
|
19
|
+
{ condition: valueFormula(true), formula: valueFormula('yes') },
|
|
20
|
+
{
|
|
21
|
+
condition: valueFormula(true),
|
|
22
|
+
formula: valueFormula('should not match'),
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
default: valueFormula('default'),
|
|
26
|
+
}
|
|
27
|
+
const ctx = createTestFormulaContext()
|
|
28
|
+
expect(applySwitchFormula(formula, ctx)).toBe('yes')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('returns the default value if no case matches', () => {
|
|
32
|
+
const formula: SwitchOperation = {
|
|
33
|
+
type: 'switch',
|
|
34
|
+
cases: [
|
|
35
|
+
{ condition: valueFormula(false), formula: valueFormula('no') },
|
|
36
|
+
{ condition: valueFormula(false), formula: valueFormula('nope') },
|
|
37
|
+
],
|
|
38
|
+
default: valueFormula('default'),
|
|
39
|
+
}
|
|
40
|
+
const ctx = createTestFormulaContext()
|
|
41
|
+
expect(applySwitchFormula(formula, ctx)).toBe('default')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('evaluates conditions using context data', () => {
|
|
45
|
+
const formula: SwitchOperation = {
|
|
46
|
+
type: 'switch',
|
|
47
|
+
cases: [
|
|
48
|
+
{
|
|
49
|
+
condition: { type: 'path', path: ['foo'] },
|
|
50
|
+
formula: valueFormula('foo matched'),
|
|
51
|
+
},
|
|
52
|
+
{ condition: valueFormula(true), formula: valueFormula('fallback') },
|
|
53
|
+
],
|
|
54
|
+
default: valueFormula('default'),
|
|
55
|
+
}
|
|
56
|
+
const ctx = createTestFormulaContext({ foo: true })
|
|
57
|
+
expect(applySwitchFormula(formula, ctx)).toBe('foo matched')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('applyEvaluateAllSwitchFormula', () => {
|
|
62
|
+
it('returns the first matching case value, but evaluates all cases and default', () => {
|
|
63
|
+
const formula: SwitchOperation = {
|
|
64
|
+
type: 'switch',
|
|
65
|
+
cases: [
|
|
66
|
+
{ condition: valueFormula(false), formula: valueFormula('no') },
|
|
67
|
+
{ condition: valueFormula(true), formula: valueFormula('yes') },
|
|
68
|
+
{
|
|
69
|
+
condition: valueFormula(true),
|
|
70
|
+
formula: valueFormula('should not match'),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
default: valueFormula('default'),
|
|
74
|
+
}
|
|
75
|
+
const results: Record<string, any> = {}
|
|
76
|
+
const ctx = createTestFormulaContextForAllPaths(
|
|
77
|
+
{},
|
|
78
|
+
(path, result) => (results[path.join('/')] = result),
|
|
79
|
+
)
|
|
80
|
+
// We use applyFormula directly since it will gather results in EVALUATE ALL PATHS mode
|
|
81
|
+
const result = applyFormula(formula, ctx, [])
|
|
82
|
+
expect(result).toBe('yes')
|
|
83
|
+
expect(results).toMatchObject({
|
|
84
|
+
'cases/0/condition': false,
|
|
85
|
+
'cases/0/formula': 'no',
|
|
86
|
+
'cases/1/condition': true,
|
|
87
|
+
'cases/1/formula': 'yes',
|
|
88
|
+
'cases/2/condition': true,
|
|
89
|
+
'cases/2/formula': 'should not match',
|
|
90
|
+
default: 'default',
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('returns the default value if no case matches', () => {
|
|
95
|
+
const formula: SwitchOperation = {
|
|
96
|
+
type: 'switch',
|
|
97
|
+
cases: [
|
|
98
|
+
{ condition: valueFormula(false), formula: valueFormula('no') },
|
|
99
|
+
{ condition: valueFormula(false), formula: valueFormula('nope') },
|
|
100
|
+
],
|
|
101
|
+
default: valueFormula('default'),
|
|
102
|
+
}
|
|
103
|
+
const ctx = createTestFormulaContext()
|
|
104
|
+
expect(applyEvaluateAllSwitchFormula(formula, ctx)).toBe('default')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('evaluates conditions using context data', () => {
|
|
108
|
+
const formula: SwitchOperation = {
|
|
109
|
+
type: 'switch',
|
|
110
|
+
cases: [
|
|
111
|
+
{
|
|
112
|
+
condition: { type: 'path', path: ['foo'] },
|
|
113
|
+
formula: valueFormula('foo matched'),
|
|
114
|
+
},
|
|
115
|
+
{ condition: valueFormula(true), formula: valueFormula('fallback') },
|
|
116
|
+
],
|
|
117
|
+
default: valueFormula('default'),
|
|
118
|
+
}
|
|
119
|
+
const ctx = createTestFormulaContext({ foo: true })
|
|
120
|
+
expect(applyEvaluateAllSwitchFormula(formula, ctx)).toBe('foo matched')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { toBoolean } from '../utils/util'
|
|
2
|
+
import {
|
|
3
|
+
applyFormula,
|
|
4
|
+
type FormulaContext,
|
|
5
|
+
type SwitchOperation,
|
|
6
|
+
} from './formula'
|
|
7
|
+
|
|
8
|
+
export const applySwitchFormula = (
|
|
9
|
+
formula: SwitchOperation,
|
|
10
|
+
ctx: FormulaContext,
|
|
11
|
+
) => {
|
|
12
|
+
// Evaluates cases until one matches
|
|
13
|
+
for (let i = 0; i < (formula.cases ?? []).length; i++) {
|
|
14
|
+
const switchCase = (formula.cases ?? [])[i]
|
|
15
|
+
if (
|
|
16
|
+
toBoolean(
|
|
17
|
+
applyFormula(switchCase?.condition, ctx, ['cases', i, 'condition']),
|
|
18
|
+
)
|
|
19
|
+
) {
|
|
20
|
+
return applyFormula(switchCase?.formula, ctx, ['cases', i, 'formula'])
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return applyFormula(formula.default, ctx, ['default'])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const applyEvaluateAllSwitchFormula = (
|
|
27
|
+
formula: SwitchOperation,
|
|
28
|
+
ctx: FormulaContext,
|
|
29
|
+
) => {
|
|
30
|
+
// Evaluate all cases and the default, but only returns the first matching case or the default
|
|
31
|
+
let switchResult: { match: true; value: any } | null = null
|
|
32
|
+
for (let i = 0; i < (formula.cases ?? []).length; i++) {
|
|
33
|
+
const switchCase = (formula.cases ?? [])[i]
|
|
34
|
+
const conditionValue = applyFormula(switchCase?.condition, ctx, [
|
|
35
|
+
'cases',
|
|
36
|
+
i,
|
|
37
|
+
'condition',
|
|
38
|
+
])
|
|
39
|
+
const formulaValue = applyFormula(switchCase?.formula, ctx, [
|
|
40
|
+
'cases',
|
|
41
|
+
i,
|
|
42
|
+
'formula',
|
|
43
|
+
])
|
|
44
|
+
if (toBoolean(conditionValue) && switchResult === null) {
|
|
45
|
+
switchResult = { match: true, value: formulaValue }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const defaultValue = applyFormula(formula.default, ctx, ['default'])
|
|
49
|
+
if (switchResult !== null) {
|
|
50
|
+
return switchResult.value
|
|
51
|
+
} else {
|
|
52
|
+
return defaultValue
|
|
53
|
+
}
|
|
54
|
+
}
|