@rippling/rippling-sdk 0.2.0-alpha.108 → 0.2.0-alpha.109
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/client.d.mts +3 -0
- package/client.d.mts.map +1 -1
- package/client.d.ts +3 -0
- package/client.d.ts.map +1 -1
- package/client.js +3 -0
- package/client.js.map +1 -1
- package/client.mjs +3 -0
- package/client.mjs.map +1 -1
- package/lib/charts/builders.d.mts +18 -19
- package/lib/charts/builders.d.mts.map +1 -1
- package/lib/charts/builders.d.ts +18 -19
- package/lib/charts/builders.d.ts.map +1 -1
- package/lib/charts/builders.js +35 -28
- package/lib/charts/builders.js.map +1 -1
- package/lib/charts/builders.mjs +35 -28
- package/lib/charts/builders.mjs.map +1 -1
- package/lib/charts/describe.d.mts +6 -7
- package/lib/charts/describe.d.mts.map +1 -1
- package/lib/charts/describe.d.ts +6 -7
- package/lib/charts/describe.d.ts.map +1 -1
- package/lib/charts/describe.js +26 -27
- package/lib/charts/describe.js.map +1 -1
- package/lib/charts/describe.mjs +27 -28
- package/lib/charts/describe.mjs.map +1 -1
- package/lib/charts/errors.d.mts +2 -6
- package/lib/charts/errors.d.mts.map +1 -1
- package/lib/charts/errors.d.ts +2 -6
- package/lib/charts/errors.d.ts.map +1 -1
- package/lib/charts/errors.js +2 -6
- package/lib/charts/errors.js.map +1 -1
- package/lib/charts/errors.mjs +2 -6
- package/lib/charts/errors.mjs.map +1 -1
- package/lib/charts/evaluate.d.mts +5 -7
- package/lib/charts/evaluate.d.mts.map +1 -1
- package/lib/charts/evaluate.d.ts +5 -7
- package/lib/charts/evaluate.d.ts.map +1 -1
- package/lib/charts/evaluate.js +21 -35
- package/lib/charts/evaluate.js.map +1 -1
- package/lib/charts/evaluate.mjs +22 -36
- package/lib/charts/evaluate.mjs.map +1 -1
- package/lib/charts/schema.d.mts +8 -97
- package/lib/charts/schema.d.mts.map +1 -1
- package/lib/charts/schema.d.ts +8 -97
- package/lib/charts/schema.d.ts.map +1 -1
- package/lib/charts/schema.js +186 -65
- package/lib/charts/schema.js.map +1 -1
- package/lib/charts/schema.mjs +185 -65
- package/lib/charts/schema.mjs.map +1 -1
- package/lib/charts/types.d.mts +40 -25
- package/lib/charts/types.d.mts.map +1 -1
- package/lib/charts/types.d.ts +40 -25
- package/lib/charts/types.d.ts.map +1 -1
- package/lib/charts/types.js +8 -10
- package/lib/charts/types.js.map +1 -1
- package/lib/charts/types.mjs +8 -10
- package/lib/charts/types.mjs.map +1 -1
- package/lib/charts/visualization-grammar.d.mts +66 -0
- package/lib/charts/visualization-grammar.d.mts.map +1 -0
- package/lib/charts/visualization-grammar.d.ts +66 -0
- package/lib/charts/visualization-grammar.d.ts.map +1 -0
- package/lib/charts/visualization-grammar.js +70 -0
- package/lib/charts/visualization-grammar.js.map +1 -0
- package/lib/charts/visualization-grammar.mjs +67 -0
- package/lib/charts/visualization-grammar.mjs.map +1 -0
- package/lib/charts/vocabulary.d.mts +7 -0
- package/lib/charts/vocabulary.d.mts.map +1 -0
- package/lib/charts/vocabulary.d.ts +7 -0
- package/lib/charts/vocabulary.d.ts.map +1 -0
- package/lib/charts/vocabulary.js +10 -0
- package/lib/charts/vocabulary.js.map +1 -0
- package/lib/charts/vocabulary.mjs +7 -0
- package/lib/charts/vocabulary.mjs.map +1 -0
- package/lib/charts.d.mts +3 -3
- package/lib/charts.d.mts.map +1 -1
- package/lib/charts.d.ts +3 -3
- package/lib/charts.d.ts.map +1 -1
- package/lib/charts.js +2 -1
- package/lib/charts.js.map +1 -1
- package/lib/charts.mjs +1 -1
- package/lib/charts.mjs.map +1 -1
- package/package.json +4 -2
- package/resources/index.d.mts +1 -0
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +1 -0
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +4 -2
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +1 -0
- package/resources/index.mjs.map +1 -1
- package/resources/reimbursements.d.mts +1 -1
- package/resources/reimbursements.d.ts +1 -1
- package/resources/reimbursements.js +1 -1
- package/resources/reimbursements.mjs +1 -1
- package/resources/spend-categories.d.mts +140 -0
- package/resources/spend-categories.d.mts.map +1 -0
- package/resources/spend-categories.d.ts +140 -0
- package/resources/spend-categories.d.ts.map +1 -0
- package/resources/spend-categories.js +83 -0
- package/resources/spend-categories.js.map +1 -0
- package/resources/spend-categories.mjs +79 -0
- package/resources/spend-categories.mjs.map +1 -0
- package/src/client.ts +17 -0
- package/src/lib/charts/README.md +35 -28
- package/src/lib/charts/agent-reference.md +73 -0
- package/src/lib/charts/builders.ts +51 -35
- package/src/lib/charts/capabilities.json +96 -0
- package/src/lib/charts/describe.ts +31 -29
- package/src/lib/charts/errors.ts +2 -6
- package/src/lib/charts/evaluate.ts +25 -38
- package/src/lib/charts/schema.ts +232 -66
- package/src/lib/charts/types.ts +42 -31
- package/src/lib/charts/visualization-grammar.ts +67 -0
- package/src/lib/charts/vocabulary.ts +7 -0
- package/src/lib/charts.ts +8 -1
- package/src/resources/index.ts +7 -0
- package/src/resources/reimbursements.ts +1 -1
- package/src/resources/spend-categories.ts +175 -0
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
- package/lib/charts/guards.d.mts +0 -15
- package/lib/charts/guards.d.mts.map +0 -1
- package/lib/charts/guards.d.ts +0 -15
- package/lib/charts/guards.d.ts.map +0 -1
- package/lib/charts/guards.js +0 -38
- package/lib/charts/guards.js.map +0 -1
- package/lib/charts/guards.mjs +0 -34
- package/lib/charts/guards.mjs.map +0 -1
- package/src/lib/charts/guards.ts +0 -42
|
@@ -1,37 +1,48 @@
|
|
|
1
1
|
import { ChartRuleError } from './errors';
|
|
2
2
|
import {
|
|
3
|
-
type
|
|
4
|
-
type
|
|
3
|
+
type ConditionNode,
|
|
4
|
+
type ComparisonOperandNode,
|
|
5
|
+
type EqualityComparisonNode,
|
|
5
6
|
type IfElseNode,
|
|
6
7
|
type LiteralNode,
|
|
8
|
+
type NumericComparisonNode,
|
|
9
|
+
type NumericOperandNode,
|
|
7
10
|
type RefNode,
|
|
8
|
-
type RuleNode,
|
|
9
11
|
type RulePlaceholder,
|
|
10
12
|
type Scalar,
|
|
11
13
|
} from './types';
|
|
12
14
|
|
|
13
|
-
/** A
|
|
14
|
-
export type Operand =
|
|
15
|
+
/** A non-recursive comparison operand: `value`, a literal node, or a scalar. */
|
|
16
|
+
export type Operand = ComparisonOperandNode | Scalar;
|
|
17
|
+
export type NumericOperand = NumericOperandNode | number;
|
|
15
18
|
|
|
16
|
-
function
|
|
17
|
-
return
|
|
19
|
+
function hasOwn(value: object, key: PropertyKey): boolean {
|
|
20
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isOperandNode(operand: Operand): operand is ComparisonOperandNode {
|
|
24
|
+
return typeof operand === 'object' && operand !== null && hasOwn(operand, 'kind');
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
/** True when a value is a `{$rule}` placeholder — the marker the builders emit. */
|
|
21
28
|
export function isRulePlaceholder(value: unknown): value is RulePlaceholder {
|
|
22
|
-
return typeof value === 'object' && value !== null && '$rule'
|
|
29
|
+
return typeof value === 'object' && value !== null && hasOwn(value, '$rule');
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/** Wrap a bare scalar as a literal node. */
|
|
26
|
-
export function literal(value:
|
|
33
|
+
export function literal<const TValue extends Scalar>(value: TValue): LiteralNode<TValue> {
|
|
34
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
35
|
+
throw new ChartRuleError('literal(value) requires a finite number');
|
|
36
|
+
}
|
|
27
37
|
return { kind: 'literal', value };
|
|
28
38
|
}
|
|
29
39
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
function toOperandNode(operand: Operand): ComparisonOperandNode {
|
|
41
|
+
return isOperandNode(operand) ? operand : literal(operand);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toNumericOperandNode(operand: NumericOperand): NumericOperandNode {
|
|
45
|
+
return typeof operand === 'number' ? literal(operand) : operand;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
/**
|
|
@@ -40,36 +51,43 @@ function toNode(operand: Operand): RuleNode {
|
|
|
40
51
|
*/
|
|
41
52
|
export const value: RefNode = Object.freeze({ kind: 'value' });
|
|
42
53
|
|
|
43
|
-
/* Comparisons — return a condition. */
|
|
44
|
-
function
|
|
45
|
-
|
|
54
|
+
/* Comparisons — return a condition with no nested expressions. */
|
|
55
|
+
function equalityComparison(
|
|
56
|
+
kind: EqualityComparisonNode['kind'],
|
|
57
|
+
): (left: Operand, right: Operand) => EqualityComparisonNode {
|
|
58
|
+
return (left, right) => ({ kind, left: toOperandNode(left), right: toOperandNode(right) });
|
|
59
|
+
}
|
|
60
|
+
function numericComparison(
|
|
61
|
+
kind: NumericComparisonNode['kind'],
|
|
62
|
+
): (left: NumericOperand, right: NumericOperand) => NumericComparisonNode {
|
|
63
|
+
return (left, right) => ({ kind, left: toNumericOperandNode(left), right: toNumericOperandNode(right) });
|
|
46
64
|
}
|
|
47
|
-
export const equals =
|
|
48
|
-
export const isNot =
|
|
49
|
-
export const lessThan =
|
|
50
|
-
export const greaterThan =
|
|
51
|
-
export const lessOrEqual =
|
|
52
|
-
export const greaterOrEqual =
|
|
65
|
+
export const equals = equalityComparison('equals');
|
|
66
|
+
export const isNot = equalityComparison('isNot');
|
|
67
|
+
export const lessThan = numericComparison('lessThan');
|
|
68
|
+
export const greaterThan = numericComparison('greaterThan');
|
|
69
|
+
export const lessOrEqual = numericComparison('lessOrEqual');
|
|
70
|
+
export const greaterOrEqual = numericComparison('greaterOrEqual');
|
|
53
71
|
|
|
54
72
|
/**
|
|
55
|
-
* A conditional
|
|
56
|
-
* Channel-agnostic — wrap it in a channel builder (`color`) to target something.
|
|
73
|
+
* A conditional colour: pick `ifTrue` when the condition holds, else `ifFalse`.
|
|
57
74
|
*
|
|
58
75
|
* when(lessThan(value, 100), '#E24B4A', '#378ADD')
|
|
59
76
|
* // → '#E24B4A' when this point's value is under 100, '#378ADD' otherwise
|
|
60
77
|
*
|
|
61
78
|
* `ifFalse` is optional and defaults to `null` — for a colour, that means no
|
|
62
|
-
* override (the chart's default is kept).
|
|
79
|
+
* override (the chart's default is kept). Branches are colour literals only;
|
|
80
|
+
* nested rules are intentionally unsupported in v1.
|
|
63
81
|
*/
|
|
64
|
-
export function when(condition:
|
|
82
|
+
export function when(condition: ConditionNode, ifTrue: string, ifFalse: string | null = null): IfElseNode {
|
|
65
83
|
if (ifTrue === undefined) {
|
|
66
84
|
throw new ChartRuleError('when(condition, ifTrue, ifFalse?) needs a value for the true case');
|
|
67
85
|
}
|
|
68
86
|
return {
|
|
69
87
|
kind: 'ifElse',
|
|
70
88
|
cond: condition,
|
|
71
|
-
then:
|
|
72
|
-
else:
|
|
89
|
+
then: literal(ifTrue),
|
|
90
|
+
else: literal(ifFalse),
|
|
73
91
|
};
|
|
74
92
|
}
|
|
75
93
|
|
|
@@ -80,11 +98,9 @@ export function when(condition: RuleNode, ifTrue: Operand, ifFalse?: Operand): I
|
|
|
80
98
|
* color(when(lessThan(value, 100), '#E24B4A', '#378ADD'))
|
|
81
99
|
* // → red when this point's value is under 100, blue otherwise
|
|
82
100
|
*
|
|
83
|
-
* Takes
|
|
84
|
-
*
|
|
85
|
-
* boolean, which is not a colour. `evaluate` enforces the same at runtime for a
|
|
86
|
-
* deserialized rule. Returns a `{$rule}` placeholder tagged `channel: 'color'`.
|
|
101
|
+
* Takes the flat conditional value produced by `when(...)`. Returns a `{$rule}`
|
|
102
|
+
* placeholder tagged `channel: 'color'`.
|
|
87
103
|
*/
|
|
88
|
-
export function color(rule: IfElseNode
|
|
89
|
-
return { $rule:
|
|
104
|
+
export function color(rule: IfElseNode): RulePlaceholder {
|
|
105
|
+
return { $rule: rule, channel: 'color' };
|
|
90
106
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contractVersion": 1,
|
|
3
|
+
"moduleSpecifier": "npm:@rippling/rippling-sdk/lib/charts",
|
|
4
|
+
"builders": {
|
|
5
|
+
"value": {
|
|
6
|
+
"kind": "input",
|
|
7
|
+
"returns": "number"
|
|
8
|
+
},
|
|
9
|
+
"literal": {
|
|
10
|
+
"kind": "literal",
|
|
11
|
+
"parameters": [
|
|
12
|
+
"scalar"
|
|
13
|
+
],
|
|
14
|
+
"returns": "value"
|
|
15
|
+
},
|
|
16
|
+
"equals": {
|
|
17
|
+
"kind": "comparison",
|
|
18
|
+
"parameters": [
|
|
19
|
+
"value",
|
|
20
|
+
"value"
|
|
21
|
+
],
|
|
22
|
+
"returns": "condition"
|
|
23
|
+
},
|
|
24
|
+
"isNot": {
|
|
25
|
+
"kind": "comparison",
|
|
26
|
+
"parameters": [
|
|
27
|
+
"value",
|
|
28
|
+
"value"
|
|
29
|
+
],
|
|
30
|
+
"returns": "condition"
|
|
31
|
+
},
|
|
32
|
+
"lessThan": {
|
|
33
|
+
"kind": "comparison",
|
|
34
|
+
"parameters": [
|
|
35
|
+
"value",
|
|
36
|
+
"value"
|
|
37
|
+
],
|
|
38
|
+
"returns": "condition"
|
|
39
|
+
},
|
|
40
|
+
"greaterThan": {
|
|
41
|
+
"kind": "comparison",
|
|
42
|
+
"parameters": [
|
|
43
|
+
"value",
|
|
44
|
+
"value"
|
|
45
|
+
],
|
|
46
|
+
"returns": "condition"
|
|
47
|
+
},
|
|
48
|
+
"lessOrEqual": {
|
|
49
|
+
"kind": "comparison",
|
|
50
|
+
"parameters": [
|
|
51
|
+
"value",
|
|
52
|
+
"value"
|
|
53
|
+
],
|
|
54
|
+
"returns": "condition"
|
|
55
|
+
},
|
|
56
|
+
"greaterOrEqual": {
|
|
57
|
+
"kind": "comparison",
|
|
58
|
+
"parameters": [
|
|
59
|
+
"value",
|
|
60
|
+
"value"
|
|
61
|
+
],
|
|
62
|
+
"returns": "condition"
|
|
63
|
+
},
|
|
64
|
+
"when": {
|
|
65
|
+
"kind": "conditional",
|
|
66
|
+
"parameters": [
|
|
67
|
+
"condition",
|
|
68
|
+
"color",
|
|
69
|
+
"color?"
|
|
70
|
+
],
|
|
71
|
+
"returns": "conditional-color"
|
|
72
|
+
},
|
|
73
|
+
"color": {
|
|
74
|
+
"kind": "channel",
|
|
75
|
+
"parameters": [
|
|
76
|
+
"conditional-color"
|
|
77
|
+
],
|
|
78
|
+
"returns": "color-rule",
|
|
79
|
+
"placeholderChannel": "color"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"supportedChannels": [
|
|
83
|
+
"color"
|
|
84
|
+
],
|
|
85
|
+
"supportedNodeKinds": [
|
|
86
|
+
"literal",
|
|
87
|
+
"value",
|
|
88
|
+
"equals",
|
|
89
|
+
"isNot",
|
|
90
|
+
"lessThan",
|
|
91
|
+
"greaterThan",
|
|
92
|
+
"lessOrEqual",
|
|
93
|
+
"greaterOrEqual",
|
|
94
|
+
"ifElse"
|
|
95
|
+
]
|
|
96
|
+
}
|
|
@@ -1,56 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { MAX_RULE_DEPTH, type Control, type Rule, type RuleNode } from './types';
|
|
1
|
+
import { UnknownNodeError } from './errors';
|
|
2
|
+
import { parseChartRule } from './schema';
|
|
3
|
+
import { type ComparisonOperandNode, type Control, type RuleNode } from './types';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Turn a rule into the shape the settings panel renders: a label plus its
|
|
8
|
-
* editable parts. Accepts a `{$rule}` placeholder or a bare
|
|
9
|
-
*
|
|
7
|
+
* editable parts. Accepts a `{$rule}` placeholder or a bare node. Reads the same
|
|
8
|
+
* value `evaluate` runs — and validates it the same way — so a malformed or
|
|
10
9
|
* unknown node throws a `ChartRuleError` here too, never a native `TypeError`.
|
|
11
|
-
* The
|
|
12
|
-
*
|
|
13
|
-
* A tree nested past `MAX_RULE_DEPTH` throws `RuleDepthError`.
|
|
10
|
+
* The Valibot parser rejects malformed or nested values before this flat
|
|
11
|
+
* formatter reads them.
|
|
14
12
|
*/
|
|
15
|
-
export function describe(rule:
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
export function describe(rule: unknown): Control {
|
|
14
|
+
const parsedRule = parseChartRule(rule);
|
|
15
|
+
if ('$rule' in parsedRule) {
|
|
16
|
+
return visit(parsedRule.$rule);
|
|
18
17
|
}
|
|
19
|
-
return visit(
|
|
18
|
+
return visit(parsedRule);
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
function visit(node: RuleNode
|
|
23
|
-
if (depth > MAX_RULE_DEPTH) {
|
|
24
|
-
throw new RuleDepthError(MAX_RULE_DEPTH);
|
|
25
|
-
}
|
|
26
|
-
assertRuleNode(node);
|
|
27
|
-
const next = depth + 1;
|
|
21
|
+
function visit(node: RuleNode): Control {
|
|
28
22
|
switch (node.kind) {
|
|
29
23
|
case 'value':
|
|
30
24
|
return { role: 'ref', label: "this row's value" };
|
|
31
25
|
case 'literal': {
|
|
32
|
-
const scalar =
|
|
26
|
+
const scalar = node.value;
|
|
33
27
|
return { role: 'literal', label: String(scalar), value: scalar };
|
|
34
28
|
}
|
|
35
29
|
|
|
36
30
|
case 'equals':
|
|
37
|
-
return comparison('is', node.left, node.right
|
|
31
|
+
return comparison('is', node.left, node.right);
|
|
38
32
|
case 'isNot':
|
|
39
|
-
return comparison('is not', node.left, node.right
|
|
33
|
+
return comparison('is not', node.left, node.right);
|
|
40
34
|
case 'lessThan':
|
|
41
|
-
return comparison('is under', node.left, node.right
|
|
35
|
+
return comparison('is under', node.left, node.right);
|
|
42
36
|
case 'greaterThan':
|
|
43
|
-
return comparison('is over', node.left, node.right
|
|
37
|
+
return comparison('is over', node.left, node.right);
|
|
44
38
|
case 'lessOrEqual':
|
|
45
|
-
return comparison('is at most', node.left, node.right
|
|
39
|
+
return comparison('is at most', node.left, node.right);
|
|
46
40
|
case 'greaterOrEqual':
|
|
47
|
-
return comparison('is at least', node.left, node.right
|
|
41
|
+
return comparison('is at least', node.left, node.right);
|
|
48
42
|
|
|
49
43
|
case 'ifElse':
|
|
50
44
|
return {
|
|
51
45
|
role: 'rule',
|
|
52
46
|
label: 'when',
|
|
53
|
-
parts: [visit(node.cond
|
|
47
|
+
parts: [visit(node.cond), literalControl(node.then.value), literalControl(node.else.value)],
|
|
54
48
|
};
|
|
55
49
|
|
|
56
50
|
default:
|
|
@@ -58,8 +52,16 @@ function visit(node: RuleNode, depth: number): Control {
|
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
function comparison(label: string, left:
|
|
62
|
-
return { role: 'comparison', label, parts: [
|
|
55
|
+
function comparison(label: string, left: ComparisonOperandNode, right: ComparisonOperandNode): Control {
|
|
56
|
+
return { role: 'comparison', label, parts: [operandControl(left), operandControl(right)] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function operandControl(node: ComparisonOperandNode): Control {
|
|
60
|
+
return node.kind === 'value' ? { role: 'ref', label: "this row's value" } : literalControl(node.value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function literalControl(value: string | number | boolean | null): Control {
|
|
64
|
+
return { role: 'literal', label: String(value), value };
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function unreachable(node: never): never {
|
package/src/lib/charts/errors.ts
CHANGED
|
@@ -21,7 +21,7 @@ export class RuleTypeError extends ChartRuleError {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* A structurally invalid node in a deserialized
|
|
24
|
+
* A structurally invalid node in a deserialized value: not an object, no string
|
|
25
25
|
* `kind`, or a `literal` without a scalar `value`. Distinct from
|
|
26
26
|
* `UnknownNodeError` (a well-formed node whose `kind` is simply not in the
|
|
27
27
|
* vocabulary — the intentional escape hatch). Like the others it is a
|
|
@@ -35,11 +35,7 @@ export class MalformedRuleError extends ChartRuleError {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
* A rule nested deeper than the evaluator will walk. Guards against a
|
|
40
|
-
* stack-overflowing tree from untrusted, deserialized input; like the others it
|
|
41
|
-
* is a `ChartRuleError`, so the caller's escape hatch catches it.
|
|
42
|
-
*/
|
|
38
|
+
/** @deprecated v1 is non-recursive. Retained for source compatibility. */
|
|
43
39
|
export class RuleDepthError extends ChartRuleError {
|
|
44
40
|
constructor(maxDepth: number) {
|
|
45
41
|
super(`chart rule nested deeper than ${maxDepth} levels`);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { assertRuleNode, literalValue } from './guards';
|
|
1
|
+
import { MalformedRuleError, RuleTypeError, UnknownNodeError } from './errors';
|
|
2
|
+
import { parseChartRule } from './schema';
|
|
4
3
|
import {
|
|
5
|
-
|
|
6
|
-
type
|
|
4
|
+
type ComparisonOperandNode,
|
|
5
|
+
type NumericOperandNode,
|
|
7
6
|
type RuleChannel,
|
|
8
7
|
type RuleContext,
|
|
9
8
|
type RuleNode,
|
|
@@ -12,51 +11,45 @@ import {
|
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Resolve a rule to a value against `ctx`. Accepts a `{$rule}` placeholder or a
|
|
15
|
-
* bare
|
|
14
|
+
* bare node. A malformed or unknown node throws a `ChartRuleError` (never a
|
|
16
15
|
* native `TypeError`), so a caller reading an untrusted, deserialized rule can
|
|
17
16
|
* catch every failure with one type. When the rule is a colour placeholder, the
|
|
18
17
|
* result is checked to be a colour (`string | null`) — a rule that resolves to a
|
|
19
18
|
* number or boolean is a `RuleTypeError`, not something the renderer paints.
|
|
20
19
|
*
|
|
21
|
-
* Pure: the only inputs are the
|
|
22
|
-
*
|
|
23
|
-
* and identical across the renderer, the panel readout, and a test. A tree
|
|
24
|
-
* nested past `MAX_RULE_DEPTH` throws `RuleDepthError` rather than overflowing.
|
|
20
|
+
* Pure and non-recursive: the only inputs are the validated flat rule and the
|
|
21
|
+
* context. No clock, randomness, callbacks, or I/O.
|
|
25
22
|
*/
|
|
26
|
-
export function evaluate(rule:
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
export function evaluate(rule: unknown, ctx: RuleContext): RuleValue {
|
|
24
|
+
const parsedRule = parseChartRule(rule);
|
|
25
|
+
if ('$rule' in parsedRule) {
|
|
26
|
+
return coerceForChannel(parsedRule.channel, visit(parsedRule.$rule, ctx));
|
|
29
27
|
}
|
|
30
|
-
return visit(
|
|
28
|
+
return visit(parsedRule, ctx);
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
function visit(node: RuleNode, ctx: RuleContext
|
|
34
|
-
if (depth > MAX_RULE_DEPTH) {
|
|
35
|
-
throw new RuleDepthError(MAX_RULE_DEPTH);
|
|
36
|
-
}
|
|
37
|
-
assertRuleNode(node);
|
|
38
|
-
const next = depth + 1;
|
|
31
|
+
function visit(node: RuleNode, ctx: RuleContext): RuleValue {
|
|
39
32
|
switch (node.kind) {
|
|
40
33
|
case 'value':
|
|
41
34
|
return ctx.value;
|
|
42
35
|
case 'literal':
|
|
43
|
-
return
|
|
36
|
+
return node.value;
|
|
44
37
|
|
|
45
38
|
case 'equals':
|
|
46
|
-
return
|
|
39
|
+
return operand(node.left, ctx) === operand(node.right, ctx);
|
|
47
40
|
case 'isNot':
|
|
48
|
-
return
|
|
41
|
+
return operand(node.left, ctx) !== operand(node.right, ctx);
|
|
49
42
|
case 'lessThan':
|
|
50
|
-
return
|
|
43
|
+
return numericOperand(node.left, ctx) < numericOperand(node.right, ctx);
|
|
51
44
|
case 'greaterThan':
|
|
52
|
-
return
|
|
45
|
+
return numericOperand(node.left, ctx) > numericOperand(node.right, ctx);
|
|
53
46
|
case 'lessOrEqual':
|
|
54
|
-
return
|
|
47
|
+
return numericOperand(node.left, ctx) <= numericOperand(node.right, ctx);
|
|
55
48
|
case 'greaterOrEqual':
|
|
56
|
-
return
|
|
49
|
+
return numericOperand(node.left, ctx) >= numericOperand(node.right, ctx);
|
|
57
50
|
|
|
58
51
|
case 'ifElse':
|
|
59
|
-
return
|
|
52
|
+
return visit(node.cond, ctx) ? node.then.value : node.else.value;
|
|
60
53
|
|
|
61
54
|
default:
|
|
62
55
|
return unreachable(node);
|
|
@@ -67,18 +60,12 @@ function visit(node: RuleNode, ctx: RuleContext, depth: number): RuleValue {
|
|
|
67
60
|
/* Coercions — narrow a RuleValue, or throw a typed error. */
|
|
68
61
|
/* -------------------------------------------------------------------------- */
|
|
69
62
|
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
|
-
throw new RuleTypeError(`expected a number, got ${typeName(value)}`);
|
|
63
|
+
function operand(node: ComparisonOperandNode, ctx: RuleContext): RuleValue {
|
|
64
|
+
return node.kind === 'value' ? ctx.value : node.value;
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
return value;
|
|
80
|
-
}
|
|
81
|
-
throw new RuleTypeError(`expected a boolean, got ${typeName(value)}`);
|
|
67
|
+
function numericOperand(node: NumericOperandNode, ctx: RuleContext): number {
|
|
68
|
+
return node.kind === 'value' ? ctx.value : node.value;
|
|
82
69
|
}
|
|
83
70
|
|
|
84
71
|
/** A colour channel must resolve to a colour string, or null for "no override". */
|