Skip to content

Commit feeb8db

Browse files
committed
Evaluate expressions, statusphase, schema validation, export, as, from
Signed-off-by: Ricardo Zanini <[email protected]>
1 parent 5b9c322 commit feeb8db

36 files changed

+1325
-292
lines changed

expr/expr.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func IsValid(expression string) bool {
3434
}
3535

3636
// TraverseAndEvaluate recursively processes and evaluates all expressions in a JSON-like structure
37-
func TraverseAndEvaluate(node interface{}, input map[string]interface{}) (interface{}, error) {
37+
func TraverseAndEvaluate(node interface{}, input interface{}) (interface{}, error) {
3838
switch v := node.(type) {
3939
case map[string]interface{}:
4040
// Traverse map
@@ -72,7 +72,7 @@ func TraverseAndEvaluate(node interface{}, input map[string]interface{}) (interf
7272
}
7373

7474
// EvaluateJQExpression evaluates a jq expression against a given JSON input
75-
func EvaluateJQExpression(expression string, input map[string]interface{}) (interface{}, error) {
75+
func EvaluateJQExpression(expression string, input interface{}) (interface{}, error) {
7676
// Parse the sanitized jq expression
7777
query, err := gojq.Parse(expression)
7878
if err != nil {

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ require (
2121
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
2222
github.com/tidwall/match v1.1.1 // indirect
2323
github.com/tidwall/pretty v1.2.1 // indirect
24+
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
25+
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
26+
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
2427
golang.org/x/crypto v0.32.0 // indirect
2528
golang.org/x/net v0.34.0 // indirect
2629
golang.org/x/sys v0.29.0 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
23
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
@@ -19,8 +20,11 @@ github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/my
1920
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
2021
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
2122
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
23+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2224
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
2325
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
26+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
27+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
2428
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
2529
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
2630
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
@@ -30,6 +34,12 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
3034
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
3135
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
3236
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
37+
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
38+
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
39+
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
40+
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
41+
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
42+
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
3343
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
3444
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
3545
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=

impl/context.go

Lines changed: 97 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,84 +8,124 @@ import (
88

99
type ctxKey string
1010

11-
const executorCtxKey ctxKey = "executorContext"
12-
13-
// ExecutorContext to not confound with Workflow Context as "$context" in the specification.
14-
// This holds the necessary data for the workflow execution within the instance.
15-
type ExecutorContext struct {
16-
mu sync.Mutex
17-
Input map[string]interface{}
18-
Output map[string]interface{}
19-
// Context or `$context` passed through the task executions see https://github.com/serverlessworkflow/specification/blob/main/dsl.md#data-flow
20-
Context map[string]interface{}
11+
const runnerCtxKey ctxKey = "wfRunnerContext"
12+
13+
// WorkflowRunnerContext holds the necessary data for the workflow execution within the instance.
14+
type WorkflowRunnerContext struct {
15+
mu sync.Mutex
16+
input interface{} // input can hold any type
17+
output interface{} // output can hold any type
18+
context map[string]interface{}
19+
StatusPhase []StatusPhaseLog
20+
TasksStatusPhase map[string][]StatusPhaseLog // Holds `$context` as the key
21+
}
22+
23+
func (runnerCtx *WorkflowRunnerContext) SetStatus(status StatusPhase) {
24+
runnerCtx.mu.Lock()
25+
defer runnerCtx.mu.Unlock()
26+
if runnerCtx.StatusPhase == nil {
27+
runnerCtx.StatusPhase = []StatusPhaseLog{}
28+
}
29+
runnerCtx.StatusPhase = append(runnerCtx.StatusPhase, NewStatusPhaseLog(status))
2130
}
2231

23-
// SetWorkflowCtx safely sets the $context
24-
func (execCtx *ExecutorContext) SetWorkflowCtx(wfCtx map[string]interface{}) {
25-
execCtx.mu.Lock()
26-
defer execCtx.mu.Unlock()
27-
execCtx.Context = wfCtx
32+
func (runnerCtx *WorkflowRunnerContext) SetTaskStatus(task string, status StatusPhase) {
33+
runnerCtx.mu.Lock()
34+
defer runnerCtx.mu.Unlock()
35+
if runnerCtx.TasksStatusPhase == nil {
36+
runnerCtx.TasksStatusPhase = map[string][]StatusPhaseLog{}
37+
}
38+
runnerCtx.TasksStatusPhase[task] = append(runnerCtx.TasksStatusPhase[task], NewStatusPhaseLog(status))
39+
}
40+
41+
// SetWorkflowCtx safely sets the `$context` value
42+
func (runnerCtx *WorkflowRunnerContext) SetWorkflowCtx(value interface{}) {
43+
runnerCtx.mu.Lock()
44+
defer runnerCtx.mu.Unlock()
45+
if runnerCtx.context == nil {
46+
runnerCtx.context = make(map[string]interface{})
47+
}
48+
runnerCtx.context["$context"] = value
2849
}
2950

30-
// GetWorkflowCtx safely retrieves the $context
31-
func (execCtx *ExecutorContext) GetWorkflowCtx() map[string]interface{} {
32-
execCtx.mu.Lock()
33-
defer execCtx.mu.Unlock()
34-
return execCtx.Context
51+
// GetWorkflowCtx safely retrieves the `$context` value
52+
func (runnerCtx *WorkflowRunnerContext) GetWorkflowCtx() interface{} {
53+
runnerCtx.mu.Lock()
54+
defer runnerCtx.mu.Unlock()
55+
if runnerCtx.context == nil {
56+
return nil
57+
}
58+
return runnerCtx.context["$context"]
3559
}
3660

37-
// SetInput safely sets the input map
38-
func (execCtx *ExecutorContext) SetInput(input map[string]interface{}) {
39-
execCtx.mu.Lock()
40-
defer execCtx.mu.Unlock()
41-
execCtx.Input = input
61+
// SetInput safely sets the input
62+
func (runnerCtx *WorkflowRunnerContext) SetInput(input interface{}) {
63+
runnerCtx.mu.Lock()
64+
defer runnerCtx.mu.Unlock()
65+
runnerCtx.input = input
4266
}
4367

44-
// GetInput safely retrieves the input map
45-
func (execCtx *ExecutorContext) GetInput() map[string]interface{} {
46-
execCtx.mu.Lock()
47-
defer execCtx.mu.Unlock()
48-
return execCtx.Input
68+
// GetInput safely retrieves the input
69+
func (runnerCtx *WorkflowRunnerContext) GetInput() interface{} {
70+
runnerCtx.mu.Lock()
71+
defer runnerCtx.mu.Unlock()
72+
return runnerCtx.input
4973
}
5074

51-
// SetOutput safely sets the output map
52-
func (execCtx *ExecutorContext) SetOutput(output map[string]interface{}) {
53-
execCtx.mu.Lock()
54-
defer execCtx.mu.Unlock()
55-
execCtx.Output = output
75+
// SetOutput safely sets the output
76+
func (runnerCtx *WorkflowRunnerContext) SetOutput(output interface{}) {
77+
runnerCtx.mu.Lock()
78+
defer runnerCtx.mu.Unlock()
79+
runnerCtx.output = output
5680
}
5781

58-
// GetOutput safely retrieves the output map
59-
func (execCtx *ExecutorContext) GetOutput() map[string]interface{} {
60-
execCtx.mu.Lock()
61-
defer execCtx.mu.Unlock()
62-
return execCtx.Output
82+
// GetOutput safely retrieves the output
83+
func (runnerCtx *WorkflowRunnerContext) GetOutput() interface{} {
84+
runnerCtx.mu.Lock()
85+
defer runnerCtx.mu.Unlock()
86+
return runnerCtx.output
6387
}
6488

65-
// UpdateOutput allows adding or updating a single key-value pair in the output map
66-
func (execCtx *ExecutorContext) UpdateOutput(key string, value interface{}) {
67-
execCtx.mu.Lock()
68-
defer execCtx.mu.Unlock()
69-
if execCtx.Output == nil {
70-
execCtx.Output = make(map[string]interface{})
89+
// GetInputAsMap safely retrieves the input as a map[string]interface{}.
90+
// If input is not a map, it creates a map with an empty string key and the input as the value.
91+
func (runnerCtx *WorkflowRunnerContext) GetInputAsMap() map[string]interface{} {
92+
runnerCtx.mu.Lock()
93+
defer runnerCtx.mu.Unlock()
94+
95+
if inputMap, ok := runnerCtx.input.(map[string]interface{}); ok {
96+
return inputMap
97+
}
98+
99+
// If input is not a map, create a map with an empty key and set input as the value
100+
return map[string]interface{}{
101+
"": runnerCtx.input,
71102
}
72-
execCtx.Output[key] = value
73103
}
74104

75-
// GetOutputValue safely retrieves a single key from the output map
76-
func (execCtx *ExecutorContext) GetOutputValue(key string) (interface{}, bool) {
77-
execCtx.mu.Lock()
78-
defer execCtx.mu.Unlock()
79-
value, exists := execCtx.Output[key]
80-
return value, exists
105+
// GetOutputAsMap safely retrieves the output as a map[string]interface{}.
106+
// If output is not a map, it creates a map with an empty string key and the output as the value.
107+
func (runnerCtx *WorkflowRunnerContext) GetOutputAsMap() map[string]interface{} {
108+
runnerCtx.mu.Lock()
109+
defer runnerCtx.mu.Unlock()
110+
111+
if outputMap, ok := runnerCtx.output.(map[string]interface{}); ok {
112+
return outputMap
113+
}
114+
115+
// If output is not a map, create a map with an empty key and set output as the value
116+
return map[string]interface{}{
117+
"": runnerCtx.output,
118+
}
81119
}
82120

83-
func WithExecutorContext(parent context.Context, wfCtx *ExecutorContext) context.Context {
84-
return context.WithValue(parent, executorCtxKey, wfCtx)
121+
// WithRunnerContext adds the WorkflowRunnerContext to a parent context
122+
func WithRunnerContext(parent context.Context, wfCtx *WorkflowRunnerContext) context.Context {
123+
return context.WithValue(parent, runnerCtxKey, wfCtx)
85124
}
86125

87-
func GetExecutorContext(ctx context.Context) (*ExecutorContext, error) {
88-
wfCtx, ok := ctx.Value(executorCtxKey).(*ExecutorContext)
126+
// GetRunnerContext retrieves the WorkflowRunnerContext from a context
127+
func GetRunnerContext(ctx context.Context) (*WorkflowRunnerContext, error) {
128+
wfCtx, ok := ctx.Value(runnerCtxKey).(*WorkflowRunnerContext)
89129
if !ok {
90130
return nil, errors.New("workflow context not found")
91131
}

impl/impl.go

Lines changed: 0 additions & 90 deletions
This file was deleted.

impl/json_schema.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package impl
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"github.com/serverlessworkflow/sdk-go/v3/model"
8+
"github.com/xeipuuv/gojsonschema"
9+
)
10+
11+
// ValidateJSONSchema validates the provided data against a model.Schema.
12+
func ValidateJSONSchema(data interface{}, schema *model.Schema) error {
13+
if schema == nil {
14+
return nil
15+
}
16+
17+
schema.ApplyDefaults()
18+
19+
if schema.Format != model.DefaultSchema {
20+
return fmt.Errorf("unsupported schema format: '%s'", schema.Format)
21+
}
22+
23+
var schemaJSON string
24+
if schema.Document != nil {
25+
documentBytes, err := json.Marshal(schema.Document)
26+
if err != nil {
27+
return fmt.Errorf("failed to marshal schema document to JSON: %w", err)
28+
}
29+
schemaJSON = string(documentBytes)
30+
} else if schema.Resource != nil {
31+
// TODO: Handle external resource references (not implemented here)
32+
return errors.New("external resources are not yet supported")
33+
} else {
34+
return errors.New("schema must have either a 'Document' or 'Resource'")
35+
}
36+
37+
schemaLoader := gojsonschema.NewStringLoader(schemaJSON)
38+
dataLoader := gojsonschema.NewGoLoader(data)
39+
40+
result, err := gojsonschema.Validate(schemaLoader, dataLoader)
41+
if err != nil {
42+
// TODO: use model.Error
43+
return fmt.Errorf("failed to validate JSON schema: %w", err)
44+
}
45+
46+
if !result.Valid() {
47+
var validationErrors string
48+
for _, err := range result.Errors() {
49+
validationErrors += fmt.Sprintf("- %s\n", err.String())
50+
}
51+
return fmt.Errorf("JSON schema validation failed:\n%s", validationErrors)
52+
}
53+
54+
return nil
55+
}

0 commit comments

Comments
 (0)