-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathexpression.go
118 lines (108 loc) · 3.96 KB
/
expression.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package expressions
import (
"fmt"
"go.flow.arcalot.io/expressions/internal/ast"
"go.flow.arcalot.io/pluginsdk/schema"
)
// New parses the specified expression and returns the expression structure.
func New(expressionString string) (Expression, error) {
parser, err := ast.InitParser(expressionString, "workflow.yaml")
if err != nil {
return nil, fmt.Errorf("failed to parse expression: %s (%w)", expressionString, err)
}
exprAst, err := parser.ParseExpression()
if err != nil {
return nil, fmt.Errorf("failed to parse expression: %s (%w)", expressionString, err)
}
return &expression{
ast: exprAst,
expression: expressionString,
}, nil
}
// Expression is an interface describing how expressions should behave.
type Expression interface {
// Type evaluates the expression and evaluates the type on the specified schema.
Type(schema schema.Scope, functions map[string]schema.Function, workflowContext map[string][]byte) (schema.Type, error)
// Dependencies traverses the passed scope and evaluates the items this expression depends on. This is useful to
// construct a dependency tree based on expressions.
// Returns the path to the object in the schema that it depends on, or nil if it's a literal that doesn't depend
// on it.
// unpackRequirements specifies which paths to include, and which values to include in paths.
Dependencies(schema schema.Type, functions map[string]schema.Function, workflowContext map[string][]byte, unpackRequirements UnpackRequirements) ([]Path, error)
// Evaluate evaluates the expression on the given data set regardless of any
// schema. The caller is responsible for validating the expected schema.
Evaluate(data any, functions map[string]schema.CallableFunction, workflowContext map[string][]byte) (any, error)
// String returns the string representation of the expression.
String() string
}
// expression is the implementation of Expression. It holds the original expression, as well as the parsed AST.
type expression struct {
expression string
ast ast.Node
}
func (e expression) String() string {
return e.expression
}
func (e expression) Type(scope schema.Scope, functions map[string]schema.Function, workflowContext map[string][]byte) (schema.Type, error) {
tree := PathTree{
PathItem: "$",
NodeType: DataRootNode,
Subtrees: nil,
}
d := &dependencyContext{
rootType: scope,
rootPath: tree,
workflowContext: workflowContext,
functions: functions,
}
dependencyResolutionResult, err := d.rootDependencies(e.ast)
if err != nil {
return nil, err
}
return dependencyResolutionResult.resolvedType, nil
}
func (e expression) Dependencies(
scope schema.Type,
functions map[string]schema.Function,
workflowContext map[string][]byte,
unpackRequirements UnpackRequirements,
) ([]Path, error) {
root := PathTree{
PathItem: "$",
NodeType: DataRootNode,
Subtrees: nil,
}
d := &dependencyContext{
rootType: scope,
rootPath: root,
workflowContext: workflowContext,
functions: functions,
}
dependencyResolutionResult, err := d.rootDependencies(e.ast)
if err != nil {
return nil, err
}
// Now convert to paths, saving only unique values.
finalDependencySet := make(map[string]bool)
finalDependencies := make([]Path, 0)
for _, dependencyTree := range dependencyResolutionResult.completedPaths {
unpackedDependencies := dependencyTree.Unpack(unpackRequirements)
for _, dependency := range unpackedDependencies {
asString := dependency.String()
_, dependencyExists := finalDependencySet[asString]
if !dependencyExists {
finalDependencies = append(finalDependencies, dependency)
finalDependencySet[asString] = true
}
}
}
return finalDependencies, nil
}
func (e expression) Evaluate(data any, functions map[string]schema.CallableFunction, workflowContext map[string][]byte) (any, error) {
context := &evaluateContext{
functions: functions,
rootData: data,
workflowContext: workflowContext,
}
return context.evaluate(e.ast, data)
}