@xemahq/dsl 0.1.1
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/LICENSE +201 -0
- package/dist/deliverable-spec/index.d.ts +3 -0
- package/dist/deliverable-spec/index.d.ts.map +1 -0
- package/dist/deliverable-spec/index.js +19 -0
- package/dist/deliverable-spec/index.js.map +1 -0
- package/dist/deliverable-spec/lib/schema.d.ts +151 -0
- package/dist/deliverable-spec/lib/schema.d.ts.map +1 -0
- package/dist/deliverable-spec/lib/schema.js +139 -0
- package/dist/deliverable-spec/lib/schema.js.map +1 -0
- package/dist/deliverable-spec/lib/types.d.ts +8 -0
- package/dist/deliverable-spec/lib/types.d.ts.map +1 -0
- package/dist/deliverable-spec/lib/types.js +3 -0
- package/dist/deliverable-spec/lib/types.js.map +1 -0
- package/dist/payload-codec/index.d.ts +8 -0
- package/dist/payload-codec/index.d.ts.map +1 -0
- package/dist/payload-codec/index.js +27 -0
- package/dist/payload-codec/index.js.map +1 -0
- package/dist/payload-codec/lib/blob-store.d.ts +37 -0
- package/dist/payload-codec/lib/blob-store.d.ts.map +1 -0
- package/dist/payload-codec/lib/blob-store.js +0 -0
- package/dist/payload-codec/lib/blob-store.js.map +1 -0
- package/dist/payload-codec/lib/codec-context.d.ts +6 -0
- package/dist/payload-codec/lib/codec-context.d.ts.map +1 -0
- package/dist/payload-codec/lib/codec-context.js +16 -0
- package/dist/payload-codec/lib/codec-context.js.map +1 -0
- package/dist/payload-codec/lib/codec.d.ts +51 -0
- package/dist/payload-codec/lib/codec.d.ts.map +1 -0
- package/dist/payload-codec/lib/codec.js +330 -0
- package/dist/payload-codec/lib/codec.js.map +1 -0
- package/dist/payload-codec/lib/enums.d.ts +18 -0
- package/dist/payload-codec/lib/enums.d.ts.map +1 -0
- package/dist/payload-codec/lib/enums.js +23 -0
- package/dist/payload-codec/lib/enums.js.map +1 -0
- package/dist/payload-codec/lib/errors.d.ts +18 -0
- package/dist/payload-codec/lib/errors.d.ts.map +1 -0
- package/dist/payload-codec/lib/errors.js +39 -0
- package/dist/payload-codec/lib/errors.js.map +1 -0
- package/dist/payload-codec/lib/http-blob-store.d.ts +21 -0
- package/dist/payload-codec/lib/http-blob-store.d.ts.map +1 -0
- package/dist/payload-codec/lib/http-blob-store.js +139 -0
- package/dist/payload-codec/lib/http-blob-store.js.map +1 -0
- package/dist/payload-codec/lib/lru-cache.d.ts +12 -0
- package/dist/payload-codec/lib/lru-cache.d.ts.map +1 -0
- package/dist/payload-codec/lib/lru-cache.js +59 -0
- package/dist/payload-codec/lib/lru-cache.js.map +1 -0
- package/dist/schema/action.schema.json +181 -0
- package/dist/schema/reusable-workflow.schema.json +46 -0
- package/dist/schema/workflow.schema.json +373 -0
- package/dist/workflow/index.d.ts +14 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +49 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/lib/action-input-validator.d.ts +10 -0
- package/dist/workflow/lib/action-input-validator.d.ts.map +1 -0
- package/dist/workflow/lib/action-input-validator.js +69 -0
- package/dist/workflow/lib/action-input-validator.js.map +1 -0
- package/dist/workflow/lib/compiler/action-shape.d.ts +5 -0
- package/dist/workflow/lib/compiler/action-shape.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/action-shape.js +43 -0
- package/dist/workflow/lib/compiler/action-shape.js.map +1 -0
- package/dist/workflow/lib/compiler/canonical-json.d.ts +3 -0
- package/dist/workflow/lib/compiler/canonical-json.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/canonical-json.js +45 -0
- package/dist/workflow/lib/compiler/canonical-json.js.map +1 -0
- package/dist/workflow/lib/compiler/compile.d.ts +4 -0
- package/dist/workflow/lib/compiler/compile.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/compile.js +794 -0
- package/dist/workflow/lib/compiler/compile.js.map +1 -0
- package/dist/workflow/lib/compiler/concurrency.d.ts +5 -0
- package/dist/workflow/lib/compiler/concurrency.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/concurrency.js +104 -0
- package/dist/workflow/lib/compiler/concurrency.js.map +1 -0
- package/dist/workflow/lib/compiler/dag.d.ts +10 -0
- package/dist/workflow/lib/compiler/dag.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/dag.js +74 -0
- package/dist/workflow/lib/compiler/dag.js.map +1 -0
- package/dist/workflow/lib/compiler/index.d.ts +6 -0
- package/dist/workflow/lib/compiler/index.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/index.js +14 -0
- package/dist/workflow/lib/compiler/index.js.map +1 -0
- package/dist/workflow/lib/compiler/inputs.d.ts +4 -0
- package/dist/workflow/lib/compiler/inputs.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/inputs.js +108 -0
- package/dist/workflow/lib/compiler/inputs.js.map +1 -0
- package/dist/workflow/lib/compiler/installation-resource-validator.d.ts +9 -0
- package/dist/workflow/lib/compiler/installation-resource-validator.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/installation-resource-validator.js +76 -0
- package/dist/workflow/lib/compiler/installation-resource-validator.js.map +1 -0
- package/dist/workflow/lib/compiler/manifest-source.d.ts +4 -0
- package/dist/workflow/lib/compiler/manifest-source.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/manifest-source.js +100 -0
- package/dist/workflow/lib/compiler/manifest-source.js.map +1 -0
- package/dist/workflow/lib/compiler/matrix.d.ts +4 -0
- package/dist/workflow/lib/compiler/matrix.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/matrix.js +76 -0
- package/dist/workflow/lib/compiler/matrix.js.map +1 -0
- package/dist/workflow/lib/compiler/mount-plan.d.ts +4 -0
- package/dist/workflow/lib/compiler/mount-plan.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/mount-plan.js +96 -0
- package/dist/workflow/lib/compiler/mount-plan.js.map +1 -0
- package/dist/workflow/lib/compiler/payload-reach-in.d.ts +14 -0
- package/dist/workflow/lib/compiler/payload-reach-in.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/payload-reach-in.js +273 -0
- package/dist/workflow/lib/compiler/payload-reach-in.js.map +1 -0
- package/dist/workflow/lib/compiler/permissions.d.ts +6 -0
- package/dist/workflow/lib/compiler/permissions.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/permissions.js +43 -0
- package/dist/workflow/lib/compiler/permissions.js.map +1 -0
- package/dist/workflow/lib/compiler/retry-timeout.d.ts +6 -0
- package/dist/workflow/lib/compiler/retry-timeout.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/retry-timeout.js +64 -0
- package/dist/workflow/lib/compiler/retry-timeout.js.map +1 -0
- package/dist/workflow/lib/compiler/review-step.d.ts +18 -0
- package/dist/workflow/lib/compiler/review-step.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/review-step.js +247 -0
- package/dist/workflow/lib/compiler/review-step.js.map +1 -0
- package/dist/workflow/lib/compiler/types.d.ts +42 -0
- package/dist/workflow/lib/compiler/types.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/types.js +3 -0
- package/dist/workflow/lib/compiler/types.js.map +1 -0
- package/dist/workflow/lib/compiler/variable-requirements.d.ts +5 -0
- package/dist/workflow/lib/compiler/variable-requirements.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/variable-requirements.js +119 -0
- package/dist/workflow/lib/compiler/variable-requirements.js.map +1 -0
- package/dist/workflow/lib/deliverable-spec-keys.d.ts +3 -0
- package/dist/workflow/lib/deliverable-spec-keys.d.ts.map +1 -0
- package/dist/workflow/lib/deliverable-spec-keys.js +90 -0
- package/dist/workflow/lib/deliverable-spec-keys.js.map +1 -0
- package/dist/workflow/lib/dispatch-inputs/index.d.ts +23 -0
- package/dist/workflow/lib/dispatch-inputs/index.d.ts.map +1 -0
- package/dist/workflow/lib/dispatch-inputs/index.js +106 -0
- package/dist/workflow/lib/dispatch-inputs/index.js.map +1 -0
- package/dist/workflow/lib/dispatch-inputs/to-json-schema.d.ts +3 -0
- package/dist/workflow/lib/dispatch-inputs/to-json-schema.d.ts.map +1 -0
- package/dist/workflow/lib/dispatch-inputs/to-json-schema.js +43 -0
- package/dist/workflow/lib/dispatch-inputs/to-json-schema.js.map +1 -0
- package/dist/workflow/lib/duration.d.ts +2 -0
- package/dist/workflow/lib/duration.d.ts.map +1 -0
- package/dist/workflow/lib/duration.js +26 -0
- package/dist/workflow/lib/duration.js.map +1 -0
- package/dist/workflow/lib/errors.d.ts +9 -0
- package/dist/workflow/lib/errors.d.ts.map +1 -0
- package/dist/workflow/lib/errors.js +28 -0
- package/dist/workflow/lib/errors.js.map +1 -0
- package/dist/workflow/lib/expression/ast.d.ts +61 -0
- package/dist/workflow/lib/expression/ast.d.ts.map +1 -0
- package/dist/workflow/lib/expression/ast.js +34 -0
- package/dist/workflow/lib/expression/ast.js.map +1 -0
- package/dist/workflow/lib/expression/context.d.ts +63 -0
- package/dist/workflow/lib/expression/context.d.ts.map +1 -0
- package/dist/workflow/lib/expression/context.js +32 -0
- package/dist/workflow/lib/expression/context.js.map +1 -0
- package/dist/workflow/lib/expression/evaluator.d.ts +5 -0
- package/dist/workflow/lib/expression/evaluator.d.ts.map +1 -0
- package/dist/workflow/lib/expression/evaluator.js +291 -0
- package/dist/workflow/lib/expression/evaluator.js.map +1 -0
- package/dist/workflow/lib/expression/index.d.ts +9 -0
- package/dist/workflow/lib/expression/index.d.ts.map +1 -0
- package/dist/workflow/lib/expression/index.js +26 -0
- package/dist/workflow/lib/expression/index.js.map +1 -0
- package/dist/workflow/lib/expression/interpolation.d.ts +9 -0
- package/dist/workflow/lib/expression/interpolation.d.ts.map +1 -0
- package/dist/workflow/lib/expression/interpolation.js +51 -0
- package/dist/workflow/lib/expression/interpolation.js.map +1 -0
- package/dist/workflow/lib/expression/parser.d.ts +4 -0
- package/dist/workflow/lib/expression/parser.d.ts.map +1 -0
- package/dist/workflow/lib/expression/parser.js +203 -0
- package/dist/workflow/lib/expression/parser.js.map +1 -0
- package/dist/workflow/lib/expression/template.d.ts +18 -0
- package/dist/workflow/lib/expression/template.d.ts.map +1 -0
- package/dist/workflow/lib/expression/template.js +63 -0
- package/dist/workflow/lib/expression/template.js.map +1 -0
- package/dist/workflow/lib/expression/tokenizer.d.ts +3 -0
- package/dist/workflow/lib/expression/tokenizer.d.ts.map +1 -0
- package/dist/workflow/lib/expression/tokenizer.js +153 -0
- package/dist/workflow/lib/expression/tokenizer.js.map +1 -0
- package/dist/workflow/lib/expression/tokens.d.ts +25 -0
- package/dist/workflow/lib/expression/tokens.d.ts.map +1 -0
- package/dist/workflow/lib/expression/tokens.js +24 -0
- package/dist/workflow/lib/expression/tokens.js.map +1 -0
- package/dist/workflow/lib/expression/walk-artifact-refs.d.ts +5 -0
- package/dist/workflow/lib/expression/walk-artifact-refs.d.ts.map +1 -0
- package/dist/workflow/lib/expression/walk-artifact-refs.js +138 -0
- package/dist/workflow/lib/expression/walk-artifact-refs.js.map +1 -0
- package/dist/workflow/lib/installation-resource-kind.d.ts +14 -0
- package/dist/workflow/lib/installation-resource-kind.d.ts.map +1 -0
- package/dist/workflow/lib/installation-resource-kind.js +59 -0
- package/dist/workflow/lib/installation-resource-kind.js.map +1 -0
- package/dist/workflow/lib/schemas-loader.d.ts +4 -0
- package/dist/workflow/lib/schemas-loader.d.ts.map +1 -0
- package/dist/workflow/lib/schemas-loader.js +36 -0
- package/dist/workflow/lib/schemas-loader.js.map +1 -0
- package/dist/workflow/lib/serializer.d.ts +3 -0
- package/dist/workflow/lib/serializer.d.ts.map +1 -0
- package/dist/workflow/lib/serializer.js +15 -0
- package/dist/workflow/lib/serializer.js.map +1 -0
- package/dist/workflow/lib/types.d.ts +179 -0
- package/dist/workflow/lib/types.d.ts.map +1 -0
- package/dist/workflow/lib/types.js +3 -0
- package/dist/workflow/lib/types.js.map +1 -0
- package/dist/workflow/lib/validate.d.ts +8 -0
- package/dist/workflow/lib/validate.d.ts.map +1 -0
- package/dist/workflow/lib/validate.js +119 -0
- package/dist/workflow/lib/validate.js.map +1 -0
- package/dist/workspace-manifest/index.d.ts +6 -0
- package/dist/workspace-manifest/index.d.ts.map +1 -0
- package/dist/workspace-manifest/index.js +22 -0
- package/dist/workspace-manifest/index.js.map +1 -0
- package/dist/workspace-manifest/lib/compile.d.ts +8 -0
- package/dist/workspace-manifest/lib/compile.d.ts.map +1 -0
- package/dist/workspace-manifest/lib/compile.js +439 -0
- package/dist/workspace-manifest/lib/compile.js.map +1 -0
- package/dist/workspace-manifest/lib/interpolate.d.ts +12 -0
- package/dist/workspace-manifest/lib/interpolate.d.ts.map +1 -0
- package/dist/workspace-manifest/lib/interpolate.js +81 -0
- package/dist/workspace-manifest/lib/interpolate.js.map +1 -0
- package/dist/workspace-manifest/lib/resolve-extends.d.ts +10 -0
- package/dist/workspace-manifest/lib/resolve-extends.d.ts.map +1 -0
- package/dist/workspace-manifest/lib/resolve-extends.js +108 -0
- package/dist/workspace-manifest/lib/resolve-extends.js.map +1 -0
- package/dist/workspace-manifest/lib/schema.d.ts +710 -0
- package/dist/workspace-manifest/lib/schema.d.ts.map +1 -0
- package/dist/workspace-manifest/lib/schema.js +355 -0
- package/dist/workspace-manifest/lib/schema.js.map +1 -0
- package/dist/workspace-manifest/lib/types.d.ts +153 -0
- package/dist/workspace-manifest/lib/types.d.ts.map +1 -0
- package/dist/workspace-manifest/lib/types.js +10 -0
- package/dist/workspace-manifest/lib/types.js.map +1 -0
- package/package.json +79 -0
- package/schema/action.schema.json +181 -0
- package/schema/reusable-workflow.schema.json +46 -0
- package/schema/workflow.schema.json +373 -0
- package/src/deliverable-spec/index.ts +19 -0
- package/src/deliverable-spec/lib/schema.ts +248 -0
- package/src/deliverable-spec/lib/types.ts +26 -0
- package/src/payload-codec/index.ts +40 -0
- package/src/payload-codec/lib/blob-store.ts +0 -0
- package/src/payload-codec/lib/codec-context.ts +38 -0
- package/src/payload-codec/lib/codec.ts +593 -0
- package/src/payload-codec/lib/enums.ts +58 -0
- package/src/payload-codec/lib/errors.ts +54 -0
- package/src/payload-codec/lib/http-blob-store.ts +257 -0
- package/src/payload-codec/lib/lru-cache.ts +81 -0
- package/src/workflow/index.ts +98 -0
- package/src/workflow/lib/action-input-validator.ts +160 -0
- package/src/workflow/lib/compiler/action-shape.ts +71 -0
- package/src/workflow/lib/compiler/canonical-json.ts +53 -0
- package/src/workflow/lib/compiler/compile.ts +1518 -0
- package/src/workflow/lib/compiler/concurrency.ts +223 -0
- package/src/workflow/lib/compiler/dag.ts +108 -0
- package/src/workflow/lib/compiler/index.ts +10 -0
- package/src/workflow/lib/compiler/inputs.ts +199 -0
- package/src/workflow/lib/compiler/installation-resource-validator.ts +114 -0
- package/src/workflow/lib/compiler/manifest-source.ts +176 -0
- package/src/workflow/lib/compiler/matrix.ts +135 -0
- package/src/workflow/lib/compiler/mount-plan.ts +202 -0
- package/src/workflow/lib/compiler/payload-reach-in.ts +497 -0
- package/src/workflow/lib/compiler/permissions.ts +64 -0
- package/src/workflow/lib/compiler/retry-timeout.ts +105 -0
- package/src/workflow/lib/compiler/review-step.ts +517 -0
- package/src/workflow/lib/compiler/types.ts +170 -0
- package/src/workflow/lib/compiler/variable-requirements.ts +208 -0
- package/src/workflow/lib/deliverable-spec-keys.ts +109 -0
- package/src/workflow/lib/dispatch-inputs/index.ts +160 -0
- package/src/workflow/lib/dispatch-inputs/to-json-schema.ts +60 -0
- package/src/workflow/lib/duration.ts +48 -0
- package/src/workflow/lib/errors.ts +37 -0
- package/src/workflow/lib/expression/ast.ts +108 -0
- package/src/workflow/lib/expression/context.ts +148 -0
- package/src/workflow/lib/expression/evaluator.ts +492 -0
- package/src/workflow/lib/expression/index.ts +28 -0
- package/src/workflow/lib/expression/interpolation.ts +84 -0
- package/src/workflow/lib/expression/parser.ts +264 -0
- package/src/workflow/lib/expression/template.ts +117 -0
- package/src/workflow/lib/expression/tokenizer.ts +200 -0
- package/src/workflow/lib/expression/tokens.ts +30 -0
- package/src/workflow/lib/expression/walk-artifact-refs.ts +232 -0
- package/src/workflow/lib/installation-resource-kind.ts +107 -0
- package/src/workflow/lib/schemas-loader.ts +64 -0
- package/src/workflow/lib/serializer.ts +30 -0
- package/src/workflow/lib/types.ts +361 -0
- package/src/workflow/lib/validate.ts +199 -0
- package/src/workspace-manifest/index.ts +27 -0
- package/src/workspace-manifest/lib/compile.ts +608 -0
- package/src/workspace-manifest/lib/interpolate.ts +140 -0
- package/src/workspace-manifest/lib/resolve-extends.ts +260 -0
- package/src/workspace-manifest/lib/schema.ts +612 -0
- package/src/workspace-manifest/lib/types.ts +392 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isArtifactRef,
|
|
3
|
+
isArtifactRefEnvelopeField,
|
|
4
|
+
WorkflowErrorCode,
|
|
5
|
+
type ArtifactRef,
|
|
6
|
+
} from '@xemahq/kernel-contracts/workflow';
|
|
7
|
+
import { WorkflowDslError } from '../errors';
|
|
8
|
+
import {
|
|
9
|
+
type BinaryNode,
|
|
10
|
+
type CallNode,
|
|
11
|
+
type ExpressionNode,
|
|
12
|
+
ExpressionFunction,
|
|
13
|
+
ExpressionNodeKind,
|
|
14
|
+
ExpressionRoot,
|
|
15
|
+
type IdentifierNode,
|
|
16
|
+
type IndexNode,
|
|
17
|
+
type LiteralNode,
|
|
18
|
+
type MemberNode,
|
|
19
|
+
} from './ast';
|
|
20
|
+
import type { ExpressionContext } from './context';
|
|
21
|
+
import { parseExpression } from './parser';
|
|
22
|
+
import { tokenize } from './tokenizer';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compile a source expression into an {@link ExpressionNode} tree. Used by
|
|
26
|
+
* the workflow compiler to pre-validate every expression at compile time
|
|
27
|
+
* (so runtime evaluation can only fail on unknown bindings, not on
|
|
28
|
+
* syntax). Reject unknown identifiers, roots, and function calls.
|
|
29
|
+
*/
|
|
30
|
+
export function compileExpression(source: string): ExpressionNode {
|
|
31
|
+
if (source.length === 0) {
|
|
32
|
+
throw new WorkflowDslError(
|
|
33
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
34
|
+
'Expression source must not be empty',
|
|
35
|
+
{ source },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const tokens = tokenize(source);
|
|
39
|
+
return parseExpression(source, tokens);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Evaluate a pre-compiled {@link ExpressionNode} against a bound context.
|
|
44
|
+
* Deterministic: same (node, context) always produces the same value, no
|
|
45
|
+
* implicit time/random/env access, no silent coercion.
|
|
46
|
+
*
|
|
47
|
+
* Throws {@link WorkflowDslError} with code DSL_EXPRESSION_INVALID on any
|
|
48
|
+
* unknown property lookup or type mismatch — the caller must surface the
|
|
49
|
+
* error; silent substitution is forbidden.
|
|
50
|
+
*/
|
|
51
|
+
export function evaluateExpression(node: ExpressionNode, ctx: ExpressionContext): unknown {
|
|
52
|
+
switch (node.kind) {
|
|
53
|
+
case ExpressionNodeKind.LITERAL:
|
|
54
|
+
return evalLiteral(node);
|
|
55
|
+
case ExpressionNodeKind.IDENTIFIER:
|
|
56
|
+
return evalIdentifier(node, ctx);
|
|
57
|
+
case ExpressionNodeKind.MEMBER:
|
|
58
|
+
return evalMember(node, ctx);
|
|
59
|
+
case ExpressionNodeKind.INDEX:
|
|
60
|
+
return evalIndex(node, ctx);
|
|
61
|
+
case ExpressionNodeKind.CALL:
|
|
62
|
+
return evalCall(node, ctx);
|
|
63
|
+
case ExpressionNodeKind.UNARY_NOT: {
|
|
64
|
+
const operand = evaluateExpression(node.operand, ctx);
|
|
65
|
+
return !toBool(operand);
|
|
66
|
+
}
|
|
67
|
+
case ExpressionNodeKind.BINARY_EQ:
|
|
68
|
+
case ExpressionNodeKind.BINARY_NEQ:
|
|
69
|
+
case ExpressionNodeKind.BINARY_AND:
|
|
70
|
+
case ExpressionNodeKind.BINARY_OR:
|
|
71
|
+
return evalBinary(node, ctx);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function evalLiteral(node: LiteralNode): unknown {
|
|
76
|
+
return node.value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function evalIdentifier(node: IdentifierNode, ctx: ExpressionContext): unknown {
|
|
80
|
+
switch (node.name as ExpressionRoot) {
|
|
81
|
+
case ExpressionRoot.NEEDS:
|
|
82
|
+
return ctx.needs;
|
|
83
|
+
case ExpressionRoot.MATRIX:
|
|
84
|
+
return ctx.matrix;
|
|
85
|
+
case ExpressionRoot.INPUTS:
|
|
86
|
+
return ctx.inputs;
|
|
87
|
+
case ExpressionRoot.VARS:
|
|
88
|
+
return ctx.vars;
|
|
89
|
+
case ExpressionRoot.SECRETS:
|
|
90
|
+
return ctx.secrets;
|
|
91
|
+
case ExpressionRoot.TRIGGER:
|
|
92
|
+
return ctx.trigger;
|
|
93
|
+
case ExpressionRoot.JOB:
|
|
94
|
+
return ctx.job;
|
|
95
|
+
case ExpressionRoot.XEMA:
|
|
96
|
+
return ctx.xema;
|
|
97
|
+
default:
|
|
98
|
+
throw new WorkflowDslError(
|
|
99
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
100
|
+
`Unknown identifier root '${node.name}'`,
|
|
101
|
+
{ name: node.name },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function evalMember(node: MemberNode, ctx: ExpressionContext): unknown {
|
|
107
|
+
const target = evaluateExpression(node.target, ctx);
|
|
108
|
+
return readPropertyWithPayloadCache(target, node.property, ctx);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function evalIndex(node: IndexNode, ctx: ExpressionContext): unknown {
|
|
112
|
+
const target = evaluateExpression(node.target, ctx);
|
|
113
|
+
const key = evaluateExpression(node.index, ctx);
|
|
114
|
+
|
|
115
|
+
if (Array.isArray(target)) {
|
|
116
|
+
if (typeof key !== 'number' || !Number.isInteger(key)) {
|
|
117
|
+
throw new WorkflowDslError(
|
|
118
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
119
|
+
`Array index must be an integer, received ${typeof key}`,
|
|
120
|
+
{ key, targetKind: 'array' },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (key < 0 || key >= target.length) {
|
|
124
|
+
throw new WorkflowDslError(
|
|
125
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
126
|
+
`Array index out of range: ${key} (length ${target.length})`,
|
|
127
|
+
{ key, length: target.length },
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return target[key];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof target === 'object' && target !== null) {
|
|
134
|
+
if (typeof key !== 'string') {
|
|
135
|
+
throw new WorkflowDslError(
|
|
136
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
137
|
+
`Object index must be a string, received ${typeof key}`,
|
|
138
|
+
{ key, targetKind: 'object' },
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return readPropertyWithPayloadCache(target, key, ctx);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw new WorkflowDslError(
|
|
145
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
146
|
+
`Indexing is only supported on arrays or objects. Target kind: ${typeof target}`,
|
|
147
|
+
{ targetKind: typeof target },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function evalCall(node: CallNode, ctx: ExpressionContext): unknown {
|
|
152
|
+
switch (node.callee) {
|
|
153
|
+
case ExpressionFunction.FROM_JSON:
|
|
154
|
+
return evalFromJson(node, ctx);
|
|
155
|
+
case ExpressionFunction.TO_JSON:
|
|
156
|
+
return evalToJson(node, ctx);
|
|
157
|
+
case ExpressionFunction.FORMAT:
|
|
158
|
+
return evalFormat(node, ctx);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* `fromJSON` accepts two argument shapes:
|
|
164
|
+
*
|
|
165
|
+
* 1. `string` — JSON-parse the string (matches GitHub Actions semantics).
|
|
166
|
+
* Parse failure → `DSL_EXPRESSION_INVALID` (the source is authored
|
|
167
|
+
* literal text; we treat it as a syntax-level error).
|
|
168
|
+
* 2. `ArtifactRef` — look up the pre-fetched parsed payload in the
|
|
169
|
+
* `payloadCache` (keyed on `versionId`). Missing entry →
|
|
170
|
+
* `DSL_PAYLOAD_PARSE_ERROR`: the runtime is responsible for
|
|
171
|
+
* pre-resolving every ref referenced by an expression before the
|
|
172
|
+
* evaluator runs. A cache miss is a runtime infra bug, not an
|
|
173
|
+
* author bug, so it carries a runtime-coded error.
|
|
174
|
+
*
|
|
175
|
+
* This is the §C.2 escape hatch. Authors who want compile-time field
|
|
176
|
+
* validation use the typed reach-in (`outputs.foo.field`, §C.1).
|
|
177
|
+
*/
|
|
178
|
+
function evalFromJson(node: CallNode, ctx: ExpressionContext): unknown {
|
|
179
|
+
const argNode = singleArg(node, 'fromJSON');
|
|
180
|
+
const arg = evaluateExpression(argNode, ctx);
|
|
181
|
+
if (typeof arg === 'string') {
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(arg);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
throw new WorkflowDslError(
|
|
186
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
187
|
+
`fromJSON failed to parse argument as JSON: ${(err as Error).message}`,
|
|
188
|
+
{ callee: node.callee },
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (isArtifactRef(arg)) {
|
|
193
|
+
const cached = ctx.payloadCache.get(arg.versionId);
|
|
194
|
+
if (cached === undefined) {
|
|
195
|
+
throw new WorkflowDslError(
|
|
196
|
+
WorkflowErrorCode.DSL_PAYLOAD_PARSE_ERROR,
|
|
197
|
+
`fromJSON(<ArtifactRef>) called against versionId='${arg.versionId}' but the runtime did not pre-resolve its payload into the per-step cache. This is a runtime infra failure — the worker is expected to fetch and parse every referenced artifact before evaluating expressions.`,
|
|
198
|
+
{
|
|
199
|
+
callee: node.callee,
|
|
200
|
+
artifactId: arg.artifactId,
|
|
201
|
+
versionId: arg.versionId,
|
|
202
|
+
type: arg.type,
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return cached;
|
|
207
|
+
}
|
|
208
|
+
throw new WorkflowDslError(
|
|
209
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
210
|
+
`fromJSON requires a string or ArtifactRef argument, received ${typeof arg}`,
|
|
211
|
+
{ callee: node.callee, argKind: typeof arg },
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function evalToJson(node: CallNode, ctx: ExpressionContext): string {
|
|
216
|
+
const argNode = singleArg(node, 'toJSON');
|
|
217
|
+
return JSON.stringify(evaluateExpression(argNode, ctx));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function evalFormat(node: CallNode, ctx: ExpressionContext): string {
|
|
221
|
+
if (node.args.length < 1) {
|
|
222
|
+
throw new WorkflowDslError(
|
|
223
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
224
|
+
`format expects at least one argument (the template string)`,
|
|
225
|
+
{ callee: node.callee, arity: node.args.length },
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const [templateNode, ...argNodes] = node.args;
|
|
229
|
+
if (!templateNode) {
|
|
230
|
+
throw new WorkflowDslError(
|
|
231
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
232
|
+
`format template is missing from AST`,
|
|
233
|
+
{ callee: node.callee },
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const template = evaluateExpression(templateNode, ctx);
|
|
237
|
+
if (typeof template !== 'string') {
|
|
238
|
+
throw new WorkflowDslError(
|
|
239
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
240
|
+
`format template must be a string, received ${typeof template}`,
|
|
241
|
+
{ callee: node.callee, templateKind: typeof template },
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const args = argNodes.map((argNode) => evaluateExpression(argNode, ctx));
|
|
245
|
+
return formatTemplate(template, args);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function singleArg(node: CallNode, fnName: string): ExpressionNode {
|
|
249
|
+
if (node.args.length !== 1) {
|
|
250
|
+
throw new WorkflowDslError(
|
|
251
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
252
|
+
`${fnName} expects exactly one argument, received ${node.args.length}`,
|
|
253
|
+
{ callee: node.callee, arity: node.args.length },
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const [argNode] = node.args;
|
|
257
|
+
if (!argNode) {
|
|
258
|
+
throw new WorkflowDslError(
|
|
259
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
260
|
+
`${fnName} argument is missing from AST`,
|
|
261
|
+
{ callee: node.callee },
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return argNode;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Replace `{N}` placeholders in `template` with the stringified arg at
|
|
269
|
+
* index N. Escape `{{` / `}}` for literal braces. Out-of-range indices
|
|
270
|
+
* fail fast — silent emptiness would be a footgun for authors.
|
|
271
|
+
*/
|
|
272
|
+
function formatTemplate(template: string, args: readonly unknown[]): string {
|
|
273
|
+
let out = '';
|
|
274
|
+
let i = 0;
|
|
275
|
+
while (i < template.length) {
|
|
276
|
+
const ch = template[i];
|
|
277
|
+
if (ch === '{') {
|
|
278
|
+
const advance = consumeOpenBrace(template, i, args);
|
|
279
|
+
out += advance.text;
|
|
280
|
+
i = advance.next;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (ch === '}') {
|
|
284
|
+
const advance = consumeCloseBrace(template, i);
|
|
285
|
+
out += advance.text;
|
|
286
|
+
i = advance.next;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
out += ch;
|
|
290
|
+
i++;
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
interface FormatAdvance {
|
|
296
|
+
readonly text: string;
|
|
297
|
+
readonly next: number;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function consumeOpenBrace(
|
|
301
|
+
template: string,
|
|
302
|
+
i: number,
|
|
303
|
+
args: readonly unknown[],
|
|
304
|
+
): FormatAdvance {
|
|
305
|
+
if (template[i + 1] === '{') {
|
|
306
|
+
return { text: '{', next: i + 2 };
|
|
307
|
+
}
|
|
308
|
+
const close = template.indexOf('}', i + 1);
|
|
309
|
+
if (close === -1) {
|
|
310
|
+
throw new WorkflowDslError(
|
|
311
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
312
|
+
`format template has unclosed '{' starting at index ${i}: ${template}`,
|
|
313
|
+
{ template },
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const indexLiteral = template.slice(i + 1, close);
|
|
317
|
+
const idx = parseFormatIndex(template, indexLiteral, args.length);
|
|
318
|
+
return { text: stringifyFormatArg(args[idx]), next: close + 1 };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function consumeCloseBrace(template: string, i: number): FormatAdvance {
|
|
322
|
+
if (template[i + 1] === '}') {
|
|
323
|
+
return { text: '}', next: i + 2 };
|
|
324
|
+
}
|
|
325
|
+
throw new WorkflowDslError(
|
|
326
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
327
|
+
`format template has unmatched '}' at index ${i}: ${template}`,
|
|
328
|
+
{ template },
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function parseFormatIndex(template: string, indexLiteral: string, argCount: number): number {
|
|
333
|
+
if (!/^\d+$/.test(indexLiteral)) {
|
|
334
|
+
throw new WorkflowDslError(
|
|
335
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
336
|
+
`format placeholder '{${indexLiteral}}' is not a non-negative integer`,
|
|
337
|
+
{ template, placeholder: indexLiteral },
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const idx = Number(indexLiteral);
|
|
341
|
+
if (idx >= argCount) {
|
|
342
|
+
throw new WorkflowDslError(
|
|
343
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
344
|
+
`format placeholder '{${idx}}' has no matching argument (received ${argCount} arg(s))`,
|
|
345
|
+
{ template, placeholder: idx, argCount },
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return idx;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function stringifyFormatArg(value: unknown): string {
|
|
352
|
+
if (typeof value === 'string') return value;
|
|
353
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
354
|
+
if (value === null) return 'null';
|
|
355
|
+
if (value === undefined) {
|
|
356
|
+
throw new WorkflowDslError(
|
|
357
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
358
|
+
`format argument resolved to undefined; expressions must produce a defined value`,
|
|
359
|
+
{},
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
// Objects + arrays serialize as JSON so authors can build deterministic
|
|
363
|
+
// strings without risk of `[object Object]` slipping through.
|
|
364
|
+
return JSON.stringify(value);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function evalBinary(node: BinaryNode, ctx: ExpressionContext): unknown {
|
|
368
|
+
switch (node.kind) {
|
|
369
|
+
case ExpressionNodeKind.BINARY_EQ: {
|
|
370
|
+
const left = evaluateExpression(node.left, ctx);
|
|
371
|
+
const right = evaluateExpression(node.right, ctx);
|
|
372
|
+
return strictEquals(left, right);
|
|
373
|
+
}
|
|
374
|
+
case ExpressionNodeKind.BINARY_NEQ: {
|
|
375
|
+
const left = evaluateExpression(node.left, ctx);
|
|
376
|
+
const right = evaluateExpression(node.right, ctx);
|
|
377
|
+
return !strictEquals(left, right);
|
|
378
|
+
}
|
|
379
|
+
case ExpressionNodeKind.BINARY_AND: {
|
|
380
|
+
const left = evaluateExpression(node.left, ctx);
|
|
381
|
+
if (!toBool(left)) return false;
|
|
382
|
+
return toBool(evaluateExpression(node.right, ctx));
|
|
383
|
+
}
|
|
384
|
+
case ExpressionNodeKind.BINARY_OR: {
|
|
385
|
+
const left = evaluateExpression(node.left, ctx);
|
|
386
|
+
if (toBool(left)) return true;
|
|
387
|
+
return toBool(evaluateExpression(node.right, ctx));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Property-read wrapper that transparently redirects payload reach-in
|
|
394
|
+
* (`outputs.foo.<payload-field>`) through the per-step `payloadCache`.
|
|
395
|
+
*
|
|
396
|
+
* When `target` is an `ArtifactRef`:
|
|
397
|
+
* • envelope-field property (artifactId/versionId/version/hash/type/title)
|
|
398
|
+
* → read directly off the ref (no fetch).
|
|
399
|
+
* • any other property → look up the artifact's parsed payload in
|
|
400
|
+
* `ctx.payloadCache` (populated by the worker's reach-in pre-fetch)
|
|
401
|
+
* and read the field off that. Cache miss surfaces as
|
|
402
|
+
* `DSL_PAYLOAD_PARSE_ERROR` so the author sees a clean DSL error
|
|
403
|
+
* instead of a misleading "Unknown property" on the ref.
|
|
404
|
+
*
|
|
405
|
+
* Anything else delegates to the plain `readProperty` path.
|
|
406
|
+
*/
|
|
407
|
+
function readPropertyWithPayloadCache(
|
|
408
|
+
target: unknown,
|
|
409
|
+
property: string,
|
|
410
|
+
ctx: ExpressionContext,
|
|
411
|
+
): unknown {
|
|
412
|
+
if (isArtifactRef(target) && !isArtifactRefEnvelopeField(property)) {
|
|
413
|
+
return readPayloadField(target, property, ctx);
|
|
414
|
+
}
|
|
415
|
+
return readProperty(target, property);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function readPayloadField(
|
|
419
|
+
ref: ArtifactRef,
|
|
420
|
+
property: string,
|
|
421
|
+
ctx: ExpressionContext,
|
|
422
|
+
): unknown {
|
|
423
|
+
if (!ctx.payloadCache.has(ref.versionId)) {
|
|
424
|
+
throw new WorkflowDslError(
|
|
425
|
+
WorkflowErrorCode.DSL_PAYLOAD_PARSE_ERROR,
|
|
426
|
+
`Payload reach-in '${property}' on artifact '${ref.artifactId}' v${ref.version} ` +
|
|
427
|
+
`(versionId '${ref.versionId}') is not available — the per-step payloadCache ` +
|
|
428
|
+
`has no entry for this version. The runtime pre-fetch should have populated ` +
|
|
429
|
+
`the cache before this expression evaluated; this typically means the ref ` +
|
|
430
|
+
`surfaced from an indirection the pre-fetch walker can't trace (e.g. a ` +
|
|
431
|
+
`dynamically computed ref). Use 'fromJSON(<ref>)' as the documented ` +
|
|
432
|
+
`escape hatch when the runtime cannot statically pre-fetch.`,
|
|
433
|
+
{ artifactId: ref.artifactId, versionId: ref.versionId, version: ref.version, property },
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const payload = ctx.payloadCache.get(ref.versionId);
|
|
437
|
+
if (payload === null || payload === undefined) {
|
|
438
|
+
throw new WorkflowDslError(
|
|
439
|
+
WorkflowErrorCode.DSL_PAYLOAD_PARSE_ERROR,
|
|
440
|
+
`Payload reach-in '${property}' on artifact '${ref.artifactId}' v${ref.version} ` +
|
|
441
|
+
`failed: cached payload is ${payload === null ? 'null' : 'undefined'}.`,
|
|
442
|
+
{ artifactId: ref.artifactId, versionId: ref.versionId, property },
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
return readProperty(payload, property);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function readProperty(target: unknown, property: string): unknown {
|
|
449
|
+
if (target === null || target === undefined) {
|
|
450
|
+
throw new WorkflowDslError(
|
|
451
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
452
|
+
`Cannot read property '${property}' of ${target === null ? 'null' : 'undefined'}`,
|
|
453
|
+
{ property },
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
if (typeof target !== 'object' || Array.isArray(target)) {
|
|
457
|
+
throw new WorkflowDslError(
|
|
458
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
459
|
+
`Member access '${property}' is not supported on ${Array.isArray(target) ? 'arrays' : typeof target}`,
|
|
460
|
+
{ property, targetKind: Array.isArray(target) ? 'array' : typeof target },
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const record = target as Record<string, unknown>;
|
|
464
|
+
if (!Object.prototype.hasOwnProperty.call(record, property)) {
|
|
465
|
+
throw new WorkflowDslError(
|
|
466
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
467
|
+
`Unknown property '${property}'. Available keys: ${Object.keys(record).sort().join(', ') || '(none)'}.`,
|
|
468
|
+
{ property, availableKeys: Object.keys(record) },
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return record[property];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function toBool(value: unknown): boolean {
|
|
475
|
+
// Deterministic boolean coercion — closed set of accepted types. Anything
|
|
476
|
+
// else is a typed error. No JS truthiness footguns in expressions.
|
|
477
|
+
if (typeof value === 'boolean') return value;
|
|
478
|
+
if (value === null) return false;
|
|
479
|
+
throw new WorkflowDslError(
|
|
480
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
481
|
+
`Expected boolean, received ${typeof value}. Xema expressions do not implicitly coerce truthiness.`,
|
|
482
|
+
{ valueKind: value === null ? 'null' : typeof value },
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function strictEquals(a: unknown, b: unknown): boolean {
|
|
487
|
+
// Primitive equality only. Arrays/objects fall through to reference
|
|
488
|
+
// equality — expression authors should compare primitives.
|
|
489
|
+
if (typeof a !== typeof b) return false;
|
|
490
|
+
if (a === b) return true;
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type ExpressionContext,
|
|
3
|
+
type NeedsEntry,
|
|
4
|
+
type PayloadCache,
|
|
5
|
+
type XemaActor,
|
|
6
|
+
type XemaContext,
|
|
7
|
+
XemaActorKind,
|
|
8
|
+
EMPTY_CONTEXT_SEEDS,
|
|
9
|
+
} from './context';
|
|
10
|
+
export { compileExpression, evaluateExpression } from './evaluator';
|
|
11
|
+
export { ExpressionFunction, ExpressionNodeKind, ExpressionRoot } from './ast';
|
|
12
|
+
export type { ExpressionNode } from './ast';
|
|
13
|
+
export { extractInterpolations, isInterpolationWrapped, stripInterpolation } from './interpolation';
|
|
14
|
+
export {
|
|
15
|
+
extractUpstreamJobKey,
|
|
16
|
+
extractUpstreamOutputName,
|
|
17
|
+
walkRuntimeArtifactRefTargets,
|
|
18
|
+
} from './walk-artifact-refs';
|
|
19
|
+
export {
|
|
20
|
+
parseTemplate,
|
|
21
|
+
templateHasExpressions,
|
|
22
|
+
TemplateSegmentKind,
|
|
23
|
+
} from './template';
|
|
24
|
+
export type {
|
|
25
|
+
TemplateExpressionSegment,
|
|
26
|
+
TemplateLiteralSegment,
|
|
27
|
+
TemplateSegment,
|
|
28
|
+
} from './template';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
|
|
2
|
+
import { WorkflowDslError } from '../errors';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authored expressions in YAML are wrapped as `${{ ... }}`. These helpers
|
|
6
|
+
* strip the wrapping deterministically and detect partial interpolation.
|
|
7
|
+
*
|
|
8
|
+
* Design choice: partial interpolation (`hello ${{ inputs.name }}!`) is
|
|
9
|
+
* NOT supported in v1. Every authored expression must be the whole value
|
|
10
|
+
* of its field. String templating introduces ambiguity around type
|
|
11
|
+
* coercion — we keep the surface small and composable instead.
|
|
12
|
+
*/
|
|
13
|
+
const INTERPOLATION_RE = /^\s*\$\{\{\s*(?<body>[\s\S]+?)\s*\}\}\s*$/;
|
|
14
|
+
export const ANY_INTERPOLATION_RE = /\$\{\{/;
|
|
15
|
+
|
|
16
|
+
/** True if the raw string is a single `${{ ... }}` expression (and only that). */
|
|
17
|
+
export function isInterpolationWrapped(raw: string): boolean {
|
|
18
|
+
return INTERPOLATION_RE.test(raw);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Strip `${{ ... }}` wrapping. Throws DSL_EXPRESSION_INVALID if the raw
|
|
23
|
+
* string is not fully wrapped (rejects partial interpolation).
|
|
24
|
+
*/
|
|
25
|
+
export function stripInterpolation(raw: string): string {
|
|
26
|
+
const match = INTERPOLATION_RE.exec(raw);
|
|
27
|
+
if (!match) {
|
|
28
|
+
throw new WorkflowDslError(
|
|
29
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
30
|
+
`Expression must be fully wrapped in \${{ ... }}: ${raw}`,
|
|
31
|
+
{ raw },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return match.groups?.['body'] ?? '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Walks an arbitrary value tree and returns every `${{ ... }}` source it
|
|
39
|
+
* finds. Used by the compiler's validation pass: collect all expressions
|
|
40
|
+
* in `with`, `outputs`, `if`, `strategy.dynamic.from`, etc., and compile
|
|
41
|
+
* each one so authored expressions are caught at compile time.
|
|
42
|
+
*/
|
|
43
|
+
export function extractInterpolations(node: unknown, path: string[] = []): ExtractedExpression[] {
|
|
44
|
+
if (typeof node === 'string') {
|
|
45
|
+
if (isInterpolationWrapped(node)) {
|
|
46
|
+
return [{ path: [...path], source: stripInterpolation(node) }];
|
|
47
|
+
}
|
|
48
|
+
if (ANY_INTERPOLATION_RE.test(node)) {
|
|
49
|
+
// Partial interpolation — reject fast so authors can't accidentally
|
|
50
|
+
// rely on broken string templating.
|
|
51
|
+
throw new WorkflowDslError(
|
|
52
|
+
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
53
|
+
`Partial \${{ ... }} interpolation is not supported at ${pathLabel(path)}. Use a fully wrapped expression or plain text.`,
|
|
54
|
+
{ path },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(node)) {
|
|
60
|
+
const collected: ExtractedExpression[] = [];
|
|
61
|
+
for (let i = 0; i < node.length; i++) {
|
|
62
|
+
collected.push(...extractInterpolations(node[i], [...path, String(i)]));
|
|
63
|
+
}
|
|
64
|
+
return collected;
|
|
65
|
+
}
|
|
66
|
+
if (node !== null && typeof node === 'object') {
|
|
67
|
+
const record = node as Record<string, unknown>;
|
|
68
|
+
const collected: ExtractedExpression[] = [];
|
|
69
|
+
for (const key of Object.keys(record)) {
|
|
70
|
+
collected.push(...extractInterpolations(record[key], [...path, key]));
|
|
71
|
+
}
|
|
72
|
+
return collected;
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ExtractedExpression {
|
|
78
|
+
readonly path: readonly string[];
|
|
79
|
+
readonly source: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pathLabel(path: readonly string[]): string {
|
|
83
|
+
return path.length === 0 ? '<root>' : path.join('.');
|
|
84
|
+
}
|