Skip to content

Commit 5b9c322

Browse files
committed
Fix #222 - (WIP): Basic raw implementation for DSL 1.0.0
Signed-off-by: Ricardo Zanini <[email protected]>
1 parent ff500a0 commit 5b9c322

12 files changed

+880
-16
lines changed

expr/expr.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package expr
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"github.com/itchyny/gojq"
7+
"strings"
8+
)
9+
10+
// IsStrictExpr returns true if the string is enclosed in `${ }`
11+
func IsStrictExpr(expression string) bool {
12+
return strings.HasPrefix(expression, "${") && strings.HasSuffix(expression, "}")
13+
}
14+
15+
// Sanitize processes the expression to ensure it's ready for evaluation
16+
// It removes `${}` if present and replaces single quotes with double quotes
17+
func Sanitize(expression string) string {
18+
// Remove `${}` enclosure if present
19+
if IsStrictExpr(expression) {
20+
expression = strings.TrimSpace(expression[2 : len(expression)-1])
21+
}
22+
23+
// Replace single quotes with double quotes
24+
expression = strings.ReplaceAll(expression, "'", "\"")
25+
26+
return expression
27+
}
28+
29+
// IsValid tries to parse and check if the given value is a valid expression
30+
func IsValid(expression string) bool {
31+
expression = Sanitize(expression)
32+
_, err := gojq.Parse(expression)
33+
return err == nil
34+
}
35+
36+
// TraverseAndEvaluate recursively processes and evaluates all expressions in a JSON-like structure
37+
func TraverseAndEvaluate(node interface{}, input map[string]interface{}) (interface{}, error) {
38+
switch v := node.(type) {
39+
case map[string]interface{}:
40+
// Traverse map
41+
for key, value := range v {
42+
evaluatedValue, err := TraverseAndEvaluate(value, input)
43+
if err != nil {
44+
return nil, err
45+
}
46+
v[key] = evaluatedValue
47+
}
48+
return v, nil
49+
50+
case []interface{}:
51+
// Traverse array
52+
for i, value := range v {
53+
evaluatedValue, err := TraverseAndEvaluate(value, input)
54+
if err != nil {
55+
return nil, err
56+
}
57+
v[i] = evaluatedValue
58+
}
59+
return v, nil
60+
61+
case string:
62+
// Check if the string is a runtime expression (e.g., ${ .some.path })
63+
if IsStrictExpr(v) {
64+
return EvaluateJQExpression(Sanitize(v), input)
65+
}
66+
return v, nil
67+
68+
default:
69+
// Return other types as-is
70+
return v, nil
71+
}
72+
}
73+
74+
// EvaluateJQExpression evaluates a jq expression against a given JSON input
75+
func EvaluateJQExpression(expression string, input map[string]interface{}) (interface{}, error) {
76+
// Parse the sanitized jq expression
77+
query, err := gojq.Parse(expression)
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to parse jq expression: %s, error: %w", expression, err)
80+
}
81+
82+
// Compile and evaluate the expression
83+
iter := query.Run(input)
84+
result, ok := iter.Next()
85+
if !ok {
86+
return nil, errors.New("no result from jq evaluation")
87+
}
88+
89+
// Check if an error occurred during evaluation
90+
if err, isErr := result.(error); isErr {
91+
return nil, fmt.Errorf("jq evaluation error: %w", err)
92+
}
93+
94+
return result, nil
95+
}

impl/context.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package impl
2+
3+
import (
4+
"context"
5+
"errors"
6+
"sync"
7+
)
8+
9+
type ctxKey string
10+
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{}
21+
}
22+
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
28+
}
29+
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
35+
}
36+
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
42+
}
43+
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
49+
}
50+
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
56+
}
57+
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
63+
}
64+
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{})
71+
}
72+
execCtx.Output[key] = value
73+
}
74+
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
81+
}
82+
83+
func WithExecutorContext(parent context.Context, wfCtx *ExecutorContext) context.Context {
84+
return context.WithValue(parent, executorCtxKey, wfCtx)
85+
}
86+
87+
func GetExecutorContext(ctx context.Context) (*ExecutorContext, error) {
88+
wfCtx, ok := ctx.Value(executorCtxKey).(*ExecutorContext)
89+
if !ok {
90+
return nil, errors.New("workflow context not found")
91+
}
92+
return wfCtx, nil
93+
}

impl/impl.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package impl
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/serverlessworkflow/sdk-go/v3/model"
7+
)
8+
9+
type StatusPhase string
10+
11+
const (
12+
PendingStatus StatusPhase = "pending"
13+
RunningStatus StatusPhase = "running"
14+
WaitingStatus StatusPhase = "waiting"
15+
CancelledStatus StatusPhase = "cancelled"
16+
FaultedStatus StatusPhase = "faulted"
17+
CompletedStatus StatusPhase = "completed"
18+
)
19+
20+
var _ WorkflowRunner = &workflowRunnerImpl{}
21+
22+
type WorkflowRunner interface {
23+
GetWorkflow() *model.Workflow
24+
Run(input map[string]interface{}) (output map[string]interface{}, err error)
25+
}
26+
27+
func NewDefaultRunner(workflow *model.Workflow) WorkflowRunner {
28+
// later we can implement the opts pattern to define context timeout, deadline, cancel, etc.
29+
// also fetch from the workflow model this information
30+
ctx := WithExecutorContext(context.Background(), &ExecutorContext{})
31+
return &workflowRunnerImpl{
32+
Workflow: workflow,
33+
Context: ctx,
34+
}
35+
}
36+
37+
type workflowRunnerImpl struct {
38+
Workflow *model.Workflow
39+
Context context.Context
40+
}
41+
42+
func (wr *workflowRunnerImpl) GetWorkflow() *model.Workflow {
43+
return wr.Workflow
44+
}
45+
46+
// Run the workflow.
47+
// TODO: Sync execution, we think about async later
48+
func (wr *workflowRunnerImpl) Run(input map[string]interface{}) (output map[string]interface{}, err error) {
49+
output = make(map[string]interface{})
50+
if input == nil {
51+
input = make(map[string]interface{})
52+
}
53+
54+
// TODO: validates input via wr.Workflow.Input.Schema
55+
56+
wfCtx, err := GetExecutorContext(wr.Context)
57+
if err != nil {
58+
return nil, err
59+
}
60+
wfCtx.SetInput(input)
61+
wfCtx.SetOutput(output)
62+
63+
// TODO: process wr.Workflow.Input.From, the result we set to WorkFlowCtx
64+
wfCtx.SetWorkflowCtx(input)
65+
66+
// Run tasks
67+
// For each task, execute.
68+
if wr.Workflow.Do != nil {
69+
for _, taskItem := range *wr.Workflow.Do {
70+
switch task := taskItem.Task.(type) {
71+
case *model.SetTask:
72+
exec, err := NewSetTaskExecutor(taskItem.Key, task)
73+
if err != nil {
74+
return nil, err
75+
}
76+
output, err = exec.Exec(wfCtx.GetWorkflowCtx())
77+
if err != nil {
78+
return nil, err
79+
}
80+
wfCtx.SetWorkflowCtx(output)
81+
default:
82+
return nil, fmt.Errorf("workflow does not support task '%T' named '%s'", task, taskItem.Key)
83+
}
84+
}
85+
}
86+
87+
// Process output and return
88+
89+
return output, err
90+
}

impl/task.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package impl
2+
3+
import (
4+
"fmt"
5+
"github.com/serverlessworkflow/sdk-go/v3/expr"
6+
"github.com/serverlessworkflow/sdk-go/v3/model"
7+
)
8+
9+
var _ TaskExecutor = &SetTaskExecutor{}
10+
11+
type TaskExecutor interface {
12+
Exec(input map[string]interface{}) (map[string]interface{}, error)
13+
}
14+
15+
type SetTaskExecutor struct {
16+
Task *model.SetTask
17+
TaskName string
18+
}
19+
20+
func NewSetTaskExecutor(taskName string, task *model.SetTask) (*SetTaskExecutor, error) {
21+
if task == nil || task.Set == nil {
22+
return nil, fmt.Errorf("no set configuration provided for SetTask %s", taskName)
23+
}
24+
return &SetTaskExecutor{
25+
Task: task,
26+
TaskName: taskName,
27+
}, nil
28+
}
29+
30+
func (s *SetTaskExecutor) Exec(input map[string]interface{}) (output map[string]interface{}, err error) {
31+
setObject := deepClone(s.Task.Set)
32+
result, err := expr.TraverseAndEvaluate(setObject, input)
33+
if err != nil {
34+
return nil, fmt.Errorf("failed to execute Set task '%s': %w", s.TaskName, err)
35+
}
36+
37+
output, ok := result.(map[string]interface{})
38+
if !ok {
39+
return nil, fmt.Errorf("expected output to be a map[string]interface{}, but got a different type. Got: %v", result)
40+
}
41+
42+
return output, nil
43+
}

0 commit comments

Comments
 (0)