├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── bug-report.md
│ ├── enhancement.md
│ └── question.md
├── OWNERS
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── Go-SDK-PR-Check.yaml
│ └── stale.yaml
├── .gitignore
├── .lift.toml
├── .whitesource
├── LICENSE
├── MAINTAINERS.md
├── Makefile
├── OWNERS
├── README.md
├── builder
├── builder.go
└── builder_test.go
├── code-of-conduct.md
├── codecov.yaml
├── contrib
└── intellij.editorconfig
├── go.mod
├── go.sum
├── hack
├── boilerplate.txt
├── go-lint.sh
└── integration-test.sh
├── impl
├── ctx
│ ├── context.go
│ └── status_phase.go
├── expr
│ ├── expr.go
│ └── expr_test.go
├── json_pointer.go
├── json_pointer_test.go
├── runner.go
├── runner_test.go
├── task_runner.go
├── task_runner_call_http.go
├── task_runner_do.go
├── task_runner_for.go
├── task_runner_fork.go
├── task_runner_fork_test.go
├── task_runner_raise.go
├── task_runner_raise_test.go
├── task_runner_set.go
├── task_runner_set_test.go
├── testdata
│ ├── chained_set_tasks.yaml
│ ├── concatenating_strings.yaml
│ ├── conditional_logic.yaml
│ ├── conditional_logic_input_from.yaml
│ ├── for_colors.yaml
│ ├── for_nested_loops.yaml
│ ├── for_sum_numbers.yaml
│ ├── fork_simple.yaml
│ ├── raise_conditional.yaml
│ ├── raise_error_with_input.yaml
│ ├── raise_inline.yaml
│ ├── raise_reusable.yaml
│ ├── raise_undefined_reference.yaml
│ ├── sequential_set_colors.yaml
│ ├── sequential_set_colors_output_as.yaml
│ ├── set_tasks_invalid_then.yaml
│ ├── set_tasks_with_termination.yaml
│ ├── set_tasks_with_then.yaml
│ ├── switch_match.yaml
│ ├── switch_with_default.yaml
│ ├── task_export_schema.yaml
│ ├── task_input_schema.yaml
│ ├── task_output_schema.yaml
│ ├── task_output_schema_with_dynamic_value.yaml
│ └── workflow_input_schema.yaml
└── utils
│ ├── json_schema.go
│ └── utils.go
├── maintainer_guidelines.md
├── model
├── authentication.go
├── authentication_oauth.go
├── authentication_oauth_test.go
├── authentication_test.go
├── builder.go
├── endpoint.go
├── endpoint_test.go
├── errors.go
├── extension.go
├── extension_test.go
├── objects.go
├── objects_test.go
├── runtime_expression.go
├── runtime_expression_test.go
├── task.go
├── task_call.go
├── task_call_test.go
├── task_do.go
├── task_do_test.go
├── task_event.go
├── task_event_test.go
├── task_for.go
├── task_for_test.go
├── task_fork.go
├── task_fork_test.go
├── task_raise.go
├── task_raise_test.go
├── task_run.go
├── task_run_test.go
├── task_set.go
├── task_set_test.go
├── task_switch.go
├── task_switch_test.go
├── task_test.go
├── task_try.go
├── task_try_test.go
├── task_wait.go
├── task_wait_test.go
├── timeout.go
├── timeout_test.go
├── validator.go
├── validator_test.go
├── workflow.go
└── workflow_test.go
├── parser
├── cmd
│ └── main.go
├── parser.go
├── parser_test.go
└── testdata
│ ├── invalid_workflow.yaml
│ ├── valid_workflow.json
│ └── valid_workflow.yaml
├── test
└── utils.go
├── tools.mod
└── tools.sum
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @ricardozanini
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Report a bug encountered with the Serverless Workflow Go SDK
4 | labels: "bug :bug:"
5 |
6 | ---
7 |
8 | **What happened**:
9 |
10 | **What you expected to happen**:
11 |
12 | **How to reproduce it**:
13 |
14 | **Anything else we need to know?**:
15 |
16 | **Environment**:
17 | - Specification version used:
18 |
19 | - Go version:
20 |
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/enhancement.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Enhancement Request
3 | about: Suggest an enhancement to the Serverless Workflow Go SDK
4 | labels: "enhancement :pray:"
5 |
6 | ---
7 |
8 | **What would you like to be added**:
9 |
10 | **Why is this needed**:
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask a question about the Serverless Workflow Go SDK
4 | labels: "question :question:"
5 |
6 | ---
7 |
8 | **What is the question**:
9 |
--------------------------------------------------------------------------------
/.github/OWNERS:
--------------------------------------------------------------------------------
1 | reviewers:
2 | - ricardozanini
3 | approvers:
4 | - ricardozanini
5 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **Many thanks for submitting your Pull Request :heart:!**
2 |
3 | **What this PR does / why we need it**:
4 |
5 | **Special notes for reviewers**:
6 |
7 | **Additional information (if needed):**
8 |
--------------------------------------------------------------------------------
/.github/workflows/Go-SDK-PR-Check.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | name: Go SDK PR Checks
16 |
17 | on:
18 | pull_request:
19 | paths-ignore:
20 | - "**.md"
21 | - "hack/**"
22 | - "LICENSE"
23 | - "Makefile"
24 | branches:
25 | - main
26 |
27 |
28 | permissions:
29 | contents: read
30 |
31 | env:
32 | GO_VERSION: 1.22
33 |
34 | jobs:
35 | basic_checks:
36 | name: Basic Checks
37 | runs-on: ubuntu-latest
38 | steps:
39 | - name: Checkout Code
40 | uses: actions/checkout@v4
41 |
42 | - name: Setup Go
43 | uses: actions/setup-go@v5
44 | with:
45 | go-version: ${{ env.GO_VERSION }}
46 | id: go
47 |
48 | - name: Cache Go Modules
49 | uses: actions/cache@v4
50 | with:
51 | path: |
52 | ~/.cache/go-build
53 | ~/go/pkg/mod
54 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
55 | restore-keys: |
56 | ${{ runner.os }}-go-
57 |
58 | - name: Cache Tools
59 | uses: actions/cache@v4
60 | with:
61 | path: ~/go/bin
62 | key: ${{ runner.os }}-go-tools-${{ hashFiles('**/tools.sum') }}
63 | restore-keys: |
64 | ${{ runner.os }}-go-tools-
65 |
66 | - name: Check Headers
67 | run: |
68 | make addheaders
69 | changed_files=$(git status -s | grep -v 'go.mod\|go.sum\|tools.mod\|tools.sum' || :)
70 | if [[ -n "$changed_files" ]]; then
71 | echo "❌ Some files are missing headers:\n$changed_files"
72 | exit 1
73 | fi
74 |
75 | - name: Check Formatting
76 | run: |
77 | make fmt
78 | changed_files=$(git status -s | grep -v 'go.mod\|go.sum\|tools.mod\|tools.sum' || :)
79 | if [[ -n "$changed_files" ]]; then
80 | echo "❌ Some files are not formatted correctly:\n$changed_files"
81 | exit 1
82 | fi
83 |
84 | - name: Run Linter
85 | uses: golangci/golangci-lint-action@ec5d18412c0aeab7936cb16880d708ba2a64e1ae # v6.1.1 - Please ALWAYS use SHA to avoid GH sec issues
86 | with:
87 | version: latest
88 |
89 | - name: Install Cover Tool
90 | run: go install golang.org/x/tools/cmd/cover@latest
91 |
92 | - name: Run Unit Tests
93 | run: go test ./... -coverprofile=test_coverage.out -covermode=atomic
94 |
95 | - name: Upload Coverage Report
96 | uses: actions/upload-artifact@v4
97 | with:
98 | name: Test Coverage Report
99 | path: test_coverage.out
100 |
101 | integration_tests:
102 | name: Integration Tests
103 | runs-on: ubuntu-latest
104 | needs: basic_checks
105 | steps:
106 | - name: Checkout Code
107 | uses: actions/checkout@v4
108 |
109 | - name: Setup Go
110 | uses: actions/setup-go@v5
111 | with:
112 | go-version: ${{ env.GO_VERSION }}
113 | id: go
114 |
115 | - name: Run Integration Tests
116 | run: |
117 | chmod +x ./hack/integration-test.sh
118 | ./hack/integration-test.sh
119 | continue-on-error: true
120 |
121 | - name: Upload JUnit Report
122 | if: always()
123 | uses: actions/upload-artifact@v4
124 | with:
125 | name: Integration Test JUnit Report
126 | path: ./integration-test-junit.xml
127 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2022 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | name: Mark stale issues and pull requests
16 | on:
17 | schedule:
18 | - cron: "0 0 * * *"
19 | permissions:
20 | issues: write
21 | pull-requests: write
22 | jobs:
23 | stale:
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/stale@v3
27 | with:
28 | repo-token: ${{ secrets.GITHUB_TOKEN }}
29 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
30 | stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
31 | stale-issue-label: 'Stale Issue'
32 | exempt-issue-labels: 'Status: Blocked, Status: In progress, Status: On hold, Status: Awaiting response'
33 | stale-pr-label: 'Stale PR'
34 | exempt-pr-labels: 'Status: Blocked, Status: In progress, Status: On hold, Status: Awaiting response'
35 | days-before-stale: 45
36 | days-before-close: 20
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | .idea
3 | *.out
4 | .vscode
5 |
6 | integration-test-junit.xml
7 |
--------------------------------------------------------------------------------
/.lift.toml:
--------------------------------------------------------------------------------
1 | ignoreFiles = """
2 | model/zz_generated.deepcopy.go
3 | """
--------------------------------------------------------------------------------
/.whitesource:
--------------------------------------------------------------------------------
1 | {
2 | "scanSettings": {
3 | "baseBranches": []
4 | },
5 | "checkRunSettings": {
6 | "vulnerableCheckRunConclusionLevel": "failure",
7 | "displayMode": "diff"
8 | },
9 | "issueSettings": {
10 | "minSeverityLevel": "LOW",
11 | "issueType": "DEPENDENCY"
12 | }
13 | }
--------------------------------------------------------------------------------
/MAINTAINERS.md:
--------------------------------------------------------------------------------
1 | # Serverless Workflow Go SDK Maintainers
2 |
3 | * [Ricardo Zanini](https://github.com/ricardozanini)
4 | * [Filippe Spolti](https://github.com/spolti)
5 | *
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | addheaders:
2 | @command -v addlicense > /dev/null || (echo "🚀 Installing addlicense..."; go install -modfile=tools.mod -v github.com/google/addlicense)
3 | @addlicense -c "The Serverless Workflow Specification Authors" -l apache .
4 |
5 | fmt:
6 | @go vet ./...
7 | @go fmt ./...
8 |
9 | goimports:
10 | @command -v goimports > /dev/null || (echo "🚀 Installing goimports..."; go install golang.org/x/tools/cmd/goimports@latest)
11 | @goimports -w .
12 |
13 | lint:
14 | @echo "🚀 Installing/updating golangci-lint…"
15 | GO111MODULE=on go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
16 |
17 | @echo "🚀 Running lint…"
18 | @make addheaders
19 | @make goimports
20 | @make fmt
21 | @$(GOPATH)/bin/golangci-lint run ./... ${params}
22 | @echo "✅ Linting completed!"
23 |
24 | .PHONY: test
25 | coverage="false"
26 |
27 | test:
28 | @echo "🧪 Running tests..."
29 | @go test ./...
30 | @echo "✅ Tests completed!"
31 |
32 | .PHONY: integration-test
33 |
34 | integration-test:
35 | @echo "🔄 Running integration tests..."
36 | @./hack/integration-test.sh
37 | @echo "✅ Integration tests completed!"
--------------------------------------------------------------------------------
/OWNERS:
--------------------------------------------------------------------------------
1 | # List of usernames who may use /lgtm
2 | reviewers:
3 | - ricardozanini
4 | - spolti
5 |
6 | # List of usernames who may use /approve
7 | approvers:
8 | - ricardozanini
--------------------------------------------------------------------------------
/builder/builder.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package builder
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 |
21 | "github.com/serverlessworkflow/sdk-go/v3/model"
22 |
23 | "sigs.k8s.io/yaml"
24 | )
25 |
26 | // New initializes a new WorkflowBuilder instance.
27 | func New() *model.WorkflowBuilder {
28 | return model.NewWorkflowBuilder()
29 | }
30 |
31 | // Yaml generates YAML output from the WorkflowBuilder using custom MarshalYAML implementations.
32 | func Yaml(builder *model.WorkflowBuilder) ([]byte, error) {
33 | workflow, err := Object(builder)
34 | if err != nil {
35 | return nil, fmt.Errorf("failed to build workflow object: %w", err)
36 | }
37 | return yaml.Marshal(workflow)
38 | }
39 |
40 | // Json generates JSON output from the WorkflowBuilder.
41 | func Json(builder *model.WorkflowBuilder) ([]byte, error) {
42 | workflow, err := Object(builder)
43 | if err != nil {
44 | return nil, fmt.Errorf("failed to build workflow object: %w", err)
45 | }
46 | return json.MarshalIndent(workflow, "", " ")
47 | }
48 |
49 | // Object builds and validates the Workflow object from the builder.
50 | func Object(builder *model.WorkflowBuilder) (*model.Workflow, error) {
51 | workflow := builder.Build()
52 |
53 | // Validate the workflow object
54 | if err := model.GetValidator().Struct(workflow); err != nil {
55 | return nil, fmt.Errorf("workflow validation failed: %w", err)
56 | }
57 |
58 | return workflow, nil
59 | }
60 |
61 | // Validate validates any given object using the Workflow model validator.
62 | func Validate(object interface{}) error {
63 | if err := model.GetValidator().Struct(object); err != nil {
64 | return fmt.Errorf("validation failed: %w", err)
65 | }
66 | return nil
67 | }
68 |
--------------------------------------------------------------------------------
/builder/builder_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package builder
16 |
17 | import (
18 | "errors"
19 | "testing"
20 |
21 | validator "github.com/go-playground/validator/v10"
22 | "github.com/serverlessworkflow/sdk-go/v3/model"
23 | "github.com/serverlessworkflow/sdk-go/v3/test"
24 |
25 | "github.com/stretchr/testify/assert"
26 | )
27 |
28 | func TestBuilder_Yaml(t *testing.T) {
29 | builder := New().
30 | SetDocument("1.0.0", "examples", "example-workflow", "1.0.0").
31 | AddTask("task1", &model.CallHTTP{
32 | TaskBase: model.TaskBase{
33 | If: &model.RuntimeExpression{Value: "${condition}"},
34 | },
35 | Call: "http",
36 | With: model.HTTPArguments{
37 | Method: "GET",
38 | Endpoint: model.NewEndpoint("http://example.com"),
39 | },
40 | })
41 |
42 | // Generate YAML from the builder
43 | yamlData, err := Yaml(builder)
44 | assert.NoError(t, err)
45 |
46 | // Define the expected YAML structure
47 | expectedYAML := `document:
48 | dsl: 1.0.0
49 | namespace: examples
50 | name: example-workflow
51 | version: 1.0.0
52 | do:
53 | - task1:
54 | call: http
55 | if: ${condition}
56 | with:
57 | method: GET
58 | endpoint: http://example.com
59 | `
60 |
61 | // Use assertYAMLEq to compare YAML structures
62 | test.AssertYAMLEq(t, expectedYAML, string(yamlData))
63 | }
64 |
65 | func TestBuilder_Json(t *testing.T) {
66 | builder := New().
67 | SetDocument("1.0.0", "examples", "example-workflow", "1.0.0").
68 | AddTask("task1", &model.CallHTTP{
69 | TaskBase: model.TaskBase{
70 | If: &model.RuntimeExpression{Value: "${condition}"},
71 | },
72 | Call: "http",
73 | With: model.HTTPArguments{
74 | Method: "GET",
75 | Endpoint: model.NewEndpoint("http://example.com"),
76 | },
77 | })
78 |
79 | jsonData, err := Json(builder)
80 | assert.NoError(t, err)
81 |
82 | expectedJSON := `{
83 | "document": {
84 | "dsl": "1.0.0",
85 | "namespace": "examples",
86 | "name": "example-workflow",
87 | "version": "1.0.0"
88 | },
89 | "do": [
90 | {
91 | "task1": {
92 | "call": "http",
93 | "if": "${condition}",
94 | "with": {
95 | "method": "GET",
96 | "endpoint": "http://example.com"
97 | }
98 | }
99 | }
100 | ]
101 | }`
102 | assert.JSONEq(t, expectedJSON, string(jsonData))
103 | }
104 |
105 | func TestBuilder_Object(t *testing.T) {
106 | builder := New().
107 | SetDocument("1.0.0", "examples", "example-workflow", "1.0.0").
108 | AddTask("task1", &model.CallHTTP{
109 | TaskBase: model.TaskBase{
110 | If: &model.RuntimeExpression{Value: "${condition}"},
111 | },
112 | Call: "http",
113 | With: model.HTTPArguments{
114 | Method: "GET",
115 | Endpoint: model.NewEndpoint("http://example.com"),
116 | },
117 | })
118 |
119 | workflow, err := Object(builder)
120 | assert.NoError(t, err)
121 | assert.NotNil(t, workflow)
122 |
123 | assert.Equal(t, "1.0.0", workflow.Document.DSL)
124 | assert.Equal(t, "examples", workflow.Document.Namespace)
125 | assert.Equal(t, "example-workflow", workflow.Document.Name)
126 | assert.Equal(t, "1.0.0", workflow.Document.Version)
127 | assert.Len(t, *workflow.Do, 1)
128 | assert.Equal(t, "http", (*workflow.Do)[0].Task.(*model.CallHTTP).Call)
129 | }
130 |
131 | func TestBuilder_Validate(t *testing.T) {
132 | workflow := &model.Workflow{
133 | Document: model.Document{
134 | DSL: "1.0.0",
135 | Namespace: "examples",
136 | Name: "example-workflow",
137 | Version: "1.0.0",
138 | },
139 | Do: &model.TaskList{
140 | &model.TaskItem{
141 | Key: "task1",
142 | Task: &model.CallHTTP{
143 | Call: "http",
144 | With: model.HTTPArguments{
145 | Method: "GET",
146 | Endpoint: model.NewEndpoint("http://example.com"),
147 | },
148 | },
149 | },
150 | },
151 | }
152 |
153 | err := Validate(workflow)
154 | assert.NoError(t, err)
155 |
156 | // Test validation failure
157 | workflow.Do = &model.TaskList{
158 | &model.TaskItem{
159 | Key: "task2",
160 | Task: &model.CallHTTP{
161 | Call: "http",
162 | With: model.HTTPArguments{
163 | Method: "GET", // Missing Endpoint
164 | },
165 | },
166 | },
167 | }
168 | err = Validate(workflow)
169 | assert.Error(t, err)
170 |
171 | var validationErrors validator.ValidationErrors
172 | if errors.As(err, &validationErrors) {
173 | t.Logf("Validation errors: %v", validationErrors)
174 | assert.Contains(t, validationErrors.Error(), "Do[0].Task.With.Endpoint")
175 | assert.Contains(t, validationErrors.Error(), "required")
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/code-of-conduct.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).
4 |
5 |
10 | Please contact the [CNCF Code of Conduct Committee](mailto:conduct@cncf.io)
11 | in order to report violations of the Code of Conduct.
12 |
--------------------------------------------------------------------------------
/codecov.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2020 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | comment:
16 | require_changes: true
17 | coverage:
18 | status:
19 | project:
20 | sdk-go:
21 | target: auto
22 | base: auto
23 | threshold: 5%
--------------------------------------------------------------------------------
/contrib/intellij.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [{*.go,*.go2}]
4 | indent_style = tab
5 | ij_continuation_indent_size = 4
6 | ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true
7 | ij_go_add_leading_space_to_comments = true
8 | ij_go_add_parentheses_for_single_import = false
9 | ij_go_call_parameters_new_line_after_left_paren = true
10 | ij_go_call_parameters_right_paren_on_new_line = true
11 | ij_go_call_parameters_wrap = off
12 | ij_go_fill_paragraph_width = 80
13 | ij_go_group_stdlib_imports = true
14 | ij_go_import_sorting = goimports
15 | ij_go_keep_indents_on_empty_lines = false
16 | ij_go_local_group_mode = project
17 | ij_go_move_all_imports_in_one_declaration = true
18 | ij_go_move_all_stdlib_imports_in_one_group = false
19 | ij_go_remove_redundant_import_aliases = true
20 | ij_go_run_go_fmt_on_reformat = true
21 | ij_go_use_back_quotes_for_imports = false
22 | ij_go_wrap_comp_lit = off
23 | ij_go_wrap_comp_lit_newline_after_lbrace = true
24 | ij_go_wrap_comp_lit_newline_before_rbrace = true
25 | ij_go_wrap_func_params = off
26 | ij_go_wrap_func_params_newline_after_lparen = true
27 | ij_go_wrap_func_params_newline_before_rparen = true
28 | ij_go_wrap_func_result = off
29 | ij_go_wrap_func_result_newline_after_lparen = true
30 | ij_go_wrap_func_result_newline_before_rparen = true
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/serverlessworkflow/sdk-go/v3
2 |
3 | go 1.23.0
4 |
5 | toolchain go1.24.0
6 |
7 | require (
8 | github.com/go-playground/validator/v10 v10.25.0
9 | github.com/google/uuid v1.6.0
10 | github.com/itchyny/gojq v0.12.17
11 | github.com/stretchr/testify v1.10.0
12 | github.com/tidwall/gjson v1.18.0
13 | github.com/xeipuuv/gojsonschema v1.2.0
14 | sigs.k8s.io/yaml v1.4.0
15 | )
16 |
17 | require (
18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
19 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect
20 | github.com/go-playground/locales v0.14.1 // indirect
21 | github.com/go-playground/universal-translator v0.18.1 // indirect
22 | github.com/google/go-cmp v0.7.0 // indirect
23 | github.com/itchyny/timefmt-go v0.1.6 // indirect
24 | github.com/leodido/go-urn v1.4.0 // indirect
25 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
26 | github.com/tidwall/match v1.1.1 // indirect
27 | github.com/tidwall/pretty v1.2.1 // indirect
28 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
29 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
30 | golang.org/x/crypto v0.36.0 // indirect
31 | golang.org/x/net v0.38.0 // indirect
32 | golang.org/x/sys v0.31.0 // indirect
33 | golang.org/x/text v0.23.0 // indirect
34 | gopkg.in/yaml.v3 v3.0.1 // indirect
35 | )
36 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
3 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
5 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
6 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
7 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
8 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
9 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
10 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
11 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
12 | github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
13 | github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
14 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
15 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
16 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
17 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
18 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
19 | github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg=
20 | github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY=
21 | github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
22 | github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
23 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
24 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
26 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
27 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
29 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
30 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
31 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
32 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
33 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
34 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
35 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
36 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
37 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
38 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
39 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
40 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
41 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
42 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
43 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
44 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
45 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
46 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
47 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
48 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
49 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
50 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
51 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
52 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
53 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
55 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
56 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
57 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
58 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
59 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
60 |
--------------------------------------------------------------------------------
/hack/boilerplate.txt:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
--------------------------------------------------------------------------------
/hack/go-lint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2020 The Serverless Workflow Specification Authors
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | golangci-lint run -E goimports -E errorlint -E gosec ${1} ./... --timeout 2m0s
17 |
--------------------------------------------------------------------------------
/hack/integration-test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2025 The Serverless Workflow Specification Authors
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 |
17 | # Script to fetch workflow examples, parse, and validate them using the Go parser.
18 |
19 | # Variables
20 | SPEC_REPO="https://github.com/serverlessworkflow/specification"
21 | EXAMPLES_DIR="examples"
22 | PARSER_BINARY="./parser/cmd/main.go"
23 | JUNIT_FILE="./integration-test-junit.xml"
24 |
25 | # Create a temporary directory
26 | TEMP_DIR=$(mktemp -d)
27 |
28 | # Ensure temporary directory was created
29 | if [ ! -d "$TEMP_DIR" ]; then
30 | echo "❌ Failed to create a temporary directory."
31 | exit 1
32 | fi
33 |
34 | # shellcheck disable=SC2317
35 | # Clean up the temporary directory on script exit
36 | cleanup() {
37 | echo "🧹 Cleaning up temporary directory..."
38 | rm -rf "$TEMP_DIR"
39 | }
40 | trap cleanup EXIT
41 |
42 | # Fetch the examples directory
43 | echo "📥 Fetching workflow examples from ${SPEC_REPO}/${EXAMPLES_DIR}..."
44 | if ! git clone --depth=1 --filter=blob:none --sparse "$SPEC_REPO" "$TEMP_DIR" &> /dev/null; then
45 | echo "❌ Failed to clone specification repository."
46 | exit 1
47 | fi
48 |
49 | cd "$TEMP_DIR" || exit
50 | if ! git sparse-checkout set "$EXAMPLES_DIR" &> /dev/null; then
51 | echo "❌ Failed to checkout examples directory."
52 | exit 1
53 | fi
54 |
55 | cd - || exit
56 |
57 | # Prepare JUnit XML output
58 | echo '' > "$JUNIT_FILE"
59 | echo '' >> "$JUNIT_FILE"
60 |
61 | # Initialize test summary
62 | total_tests=0
63 | failed_tests=0
64 |
65 | # Walk through files and validate
66 | echo "⚙️ Running parser on fetched examples..."
67 | while IFS= read -r file; do
68 | filename=$(basename "$file")
69 | echo "🔍 Validating: $filename"
70 |
71 | # Run the parser for the file
72 | if go run "$PARSER_BINARY" "$file" > "$TEMP_DIR/validation.log" 2>&1; then
73 | echo "✅ Validation succeeded for $filename"
74 | echo " " >> "$JUNIT_FILE"
75 | else
76 | echo "❌ Validation failed for $filename"
77 | failure_message=$(cat "$TEMP_DIR/validation.log" | sed 's/&/&/g; s/</g; s/>/>/g')
78 | echo " " >> "$JUNIT_FILE"
79 | echo " " >> "$JUNIT_FILE"
80 | echo " " >> "$JUNIT_FILE"
81 | ((failed_tests++))
82 | fi
83 |
84 | ((total_tests++))
85 | done < <(find "$TEMP_DIR/$EXAMPLES_DIR" -type f \( -name "*.yaml" -o -name "*.yml" -o -name "*.json" \))
86 |
87 | # Finalize JUnit XML output
88 | echo '' >> "$JUNIT_FILE"
89 |
90 | # Display test summary
91 | if [ $failed_tests -ne 0 ]; then
92 | echo "❌ Validation failed for $failed_tests out of $total_tests workflows."
93 | exit 1
94 | else
95 | echo "✅ All $total_tests workflows validated successfully."
96 | fi
97 |
98 | exit 0
99 |
--------------------------------------------------------------------------------
/impl/ctx/status_phase.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package ctx
16 |
17 | import "time"
18 |
19 | type StatusPhase string
20 |
21 | const (
22 | // PendingStatus The workflow/task has been initiated and is pending execution.
23 | PendingStatus StatusPhase = "pending"
24 | // RunningStatus The workflow/task is currently in progress.
25 | RunningStatus StatusPhase = "running"
26 | // WaitingStatus The workflow/task execution is temporarily paused, awaiting either inbound event(s) or a specified time interval as defined by a wait task.
27 | WaitingStatus StatusPhase = "waiting"
28 | // SuspendedStatus The workflow/task execution has been manually paused by a user and will remain halted until explicitly resumed.
29 | SuspendedStatus StatusPhase = "suspended"
30 | // CancelledStatus The workflow/task execution has been terminated before completion.
31 | CancelledStatus StatusPhase = "cancelled"
32 | // FaultedStatus The workflow/task execution has encountered an error.
33 | FaultedStatus StatusPhase = "faulted"
34 | // CompletedStatus The workflow/task ran to completion.
35 | CompletedStatus StatusPhase = "completed"
36 | )
37 |
38 | func (s StatusPhase) String() string {
39 | return string(s)
40 | }
41 |
42 | type StatusPhaseLog struct {
43 | Timestamp int64 `json:"timestamp"`
44 | Status StatusPhase `json:"status"`
45 | }
46 |
47 | func NewStatusPhaseLog(status StatusPhase) StatusPhaseLog {
48 | return StatusPhaseLog{
49 | Status: status,
50 | Timestamp: time.Now().UnixMilli(),
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/impl/expr/expr.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package expr
16 |
17 | import (
18 | "context"
19 | "errors"
20 | "fmt"
21 |
22 | "github.com/itchyny/gojq"
23 | "github.com/serverlessworkflow/sdk-go/v3/impl/ctx"
24 | "github.com/serverlessworkflow/sdk-go/v3/model"
25 | )
26 |
27 | func TraverseAndEvaluateWithVars(node interface{}, input interface{}, variables map[string]interface{}, nodeContext context.Context) (interface{}, error) {
28 | if err := mergeContextInVars(nodeContext, variables); err != nil {
29 | return nil, err
30 | }
31 | return traverseAndEvaluate(node, input, variables)
32 | }
33 |
34 | // TraverseAndEvaluate recursively processes and evaluates all expressions in a JSON-like structure
35 | func TraverseAndEvaluate(node interface{}, input interface{}, nodeContext context.Context) (interface{}, error) {
36 | return TraverseAndEvaluateWithVars(node, input, map[string]interface{}{}, nodeContext)
37 | }
38 |
39 | func traverseAndEvaluate(node interface{}, input interface{}, variables map[string]interface{}) (interface{}, error) {
40 | switch v := node.(type) {
41 | case map[string]interface{}:
42 | // Traverse map
43 | for key, value := range v {
44 | evaluatedValue, err := traverseAndEvaluate(value, input, variables)
45 | if err != nil {
46 | return nil, err
47 | }
48 | v[key] = evaluatedValue
49 | }
50 | return v, nil
51 |
52 | case []interface{}:
53 | // Traverse array
54 | for i, value := range v {
55 | evaluatedValue, err := traverseAndEvaluate(value, input, variables)
56 | if err != nil {
57 | return nil, err
58 | }
59 | v[i] = evaluatedValue
60 | }
61 | return v, nil
62 |
63 | case string:
64 | // Check if the string is a runtime expression (e.g., ${ .some.path })
65 | if model.IsStrictExpr(v) {
66 | return evaluateJQExpression(model.SanitizeExpr(v), input, variables)
67 | }
68 | return v, nil
69 |
70 | default:
71 | // Return other types as-is
72 | return v, nil
73 | }
74 | }
75 |
76 | // evaluateJQExpression evaluates a jq expression against a given JSON input
77 | func evaluateJQExpression(expression string, input interface{}, variables map[string]interface{}) (interface{}, error) {
78 | query, err := gojq.Parse(expression)
79 | if err != nil {
80 | return nil, fmt.Errorf("failed to parse jq expression: %s, error: %w", expression, err)
81 | }
82 |
83 | // Get the variable names & values in a single pass:
84 | names, values := getVariableNamesAndValues(variables)
85 |
86 | code, err := gojq.Compile(query, gojq.WithVariables(names))
87 | if err != nil {
88 | return nil, fmt.Errorf("failed to compile jq expression: %s, error: %w", expression, err)
89 | }
90 |
91 | iter := code.Run(input, values...)
92 | result, ok := iter.Next()
93 | if !ok {
94 | return nil, errors.New("no result from jq evaluation")
95 | }
96 |
97 | // If there's an error from the jq engine, report it
98 | if errVal, isErr := result.(error); isErr {
99 | return nil, fmt.Errorf("jq evaluation error: %w", errVal)
100 | }
101 |
102 | return result, nil
103 | }
104 |
105 | // getVariableNamesAndValues constructs two slices, where 'names[i]' matches 'values[i]'.
106 | func getVariableNamesAndValues(vars map[string]interface{}) ([]string, []interface{}) {
107 | names := make([]string, 0, len(vars))
108 | values := make([]interface{}, 0, len(vars))
109 |
110 | for k, v := range vars {
111 | names = append(names, k)
112 | values = append(values, v)
113 | }
114 | return names, values
115 | }
116 |
117 | func mergeContextInVars(nodeCtx context.Context, variables map[string]interface{}) error {
118 | if variables == nil {
119 | variables = make(map[string]interface{})
120 | }
121 | wfCtx, err := ctx.GetWorkflowContext(nodeCtx)
122 | if err != nil {
123 | if errors.Is(err, ctx.ErrWorkflowContextNotFound) {
124 | return nil
125 | }
126 | return err
127 | }
128 | // merge
129 | for k, val := range wfCtx.GetVars() {
130 | variables[k] = val
131 | }
132 |
133 | return nil
134 | }
135 |
136 | func TraverseAndEvaluateObj(runtimeExpr *model.ObjectOrRuntimeExpr, input interface{}, taskName string, wfCtx context.Context) (output interface{}, err error) {
137 | if runtimeExpr == nil {
138 | return input, nil
139 | }
140 | output, err = TraverseAndEvaluate(runtimeExpr.AsStringOrMap(), input, wfCtx)
141 | if err != nil {
142 | return nil, model.NewErrExpression(err, taskName)
143 | }
144 | return output, nil
145 | }
146 |
147 | func TraverseAndEvaluateBool(runtimeExpr string, input interface{}, wfCtx context.Context) (bool, error) {
148 | if len(runtimeExpr) == 0 {
149 | return false, nil
150 | }
151 | output, err := TraverseAndEvaluate(runtimeExpr, input, wfCtx)
152 | if err != nil {
153 | return false, nil
154 | }
155 | if result, ok := output.(bool); ok {
156 | return result, nil
157 | }
158 | return false, nil
159 | }
160 |
--------------------------------------------------------------------------------
/impl/json_pointer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package impl
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | "reflect"
21 | "strings"
22 |
23 | "github.com/serverlessworkflow/sdk-go/v3/model"
24 | )
25 |
26 | func findJsonPointer(data interface{}, target string, path string) (string, bool) {
27 | switch node := data.(type) {
28 | case map[string]interface{}:
29 | for key, value := range node {
30 | newPath := fmt.Sprintf("%s/%s", path, key)
31 | if key == target {
32 | return newPath, true
33 | }
34 | if result, found := findJsonPointer(value, target, newPath); found {
35 | return result, true
36 | }
37 | }
38 | case []interface{}:
39 | for i, item := range node {
40 | newPath := fmt.Sprintf("%s/%d", path, i)
41 | if result, found := findJsonPointer(item, target, newPath); found {
42 | return result, true
43 | }
44 | }
45 | }
46 | return "", false
47 | }
48 |
49 | // GenerateJSONPointer Function to generate JSON Pointer from a Workflow reference
50 | func GenerateJSONPointer(workflow *model.Workflow, targetNode interface{}) (string, error) {
51 | // Convert struct to JSON
52 | jsonData, err := json.Marshal(workflow)
53 | if err != nil {
54 | return "", fmt.Errorf("error marshalling to JSON: %w", err)
55 | }
56 |
57 | // Convert JSON to a generic map for traversal
58 | var jsonMap map[string]interface{}
59 | if err := json.Unmarshal(jsonData, &jsonMap); err != nil {
60 | return "", fmt.Errorf("error unmarshalling JSON: %w", err)
61 | }
62 |
63 | transformedNode := ""
64 | switch node := targetNode.(type) {
65 | case string:
66 | transformedNode = node
67 | default:
68 | transformedNode = strings.ToLower(reflect.TypeOf(targetNode).Name())
69 | }
70 |
71 | // Search for the target node
72 | jsonPointer, found := findJsonPointer(jsonMap, transformedNode, "")
73 | if !found {
74 | return "", fmt.Errorf("node '%s' not found", targetNode)
75 | }
76 |
77 | return jsonPointer, nil
78 | }
79 |
--------------------------------------------------------------------------------
/impl/json_pointer_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package impl
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/serverlessworkflow/sdk-go/v3/model"
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | // TestGenerateJSONPointer_SimpleTask tests a simple workflow task.
25 | func TestGenerateJSONPointer_SimpleTask(t *testing.T) {
26 | workflow := &model.Workflow{
27 | Document: model.Document{Name: "simple-workflow"},
28 | Do: &model.TaskList{
29 | &model.TaskItem{Key: "task1", Task: &model.SetTask{Set: map[string]interface{}{"value": 10}}},
30 | &model.TaskItem{Key: "task2", Task: &model.SetTask{Set: map[string]interface{}{"double": "${ .value * 2 }"}}},
31 | },
32 | }
33 |
34 | jsonPointer, err := GenerateJSONPointer(workflow, "task2")
35 | assert.NoError(t, err)
36 | assert.Equal(t, "/do/1/task2", jsonPointer)
37 | }
38 |
39 | // TestGenerateJSONPointer_SimpleTask tests a simple workflow task.
40 | func TestGenerateJSONPointer_Document(t *testing.T) {
41 | workflow := &model.Workflow{
42 | Document: model.Document{Name: "simple-workflow"},
43 | Do: &model.TaskList{
44 | &model.TaskItem{Key: "task1", Task: &model.SetTask{Set: map[string]interface{}{"value": 10}}},
45 | &model.TaskItem{Key: "task2", Task: &model.SetTask{Set: map[string]interface{}{"double": "${ .value * 2 }"}}},
46 | },
47 | }
48 |
49 | jsonPointer, err := GenerateJSONPointer(workflow, workflow.Document)
50 | assert.NoError(t, err)
51 | assert.Equal(t, "/document", jsonPointer)
52 | }
53 |
54 | func TestGenerateJSONPointer_ForkTask(t *testing.T) {
55 | workflow := &model.Workflow{
56 | Document: model.Document{Name: "fork-example"},
57 | Do: &model.TaskList{
58 | &model.TaskItem{
59 | Key: "raiseAlarm",
60 | Task: &model.ForkTask{
61 | Fork: model.ForkTaskConfiguration{
62 | Compete: true,
63 | Branches: &model.TaskList{
64 | &model.TaskItem{Key: "callNurse", Task: &model.CallHTTP{Call: "http", With: model.HTTPArguments{Method: "put", Endpoint: model.NewEndpoint("https://hospital.com/api/alert/nurses")}}},
65 | &model.TaskItem{Key: "callDoctor", Task: &model.CallHTTP{Call: "http", With: model.HTTPArguments{Method: "put", Endpoint: model.NewEndpoint("https://hospital.com/api/alert/doctor")}}},
66 | },
67 | },
68 | },
69 | },
70 | },
71 | }
72 |
73 | jsonPointer, err := GenerateJSONPointer(workflow, "callDoctor")
74 | assert.NoError(t, err)
75 | assert.Equal(t, "/do/0/raiseAlarm/fork/branches/1/callDoctor", jsonPointer)
76 | }
77 |
78 | // TestGenerateJSONPointer_DeepNestedTask tests multiple nested task levels.
79 | func TestGenerateJSONPointer_DeepNestedTask(t *testing.T) {
80 | workflow := &model.Workflow{
81 | Document: model.Document{Name: "deep-nested"},
82 | Do: &model.TaskList{
83 | &model.TaskItem{
84 | Key: "step1",
85 | Task: &model.ForkTask{
86 | Fork: model.ForkTaskConfiguration{
87 | Compete: false,
88 | Branches: &model.TaskList{
89 | &model.TaskItem{
90 | Key: "branchA",
91 | Task: &model.ForkTask{
92 | Fork: model.ForkTaskConfiguration{
93 | Branches: &model.TaskList{
94 | &model.TaskItem{
95 | Key: "deepTask",
96 | Task: &model.SetTask{Set: map[string]interface{}{"result": "done"}},
97 | },
98 | },
99 | },
100 | },
101 | },
102 | },
103 | },
104 | },
105 | },
106 | },
107 | }
108 |
109 | jsonPointer, err := GenerateJSONPointer(workflow, "deepTask")
110 | assert.NoError(t, err)
111 | assert.Equal(t, "/do/0/step1/fork/branches/0/branchA/fork/branches/0/deepTask", jsonPointer)
112 | }
113 |
114 | // TestGenerateJSONPointer_NonExistentTask checks for a task that doesn't exist.
115 | func TestGenerateJSONPointer_NonExistentTask(t *testing.T) {
116 | workflow := &model.Workflow{
117 | Document: model.Document{Name: "nonexistent-test"},
118 | Do: &model.TaskList{
119 | &model.TaskItem{Key: "taskA", Task: &model.SetTask{Set: map[string]interface{}{"value": 5}}},
120 | },
121 | }
122 |
123 | _, err := GenerateJSONPointer(workflow, "taskX")
124 | assert.Error(t, err)
125 | }
126 |
127 | // TestGenerateJSONPointer_MixedTaskTypes verifies a workflow with different task types.
128 | func TestGenerateJSONPointer_MixedTaskTypes(t *testing.T) {
129 | workflow := &model.Workflow{
130 | Document: model.Document{Name: "mixed-tasks"},
131 | Do: &model.TaskList{
132 | &model.TaskItem{Key: "compute", Task: &model.SetTask{Set: map[string]interface{}{"result": 42}}},
133 | &model.TaskItem{Key: "notify", Task: &model.CallHTTP{Call: "http", With: model.HTTPArguments{Method: "post", Endpoint: model.NewEndpoint("https://api.notify.com")}}},
134 | },
135 | }
136 |
137 | jsonPointer, err := GenerateJSONPointer(workflow, "notify")
138 | assert.NoError(t, err)
139 | assert.Equal(t, "/do/1/notify", jsonPointer)
140 | }
141 |
--------------------------------------------------------------------------------
/impl/task_runner.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package impl
16 |
17 | import (
18 | "context"
19 | "time"
20 |
21 | "github.com/serverlessworkflow/sdk-go/v3/impl/ctx"
22 | "github.com/serverlessworkflow/sdk-go/v3/model"
23 | )
24 |
25 | var _ TaskRunner = &SetTaskRunner{}
26 | var _ TaskRunner = &RaiseTaskRunner{}
27 | var _ TaskRunner = &ForTaskRunner{}
28 | var _ TaskRunner = &DoTaskRunner{}
29 |
30 | type TaskRunner interface {
31 | Run(input interface{}, taskSupport TaskSupport) (interface{}, error)
32 | GetTaskName() string
33 | }
34 |
35 | type TaskSupport interface {
36 | SetTaskStatus(task string, status ctx.StatusPhase)
37 | GetWorkflowDef() *model.Workflow
38 | // SetWorkflowInstanceCtx is the `$context` variable accessible in JQ expressions and set in `export.as`
39 | SetWorkflowInstanceCtx(value interface{})
40 | // GetContext gets the sharable Workflow context. Accessible via ctx.GetWorkflowContext.
41 | GetContext() context.Context
42 | SetTaskRawInput(value interface{})
43 | SetTaskRawOutput(value interface{})
44 | SetTaskDef(task model.Task) error
45 | SetTaskStartedAt(value time.Time)
46 | SetTaskName(name string)
47 | // SetTaskReferenceFromName based on the taskName and the model.Workflow definition, set the JSON Pointer reference to the context
48 | SetTaskReferenceFromName(taskName string) error
49 | GetTaskReference() string
50 | // SetLocalExprVars overrides local variables in expression processing
51 | SetLocalExprVars(vars map[string]interface{})
52 | // AddLocalExprVars adds to the local variables in expression processing. Won't override previous entries.
53 | AddLocalExprVars(vars map[string]interface{})
54 | // RemoveLocalExprVars removes local variables added in AddLocalExprVars or SetLocalExprVars
55 | RemoveLocalExprVars(keys ...string)
56 | // CloneWithContext returns a full clone of this TaskSupport, but using
57 | // the provided context.Context (so deadlines/cancellations propagate).
58 | CloneWithContext(ctx context.Context) TaskSupport
59 | }
60 |
--------------------------------------------------------------------------------
/impl/task_runner_call_http.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package impl
16 |
17 | import (
18 | "fmt"
19 |
20 | "github.com/serverlessworkflow/sdk-go/v3/model"
21 | )
22 |
23 | type CallHTTPTaskRunner struct {
24 | TaskName string
25 | }
26 |
27 | func NewCallHttpRunner(taskName string, task *model.CallHTTP) (taskRunner *CallHTTPTaskRunner, err error) {
28 | if task == nil {
29 | err = model.NewErrValidation(fmt.Errorf("invalid For task %s", taskName), taskName)
30 | } else {
31 | taskRunner = new(CallHTTPTaskRunner)
32 | taskRunner.TaskName = taskName
33 | }
34 | return
35 | }
36 |
37 | func (f *CallHTTPTaskRunner) Run(input interface{}, taskSupport TaskSupport) (interface{}, error) {
38 | return input, nil
39 |
40 | }
41 |
42 | func (f *CallHTTPTaskRunner) GetTaskName() string {
43 | return f.TaskName
44 | }
45 |
--------------------------------------------------------------------------------
/impl/task_runner_for.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package impl
16 |
17 | import (
18 | "fmt"
19 | "reflect"
20 | "strings"
21 |
22 | "github.com/serverlessworkflow/sdk-go/v3/impl/expr"
23 | "github.com/serverlessworkflow/sdk-go/v3/model"
24 | )
25 |
26 | const (
27 | forTaskDefaultEach = "$item"
28 | forTaskDefaultAt = "$index"
29 | )
30 |
31 | func NewForTaskRunner(taskName string, task *model.ForTask) (*ForTaskRunner, error) {
32 | if task == nil || task.Do == nil {
33 | return nil, model.NewErrValidation(fmt.Errorf("invalid For task %s", taskName), taskName)
34 | }
35 |
36 | doRunner, err := NewDoTaskRunner(task.Do)
37 | if err != nil {
38 | return nil, err
39 | }
40 |
41 | return &ForTaskRunner{
42 | Task: task,
43 | TaskName: taskName,
44 | DoRunner: doRunner,
45 | }, nil
46 | }
47 |
48 | type ForTaskRunner struct {
49 | Task *model.ForTask
50 | TaskName string
51 | DoRunner *DoTaskRunner
52 | }
53 |
54 | func (f *ForTaskRunner) Run(input interface{}, taskSupport TaskSupport) (interface{}, error) {
55 | defer func() {
56 | // clear local variables
57 | taskSupport.RemoveLocalExprVars(f.Task.For.Each, f.Task.For.At)
58 | }()
59 | f.sanitizeFor()
60 | in, err := expr.TraverseAndEvaluate(f.Task.For.In, input, taskSupport.GetContext())
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | forOutput := input
66 | rv := reflect.ValueOf(in)
67 | switch rv.Kind() {
68 | case reflect.Slice, reflect.Array:
69 | for i := 0; i < rv.Len(); i++ {
70 | item := rv.Index(i).Interface()
71 |
72 | if forOutput, err = f.processForItem(i, item, taskSupport, forOutput); err != nil {
73 | return nil, err
74 | }
75 | if f.Task.While != "" {
76 | whileIsTrue, err := expr.TraverseAndEvaluateBool(f.Task.While, forOutput, taskSupport.GetContext())
77 | if err != nil {
78 | return nil, err
79 | }
80 | if !whileIsTrue {
81 | break
82 | }
83 | }
84 | }
85 | case reflect.Invalid:
86 | return input, nil
87 | default:
88 | if forOutput, err = f.processForItem(0, in, taskSupport, forOutput); err != nil {
89 | return nil, err
90 | }
91 | }
92 |
93 | return forOutput, nil
94 | }
95 |
96 | func (f *ForTaskRunner) processForItem(idx int, item interface{}, taskSupport TaskSupport, forOutput interface{}) (interface{}, error) {
97 | forVars := map[string]interface{}{
98 | f.Task.For.At: idx,
99 | f.Task.For.Each: item,
100 | }
101 | // Instead of Set, we Add since other tasks in this very same context might be adding variables to the context
102 | taskSupport.AddLocalExprVars(forVars)
103 | // output from previous iterations are merged together
104 | var err error
105 | forOutput, err = f.DoRunner.Run(forOutput, taskSupport)
106 | if err != nil {
107 | return nil, err
108 | }
109 |
110 | return forOutput, nil
111 | }
112 |
113 | func (f *ForTaskRunner) sanitizeFor() {
114 | f.Task.For.Each = strings.TrimSpace(f.Task.For.Each)
115 | f.Task.For.At = strings.TrimSpace(f.Task.For.At)
116 |
117 | if f.Task.For.Each == "" {
118 | f.Task.For.Each = forTaskDefaultEach
119 | }
120 | if f.Task.For.At == "" {
121 | f.Task.For.At = forTaskDefaultAt
122 | }
123 |
124 | if !strings.HasPrefix(f.Task.For.Each, "$") {
125 | f.Task.For.Each = "$" + f.Task.For.Each
126 | }
127 | if !strings.HasPrefix(f.Task.For.At, "$") {
128 | f.Task.For.At = "$" + f.Task.For.At
129 | }
130 | }
131 |
132 | func (f *ForTaskRunner) GetTaskName() string {
133 | return f.TaskName
134 | }
135 |
--------------------------------------------------------------------------------
/impl/task_runner_fork.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package impl
16 |
17 | import (
18 | "context"
19 | "fmt"
20 | "sync"
21 |
22 | "github.com/serverlessworkflow/sdk-go/v3/model"
23 | )
24 |
25 | func NewForkTaskRunner(taskName string, task *model.ForkTask, workflowDef *model.Workflow) (*ForkTaskRunner, error) {
26 | if task == nil || task.Fork.Branches == nil {
27 | return nil, model.NewErrValidation(fmt.Errorf("invalid Fork task %s", taskName), taskName)
28 | }
29 |
30 | var runners []TaskRunner
31 | for _, branchItem := range *task.Fork.Branches {
32 | r, err := NewTaskRunner(branchItem.Key, branchItem.Task, workflowDef)
33 | if err != nil {
34 | return nil, err
35 | }
36 | runners = append(runners, r)
37 | }
38 |
39 | return &ForkTaskRunner{
40 | Task: task,
41 | TaskName: taskName,
42 | BranchRunners: runners,
43 | }, nil
44 | }
45 |
46 | type ForkTaskRunner struct {
47 | Task *model.ForkTask
48 | TaskName string
49 | BranchRunners []TaskRunner
50 | }
51 |
52 | func (f ForkTaskRunner) GetTaskName() string {
53 | return f.TaskName
54 | }
55 |
56 | func (f ForkTaskRunner) Run(input interface{}, parentSupport TaskSupport) (interface{}, error) {
57 | cancelCtx, cancel := context.WithCancel(parentSupport.GetContext())
58 | defer cancel()
59 |
60 | n := len(f.BranchRunners)
61 | results := make([]interface{}, n)
62 | errs := make(chan error, n)
63 | done := make(chan struct{})
64 | resultCh := make(chan interface{}, 1)
65 |
66 | var (
67 | wg sync.WaitGroup
68 | once sync.Once // <-- declare a Once
69 | )
70 |
71 | for i, runner := range f.BranchRunners {
72 | wg.Add(1)
73 | go func(i int, runner TaskRunner) {
74 | defer wg.Done()
75 | // **Isolate context** for each branch!
76 | branchSupport := parentSupport.CloneWithContext(cancelCtx)
77 |
78 | select {
79 | case <-cancelCtx.Done():
80 | return
81 | default:
82 | }
83 |
84 | out, err := runner.Run(input, branchSupport)
85 | if err != nil {
86 | errs <- err
87 | return
88 | }
89 | results[i] = out
90 |
91 | if f.Task.Fork.Compete {
92 | select {
93 | case resultCh <- out:
94 | once.Do(func() {
95 | cancel() // **signal cancellation** to all other branches
96 | close(done) // signal we have a winner
97 | })
98 | default:
99 | }
100 | }
101 | }(i, runner)
102 | }
103 |
104 | if f.Task.Fork.Compete {
105 | select {
106 | case <-done:
107 | return <-resultCh, nil
108 | case err := <-errs:
109 | return nil, err
110 | }
111 | }
112 |
113 | wg.Wait()
114 | select {
115 | case err := <-errs:
116 | return nil, err
117 | default:
118 | }
119 | return results, nil
120 | }
121 |
--------------------------------------------------------------------------------
/impl/task_runner_fork_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package impl
16 |
17 | import (
18 | "context"
19 | "testing"
20 | "time"
21 |
22 | "github.com/serverlessworkflow/sdk-go/v3/model"
23 | "github.com/stretchr/testify/assert"
24 | )
25 |
26 | // dummyRunner simulates a TaskRunner that returns its name after an optional delay.
27 | type dummyRunner struct {
28 | name string
29 | delay time.Duration
30 | }
31 |
32 | func (d *dummyRunner) GetTaskName() string {
33 | return d.name
34 | }
35 |
36 | func (d *dummyRunner) Run(input interface{}, ts TaskSupport) (interface{}, error) {
37 | select {
38 | case <-ts.GetContext().Done():
39 | // canceled
40 | return nil, ts.GetContext().Err()
41 | case <-time.After(d.delay):
42 | // complete after delay
43 | return d.name, nil
44 | }
45 | }
46 |
47 | func TestForkTaskRunner_NonCompete(t *testing.T) {
48 | // Prepare a TaskSupport with a background context
49 | ts := newTaskSupport(withContext(context.Background()))
50 |
51 | // Two branches that complete immediately
52 | branches := []TaskRunner{
53 | &dummyRunner{name: "r1", delay: 0},
54 | &dummyRunner{name: "r2", delay: 0},
55 | }
56 | fork := ForkTaskRunner{
57 | Task: &model.ForkTask{
58 | Fork: model.ForkTaskConfiguration{
59 | Compete: false,
60 | },
61 | },
62 | TaskName: "fork",
63 | BranchRunners: branches,
64 | }
65 |
66 | output, err := fork.Run("in", ts)
67 | assert.NoError(t, err)
68 |
69 | results, ok := output.([]interface{})
70 | assert.True(t, ok, "expected output to be []interface{}")
71 | assert.Equal(t, []interface{}{"r1", "r2"}, results)
72 | }
73 |
74 | func TestForkTaskRunner_Compete(t *testing.T) {
75 | // Prepare a TaskSupport with a background context
76 | ts := newTaskSupport(withContext(context.Background()))
77 |
78 | // One fast branch and one slow branch
79 | branches := []TaskRunner{
80 | &dummyRunner{name: "fast", delay: 10 * time.Millisecond},
81 | &dummyRunner{name: "slow", delay: 50 * time.Millisecond},
82 | }
83 | fork := ForkTaskRunner{
84 | Task: &model.ForkTask{
85 | Fork: model.ForkTaskConfiguration{
86 | Compete: true,
87 | },
88 | },
89 | TaskName: "fork",
90 | BranchRunners: branches,
91 | }
92 |
93 | start := time.Now()
94 | output, err := fork.Run("in", ts)
95 | elapsed := time.Since(start)
96 |
97 | assert.NoError(t, err)
98 | assert.Equal(t, "fast", output)
99 | // ensure compete returns before the slow branch would finish
100 | assert.Less(t, elapsed, 50*time.Millisecond, "compete should cancel the slow branch")
101 | }
102 |
--------------------------------------------------------------------------------
/impl/task_runner_raise.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package impl
16 |
17 | import (
18 | "fmt"
19 |
20 | "github.com/serverlessworkflow/sdk-go/v3/impl/expr"
21 | "github.com/serverlessworkflow/sdk-go/v3/model"
22 | )
23 |
24 | func NewRaiseTaskRunner(taskName string, task *model.RaiseTask, workflowDef *model.Workflow) (*RaiseTaskRunner, error) {
25 | if err := resolveErrorDefinition(task, workflowDef); err != nil {
26 | return nil, err
27 | }
28 |
29 | if task.Raise.Error.Definition == nil {
30 | return nil, model.NewErrValidation(fmt.Errorf("no raise configuration provided for RaiseTask %s", taskName), taskName)
31 | }
32 | return &RaiseTaskRunner{
33 | Task: task,
34 | TaskName: taskName,
35 | }, nil
36 | }
37 |
38 | // TODO: can e refactored to a definition resolver callable from the context
39 | func resolveErrorDefinition(t *model.RaiseTask, workflowDef *model.Workflow) error {
40 | if workflowDef != nil && t.Raise.Error.Ref != nil {
41 | notFoundErr := model.NewErrValidation(fmt.Errorf("%v error definition not found in 'uses'", t.Raise.Error.Ref), "")
42 | if workflowDef.Use != nil && workflowDef.Use.Errors != nil {
43 | definition, ok := workflowDef.Use.Errors[*t.Raise.Error.Ref]
44 | if !ok {
45 | return notFoundErr
46 | }
47 | t.Raise.Error.Definition = definition
48 | return nil
49 | }
50 | return notFoundErr
51 | }
52 | return nil
53 | }
54 |
55 | type RaiseTaskRunner struct {
56 | Task *model.RaiseTask
57 | TaskName string
58 | }
59 |
60 | var raiseErrFuncMapping = map[string]func(error, string) *model.Error{
61 | model.ErrorTypeAuthentication: model.NewErrAuthentication,
62 | model.ErrorTypeValidation: model.NewErrValidation,
63 | model.ErrorTypeCommunication: model.NewErrCommunication,
64 | model.ErrorTypeAuthorization: model.NewErrAuthorization,
65 | model.ErrorTypeConfiguration: model.NewErrConfiguration,
66 | model.ErrorTypeExpression: model.NewErrExpression,
67 | model.ErrorTypeRuntime: model.NewErrRuntime,
68 | model.ErrorTypeTimeout: model.NewErrTimeout,
69 | }
70 |
71 | func (r *RaiseTaskRunner) Run(input interface{}, taskSupport TaskSupport) (output interface{}, err error) {
72 | output = input
73 | // TODO: make this an external func so we can call it after getting the reference? Or we can get the reference from the workflow definition
74 | var detailResult interface{}
75 | detailResult, err = expr.TraverseAndEvaluateObj(r.Task.Raise.Error.Definition.Detail.AsObjectOrRuntimeExpr(), input, r.TaskName, taskSupport.GetContext())
76 | if err != nil {
77 | return nil, err
78 | }
79 |
80 | var titleResult interface{}
81 | titleResult, err = expr.TraverseAndEvaluateObj(r.Task.Raise.Error.Definition.Title.AsObjectOrRuntimeExpr(), input, r.TaskName, taskSupport.GetContext())
82 | if err != nil {
83 | return nil, err
84 | }
85 |
86 | instance := taskSupport.GetTaskReference()
87 |
88 | var raiseErr *model.Error
89 | if raiseErrF, ok := raiseErrFuncMapping[r.Task.Raise.Error.Definition.Type.String()]; ok {
90 | raiseErr = raiseErrF(fmt.Errorf("%v", detailResult), instance)
91 | } else {
92 | raiseErr = r.Task.Raise.Error.Definition
93 | raiseErr.Detail = model.NewStringOrRuntimeExpr(fmt.Sprintf("%v", detailResult))
94 | raiseErr.Instance = &model.JsonPointerOrRuntimeExpression{Value: instance}
95 | }
96 |
97 | raiseErr.Title = model.NewStringOrRuntimeExpr(fmt.Sprintf("%v", titleResult))
98 | err = raiseErr
99 |
100 | return output, err
101 | }
102 |
103 | func (r *RaiseTaskRunner) GetTaskName() string {
104 | return r.TaskName
105 | }
106 |
--------------------------------------------------------------------------------
/impl/task_runner_raise_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package impl
16 |
17 | import (
18 | "encoding/json"
19 | "errors"
20 | "testing"
21 |
22 | "github.com/serverlessworkflow/sdk-go/v3/impl/ctx"
23 |
24 | "github.com/serverlessworkflow/sdk-go/v3/model"
25 | "github.com/stretchr/testify/assert"
26 | )
27 |
28 | func TestRaiseTaskRunner_WithDefinedError(t *testing.T) {
29 | input := map[string]interface{}{}
30 |
31 | raiseTask := &model.RaiseTask{
32 | Raise: model.RaiseTaskConfiguration{
33 | Error: model.RaiseTaskError{
34 | Definition: &model.Error{
35 | Type: model.NewUriTemplate(model.ErrorTypeValidation),
36 | Status: 400,
37 | Title: model.NewStringOrRuntimeExpr("Validation Error"),
38 | Detail: model.NewStringOrRuntimeExpr("Invalid input data"),
39 | },
40 | },
41 | },
42 | }
43 |
44 | wfCtx, err := ctx.NewWorkflowContext(&model.Workflow{})
45 | assert.NoError(t, err)
46 | wfCtx.SetTaskReference("task_raise_defined")
47 |
48 | taskSupport := newTaskSupport(withRunnerCtx(wfCtx))
49 | runner, err := NewRaiseTaskRunner("task_raise_defined", raiseTask, taskSupport.GetWorkflowDef())
50 | assert.NoError(t, err)
51 |
52 | output, err := runner.Run(input, taskSupport)
53 | assert.Equal(t, output, input)
54 | assert.Error(t, err)
55 |
56 | expectedErr := model.NewErrValidation(errors.New("Invalid input data"), "task_raise_defined")
57 |
58 | var modelErr *model.Error
59 | if errors.As(err, &modelErr) {
60 | assert.Equal(t, expectedErr.Type.String(), modelErr.Type.String())
61 | assert.Equal(t, expectedErr.Status, modelErr.Status)
62 | assert.Equal(t, expectedErr.Title.String(), modelErr.Title.String())
63 | assert.Equal(t, "Invalid input data", modelErr.Detail.String())
64 | assert.Equal(t, expectedErr.Instance.String(), modelErr.Instance.String())
65 | } else {
66 | t.Errorf("expected error of type *model.Error but got %T", err)
67 | }
68 | }
69 |
70 | func TestRaiseTaskRunner_WithReferencedError(t *testing.T) {
71 | ref := "someErrorRef"
72 | raiseTask := &model.RaiseTask{
73 | Raise: model.RaiseTaskConfiguration{
74 | Error: model.RaiseTaskError{
75 | Ref: &ref,
76 | },
77 | },
78 | }
79 |
80 | runner, err := NewRaiseTaskRunner("task_raise_ref", raiseTask, &model.Workflow{})
81 | assert.Error(t, err)
82 | assert.Nil(t, runner)
83 | }
84 |
85 | func TestRaiseTaskRunner_TimeoutErrorWithExpression(t *testing.T) {
86 | input := map[string]interface{}{
87 | "timeoutMessage": "Request took too long",
88 | }
89 |
90 | raiseTask := &model.RaiseTask{
91 | Raise: model.RaiseTaskConfiguration{
92 | Error: model.RaiseTaskError{
93 | Definition: &model.Error{
94 | Type: model.NewUriTemplate(model.ErrorTypeTimeout),
95 | Status: 408,
96 | Title: model.NewStringOrRuntimeExpr("Timeout Error"),
97 | Detail: model.NewStringOrRuntimeExpr("${ .timeoutMessage }"),
98 | },
99 | },
100 | },
101 | }
102 |
103 | wfCtx, err := ctx.NewWorkflowContext(&model.Workflow{})
104 | assert.NoError(t, err)
105 | wfCtx.SetTaskReference("task_raise_timeout_expr")
106 |
107 | taskSupport := newTaskSupport(withRunnerCtx(wfCtx))
108 | runner, err := NewRaiseTaskRunner("task_raise_timeout_expr", raiseTask, taskSupport.GetWorkflowDef())
109 | assert.NoError(t, err)
110 |
111 | output, err := runner.Run(input, taskSupport)
112 | assert.Equal(t, input, output)
113 | assert.Error(t, err)
114 |
115 | expectedErr := model.NewErrTimeout(errors.New("Request took too long"), "task_raise_timeout_expr")
116 |
117 | var modelErr *model.Error
118 | if errors.As(err, &modelErr) {
119 | assert.Equal(t, expectedErr.Type.String(), modelErr.Type.String())
120 | assert.Equal(t, expectedErr.Status, modelErr.Status)
121 | assert.Equal(t, expectedErr.Title.String(), modelErr.Title.String())
122 | assert.Equal(t, "Request took too long", modelErr.Detail.String())
123 | assert.Equal(t, expectedErr.Instance.String(), modelErr.Instance.String())
124 | } else {
125 | t.Errorf("expected error of type *model.Error but got %T", err)
126 | }
127 | }
128 |
129 | func TestRaiseTaskRunner_Serialization(t *testing.T) {
130 | raiseTask := &model.RaiseTask{
131 | Raise: model.RaiseTaskConfiguration{
132 | Error: model.RaiseTaskError{
133 | Definition: &model.Error{
134 | Type: model.NewUriTemplate(model.ErrorTypeRuntime),
135 | Status: 500,
136 | Title: model.NewStringOrRuntimeExpr("Runtime Error"),
137 | Detail: model.NewStringOrRuntimeExpr("Unexpected failure"),
138 | Instance: &model.JsonPointerOrRuntimeExpression{Value: "/task_runtime"},
139 | },
140 | },
141 | },
142 | }
143 |
144 | data, err := json.Marshal(raiseTask)
145 | assert.NoError(t, err)
146 |
147 | var deserializedTask model.RaiseTask
148 | err = json.Unmarshal(data, &deserializedTask)
149 | assert.NoError(t, err)
150 |
151 | assert.Equal(t, raiseTask.Raise.Error.Definition.Type.String(), deserializedTask.Raise.Error.Definition.Type.String())
152 | assert.Equal(t, raiseTask.Raise.Error.Definition.Status, deserializedTask.Raise.Error.Definition.Status)
153 | assert.Equal(t, raiseTask.Raise.Error.Definition.Title.String(), deserializedTask.Raise.Error.Definition.Title.String())
154 | assert.Equal(t, raiseTask.Raise.Error.Definition.Detail.String(), deserializedTask.Raise.Error.Definition.Detail.String())
155 | assert.Equal(t, raiseTask.Raise.Error.Definition.Instance.String(), deserializedTask.Raise.Error.Definition.Instance.String())
156 | }
157 |
158 | func TestRaiseTaskRunner_ReferenceSerialization(t *testing.T) {
159 | ref := "errorReference"
160 | raiseTask := &model.RaiseTask{
161 | Raise: model.RaiseTaskConfiguration{
162 | Error: model.RaiseTaskError{
163 | Ref: &ref,
164 | },
165 | },
166 | }
167 |
168 | data, err := json.Marshal(raiseTask)
169 | assert.NoError(t, err)
170 |
171 | var deserializedTask model.RaiseTask
172 | err = json.Unmarshal(data, &deserializedTask)
173 | assert.NoError(t, err)
174 |
175 | assert.Equal(t, *raiseTask.Raise.Error.Ref, *deserializedTask.Raise.Error.Ref)
176 | assert.Nil(t, deserializedTask.Raise.Error.Definition)
177 | }
178 |
--------------------------------------------------------------------------------
/impl/task_runner_set.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package impl
16 |
17 | import (
18 | "fmt"
19 |
20 | "github.com/serverlessworkflow/sdk-go/v3/impl/expr"
21 | "github.com/serverlessworkflow/sdk-go/v3/impl/utils"
22 |
23 | "github.com/serverlessworkflow/sdk-go/v3/model"
24 | )
25 |
26 | func NewSetTaskRunner(taskName string, task *model.SetTask) (*SetTaskRunner, error) {
27 | if task == nil || task.Set == nil {
28 | return nil, model.NewErrValidation(fmt.Errorf("no set configuration provided for SetTask %s", taskName), taskName)
29 | }
30 | return &SetTaskRunner{
31 | Task: task,
32 | TaskName: taskName,
33 | }, nil
34 | }
35 |
36 | type SetTaskRunner struct {
37 | Task *model.SetTask
38 | TaskName string
39 | }
40 |
41 | func (s *SetTaskRunner) GetTaskName() string {
42 | return s.TaskName
43 | }
44 |
45 | func (s *SetTaskRunner) Run(input interface{}, taskSupport TaskSupport) (output interface{}, err error) {
46 | setObject := utils.DeepClone(s.Task.Set)
47 | result, err := expr.TraverseAndEvaluateObj(model.NewObjectOrRuntimeExpr(setObject), input, s.TaskName, taskSupport.GetContext())
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | output, ok := result.(map[string]interface{})
53 | if !ok {
54 | return nil, model.NewErrRuntime(fmt.Errorf("expected output to be a map[string]interface{}, but got a different type. Got: %v", result), s.TaskName)
55 | }
56 |
57 | return output, nil
58 | }
59 |
--------------------------------------------------------------------------------
/impl/testdata/chained_set_tasks.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: chained-workflow
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | do:
21 | - task1:
22 | set:
23 | baseValue: 10
24 | - task2:
25 | set:
26 | doubled: "${ .baseValue * 2 }"
27 | - task3:
28 | set:
29 | tripled: "${ .doubled * 3 }"
30 |
--------------------------------------------------------------------------------
/impl/testdata/concatenating_strings.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: concatenating-strings
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | do:
21 | - task1:
22 | set:
23 | firstName: "John"
24 | lastName: ""
25 | - task2:
26 | set:
27 | firstName: "${ .firstName }"
28 | lastName: "Doe"
29 | - task3:
30 | set:
31 | fullName: "${ .firstName + ' ' + .lastName }"
32 |
--------------------------------------------------------------------------------
/impl/testdata/conditional_logic.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: conditional-logic
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | do:
21 | - task1:
22 | set:
23 | temperature: 30
24 | - task2:
25 | set:
26 | weather: "${ if .temperature > 25 then 'hot' else 'cold' end }"
27 |
--------------------------------------------------------------------------------
/impl/testdata/conditional_logic_input_from.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: conditional-logic
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | input:
21 | from: "${ .localWeather }"
22 | do:
23 | - task2:
24 | set:
25 | weather: "${ if .temperature > 25 then 'hot' else 'cold' end }"
26 |
--------------------------------------------------------------------------------
/impl/testdata/for_colors.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0'
17 | namespace: default
18 | name: for
19 | version: '1.0.0'
20 | do:
21 | - loopColors:
22 | for:
23 | each: color
24 | in: '${ .colors }'
25 | do:
26 | - markProcessed:
27 | set:
28 | processed: '${ { colors: (.processed.colors + [ $color ]), indexes: (.processed.indexes + [ $index ])} }'
29 |
--------------------------------------------------------------------------------
/impl/testdata/for_nested_loops.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0'
17 | namespace: for-tests
18 | name: nested-loops
19 | version: '1.0.0'
20 | do:
21 | - outerLoop:
22 | for:
23 | in: ${ .fruits }
24 | each: fruit
25 | at: fruitIdx
26 | do:
27 | - innerLoop:
28 | for:
29 | in: ${ $input.colors }
30 | each: color
31 | at: colorIdx
32 | do:
33 | - combinePair:
34 | set:
35 | matrix: ${ .matrix + [[$fruit, $color]] }
36 |
--------------------------------------------------------------------------------
/impl/testdata/for_sum_numbers.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0'
17 | namespace: for-tests
18 | name: sum-numbers
19 | version: '1.0.0'
20 | do:
21 | - sumLoop:
22 | for:
23 | in: ${ .numbers }
24 | do:
25 | - addNumber:
26 | set:
27 | total: ${ .total + $item }
28 | - finalize:
29 | set:
30 | result: ${ .total }
31 |
--------------------------------------------------------------------------------
/impl/testdata/fork_simple.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0'
17 | namespace: test
18 | name: fork-example
19 | version: '0.1.0'
20 | do:
21 | - branchColors:
22 | fork:
23 | compete: false
24 | branches:
25 | - setRed:
26 | set:
27 | color1: red
28 | - setBlue:
29 | set:
30 | color2: blue
31 | - joinResult:
32 | set:
33 | colors: "${ [.[] | .[]] }"
34 |
--------------------------------------------------------------------------------
/impl/testdata/raise_conditional.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # $schema: https://raw.githubusercontent.com/serverlessworkflow/specification/refs/heads/main/schema/workflow.yaml
16 | document:
17 | dsl: '1.0.0-alpha5'
18 | namespace: test
19 | name: raise-conditional
20 | version: '1.0.0'
21 | do:
22 | - underageError:
23 | if: ${ .user.age < 18 }
24 | raise:
25 | error:
26 | type: https://serverlessworkflow.io/spec/1.0.0/errors/authorization
27 | status: 403
28 | title: Authorization Error
29 | detail: "User is under the required age"
30 | - continueProcess:
31 | set:
32 | message: "User is allowed"
33 |
--------------------------------------------------------------------------------
/impl/testdata/raise_error_with_input.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0-alpha5'
17 | namespace: test
18 | name: raise-with-input
19 | version: '1.0.0'
20 | do:
21 | - dynamicError:
22 | raise:
23 | error:
24 | type: https://serverlessworkflow.io/spec/1.0.0/errors/authentication
25 | status: 401
26 | title: Authentication Error
27 | detail: '${ "User authentication failed: \( .reason )" }'
28 |
--------------------------------------------------------------------------------
/impl/testdata/raise_inline.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0-alpha5'
17 | namespace: test
18 | name: raise-inline
19 | version: '1.0.0'
20 | do:
21 | - inlineError:
22 | raise:
23 | error:
24 | type: https://serverlessworkflow.io/spec/1.0.0/errors/validation
25 | status: 400
26 | title: Validation Error
27 | detail: ${ "Invalid input provided to workflow \($workflow.definition.document.name)" }
28 |
--------------------------------------------------------------------------------
/impl/testdata/raise_reusable.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0-alpha5'
17 | namespace: test
18 | name: raise-reusable
19 | version: '1.0.0'
20 | use:
21 | errors:
22 | AuthenticationError:
23 | type: https://serverlessworkflow.io/spec/1.0.0/errors/authentication
24 | status: 401
25 | title: Authentication Error
26 | detail: "User is not authenticated"
27 | do:
28 | - authError:
29 | raise:
30 | error: AuthenticationError
31 |
--------------------------------------------------------------------------------
/impl/testdata/raise_undefined_reference.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0-alpha5'
17 | namespace: test
18 | name: raise-undefined-reference
19 | version: '1.0.0'
20 | do:
21 | - missingError:
22 | raise:
23 | error: UndefinedError
24 |
--------------------------------------------------------------------------------
/impl/testdata/sequential_set_colors.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0-alpha5'
17 | namespace: default
18 | name: do
19 | version: '1.0.0'
20 | do:
21 | - setRed:
22 | set:
23 | colors: ${ .colors + ["red"] }
24 | - setGreen:
25 | set:
26 | colors: ${ .colors + ["green"] }
27 | - setBlue:
28 | set:
29 | colors: ${ .colors + ["blue"] }
30 | output:
31 | as: "${ { resultColors: .colors } }"
--------------------------------------------------------------------------------
/impl/testdata/sequential_set_colors_output_as.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0-alpha5'
17 | namespace: default
18 | name: do
19 | version: '1.0.0'
20 | do:
21 | - setRed:
22 | set:
23 | colors: ${ .colors + ["red"] }
24 | - setGreen:
25 | set:
26 | colors: ${ .colors + ["green"] }
27 | - setBlue:
28 | set:
29 | colors: ${ .colors + ["blue"] }
30 | output:
31 | as: "${ { result: .colors } }"
--------------------------------------------------------------------------------
/impl/testdata/set_tasks_invalid_then.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: invalid-then-workflow
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | do:
21 | - task1:
22 | set:
23 | partialResult: 15
24 | then: nonExistentTask
25 | - task2:
26 | set:
27 | skipped: true
28 |
--------------------------------------------------------------------------------
/impl/testdata/set_tasks_with_termination.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: termination-workflow
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | do:
21 | - task1:
22 | set:
23 | finalValue: 20
24 | then: end
25 | - task2:
26 | set:
27 | skipped: true
28 |
--------------------------------------------------------------------------------
/impl/testdata/set_tasks_with_then.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: then-workflow
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | do:
21 | - task1:
22 | set:
23 | value: 30
24 | then: task3
25 | - task2:
26 | set:
27 | skipped: true
28 | - task3:
29 | set:
30 | result: "${ .value * 3 }"
31 |
--------------------------------------------------------------------------------
/impl/testdata/switch_match.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0'
17 | namespace: default
18 | name: switch-match
19 | version: '1.0.0'
20 | do:
21 | - switchColor:
22 | switch:
23 | - red:
24 | when: '.color == "red"'
25 | then: setRed
26 | - green:
27 | when: '.color == "green"'
28 | then: setGreen
29 | - blue:
30 | when: '.color == "blue"'
31 | then: setBlue
32 | - setRed:
33 | set:
34 | colors: '${ .colors + [ "red" ] }'
35 | then: end
36 | - setGreen:
37 | set:
38 | colors: '${ .colors + [ "green" ] }'
39 | then: end
40 | - setBlue:
41 | set:
42 | colors: '${ .colors + [ "blue" ] }'
43 | then: end
44 |
--------------------------------------------------------------------------------
/impl/testdata/switch_with_default.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: '1.0.0'
17 | namespace: default
18 | name: switch-with-default
19 | version: '1.0.0'
20 |
21 | do:
22 | - switchColor:
23 | switch:
24 | - red:
25 | when: '.color == "red"'
26 | then: setRed
27 | - green:
28 | when: '.color == "green"'
29 | then: setGreen
30 | - fallback:
31 | then: setDefault
32 | - setRed:
33 | set:
34 | colors: '${ .colors + [ "red" ] }'
35 | then: end
36 | - setGreen:
37 | set:
38 | colors: '${ .colors + [ "green" ] }'
39 | then: end
40 | - setDefault:
41 | set:
42 | colors: '${ .colors + [ "default" ] }'
43 | then: end
44 |
--------------------------------------------------------------------------------
/impl/testdata/task_export_schema.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: task-export-schema
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | do:
21 | - task1:
22 | set:
23 | exportedKey: "${ .key }"
24 | export:
25 | schema:
26 | format: "json"
27 | document:
28 | type: "object"
29 | properties:
30 | exportedKey:
31 | type: "string"
32 | required: ["exportedKey"]
33 |
--------------------------------------------------------------------------------
/impl/testdata/task_input_schema.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: task-input-schema
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | do:
21 | - task1:
22 | input:
23 | schema:
24 | format: "json"
25 | document:
26 | type: "object"
27 | properties:
28 | taskInputKey:
29 | type: "number"
30 | required: ["taskInputKey"]
31 | set:
32 | taskOutputKey: "${ .taskInputKey * 2 }"
33 |
--------------------------------------------------------------------------------
/impl/testdata/task_output_schema.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: task-output-schema
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | do:
21 | - task1:
22 | set:
23 | finalOutputKey: "resultValue"
24 | output:
25 | schema:
26 | format: "json"
27 | document:
28 | type: "object"
29 | properties:
30 | finalOutputKey:
31 | type: "string"
32 | required: ["finalOutputKey"]
33 |
--------------------------------------------------------------------------------
/impl/testdata/task_output_schema_with_dynamic_value.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: task-output-schema-with-dynamic-value
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | do:
21 | - task1:
22 | set:
23 | finalOutputKey: "${ .taskInputKey }"
24 | output:
25 | schema:
26 | format: "json"
27 | document:
28 | type: "object"
29 | properties:
30 | finalOutputKey:
31 | type: "string"
32 | required: ["finalOutputKey"]
33 |
--------------------------------------------------------------------------------
/impl/testdata/workflow_input_schema.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | name: workflow-input-schema
17 | dsl: '1.0.0-alpha5'
18 | namespace: default
19 | version: '1.0.0'
20 | input:
21 | schema:
22 | format: "json"
23 | document:
24 | type: "object"
25 | properties:
26 | key:
27 | type: "string"
28 | required: ["key"]
29 | do:
30 | - task1:
31 | set:
32 | outputKey: "${ .key }"
33 |
--------------------------------------------------------------------------------
/impl/utils/json_schema.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package utils
16 |
17 | import (
18 | "encoding/json"
19 | "errors"
20 | "fmt"
21 |
22 | "github.com/serverlessworkflow/sdk-go/v3/model"
23 | "github.com/xeipuuv/gojsonschema"
24 | )
25 |
26 | // validateJSONSchema validates the provided data against a model.Schema.
27 | func validateJSONSchema(data interface{}, schema *model.Schema) error {
28 | if schema == nil {
29 | return nil
30 | }
31 |
32 | schema.ApplyDefaults()
33 |
34 | if schema.Format != model.DefaultSchema {
35 | return fmt.Errorf("unsupported schema format: '%s'", schema.Format)
36 | }
37 |
38 | var schemaJSON string
39 | if schema.Document != nil {
40 | documentBytes, err := json.Marshal(schema.Document)
41 | if err != nil {
42 | return fmt.Errorf("failed to marshal schema document to JSON: %w", err)
43 | }
44 | schemaJSON = string(documentBytes)
45 | } else if schema.Resource != nil {
46 | // TODO: Handle external resource references (not implemented here)
47 | return errors.New("external resources are not yet supported")
48 | } else {
49 | return errors.New("schema must have either a 'Document' or 'Resource'")
50 | }
51 |
52 | schemaLoader := gojsonschema.NewStringLoader(schemaJSON)
53 | dataLoader := gojsonschema.NewGoLoader(data)
54 |
55 | result, err := gojsonschema.Validate(schemaLoader, dataLoader)
56 | if err != nil {
57 | // TODO: use model.Error
58 | return fmt.Errorf("failed to validate JSON schema: %w", err)
59 | }
60 |
61 | if !result.Valid() {
62 | var validationErrors string
63 | for _, err := range result.Errors() {
64 | validationErrors += fmt.Sprintf("- %s\n", err.String())
65 | }
66 | return fmt.Errorf("JSON schema validation failed:\n%s", validationErrors)
67 | }
68 |
69 | return nil
70 | }
71 |
72 | func ValidateSchema(data interface{}, schema *model.Schema, taskName string) error {
73 | if schema != nil {
74 | if err := validateJSONSchema(data, schema); err != nil {
75 | return model.NewErrValidation(err, taskName)
76 | }
77 | }
78 | return nil
79 | }
80 |
--------------------------------------------------------------------------------
/impl/utils/utils.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package utils
16 |
17 | // DeepClone a map to avoid modifying the original object
18 | func DeepClone(obj map[string]interface{}) map[string]interface{} {
19 | clone := make(map[string]interface{})
20 | for key, value := range obj {
21 | clone[key] = DeepCloneValue(value)
22 | }
23 | return clone
24 | }
25 |
26 | func DeepCloneValue(value interface{}) interface{} {
27 | if m, ok := value.(map[string]interface{}); ok {
28 | return DeepClone(m)
29 | }
30 | if s, ok := value.([]interface{}); ok {
31 | clonedSlice := make([]interface{}, len(s))
32 | for i, v := range s {
33 | clonedSlice[i] = DeepCloneValue(v)
34 | }
35 | return clonedSlice
36 | }
37 | return value
38 | }
39 |
--------------------------------------------------------------------------------
/maintainer_guidelines.md:
--------------------------------------------------------------------------------
1 | # Maintainer's Guide
2 |
3 | ## Tips
4 |
5 | Here are a few tips for repository maintainers.
6 |
7 | * Stay on top of your pull requests. PRs that languish for too long can become difficult to merge.
8 | * Work from your own fork. As you are making contributions to the project, you should be working from your own fork just as outside contributors do. This keeps the branches in github to a minimum and reduces unnecessary CI runs.
9 | * Try to proactively label issues with backport labels if it's obvious that a change should be backported to previous releases.
10 | * When landing pull requests, if there is more than one commit, try to squash into a single commit. Usually this can just be done with the GitHub UI when merging the PR. Use "Squash and merge".
11 | * Triage issues once in a while in order to keep the repository alive. During the triage:
12 | * If some issues are stale for too long because they are no longer valid/relevant or because the discussion reached no significant action items to perform, close them and invite the users to reopen if they need it.
13 | * If some PRs are no longer valid but still needed, ask the user to rebase them
14 | * If some issues and PRs are still relevant, use labels to help organize tasks
15 | * If you find an issue that you want to create a fix for and submit a pull request, be sure to assign it to yourself so that others maintainers don't start working on it at the same time.
16 |
17 | ## Branch Management
18 |
19 | The `main` branch is the bleeding edge. New major versions of the module
20 | are cut from this branch and tagged. If you intend to submit a pull request
21 | you should use `main HEAD` as your starting point.
22 |
23 | Each major release will result in a new branch and tag. For example, the
24 | release of version 1.0.0 of the project results in a `v1.0.0` tag on the
25 | release commit, and a new branch `release-1.y.z` for subsequent minor and patch
26 | level releases of that major version if necessary. However, development will continue
27 | apace on `main` for the next major version - e.g. 2.0.0. Version branches
28 | are only created for each major version. Minor and patch level releases
29 | are simply tagged.
30 |
--------------------------------------------------------------------------------
/model/authentication_oauth_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | "testing"
21 | )
22 |
23 | func TestOAuth2AuthenticationPolicyValidation(t *testing.T) {
24 | testCases := []struct {
25 | name string
26 | policy OAuth2AuthenticationPolicy
27 | shouldPass bool
28 | }{
29 | {
30 | name: "Valid: Use set",
31 | policy: OAuth2AuthenticationPolicy{
32 | Use: "mysecret",
33 | },
34 | shouldPass: true,
35 | },
36 | {
37 | name: "Valid: Properties set",
38 | policy: OAuth2AuthenticationPolicy{
39 | Properties: &OAuth2AuthenticationProperties{
40 | Grant: ClientCredentialsGrant,
41 | Scopes: []string{"scope1", "scope2"},
42 | Authority: &LiteralUri{Value: "https://auth.example.com"},
43 | },
44 | },
45 | shouldPass: true,
46 | },
47 | {
48 | name: "Invalid: Both Use and Properties set",
49 | policy: OAuth2AuthenticationPolicy{
50 | Use: "mysecret",
51 | Properties: &OAuth2AuthenticationProperties{
52 | Grant: ClientCredentialsGrant,
53 | Scopes: []string{"scope1", "scope2"},
54 | Authority: &LiteralUri{Value: "https://auth.example.com"},
55 | },
56 | },
57 | shouldPass: false,
58 | },
59 | {
60 | name: "Invalid: Neither Use nor Properties set",
61 | policy: OAuth2AuthenticationPolicy{},
62 | shouldPass: false,
63 | },
64 | }
65 |
66 | for _, tc := range testCases {
67 | t.Run(tc.name, func(t *testing.T) {
68 | err := validate.Struct(tc.policy)
69 | if tc.shouldPass {
70 | if err != nil {
71 | t.Errorf("Expected validation to pass, but got error: %v", err)
72 | }
73 | } else {
74 | if err == nil {
75 | t.Errorf("Expected validation to fail, but it passed")
76 | }
77 | }
78 | })
79 | }
80 | }
81 |
82 | func TestAuthenticationOAuth2Policy(t *testing.T) {
83 | testCases := []struct {
84 | name string
85 | input string
86 | expected string
87 | expectsErr bool
88 | }{
89 | {
90 | name: "Valid OAuth2 Authentication Inline",
91 | input: `{
92 | "oauth2": {
93 | "authority": "https://auth.example.com",
94 | "grant": "client_credentials",
95 | "scopes": ["scope1", "scope2"]
96 | }
97 | }`,
98 | expected: `{"oauth2":{"authority":"https://auth.example.com","grant":"client_credentials","scopes":["scope1","scope2"]}}`,
99 | expectsErr: false,
100 | },
101 | {
102 | name: "Valid OAuth2 Authentication Use",
103 | input: `{
104 | "oauth2": {
105 | "use": "mysecret"
106 | }
107 | }`,
108 | expected: `{"oauth2":{"use":"mysecret"}}`,
109 | expectsErr: false,
110 | },
111 | {
112 | name: "Invalid OAuth2: Both properties and use set",
113 | input: `{
114 | "oauth2": {
115 | "authority": "https://auth.example.com",
116 | "grant": "client_credentials",
117 | "use": "mysecret"
118 | }
119 | }`,
120 | expectsErr: true,
121 | },
122 | {
123 | name: "Invalid OAuth2: Missing required fields",
124 | input: `{
125 | "oauth2": {}
126 | }`,
127 | expectsErr: true,
128 | },
129 | }
130 |
131 | for _, tc := range testCases {
132 | t.Run(tc.name, func(t *testing.T) {
133 | var authPolicy AuthenticationPolicy
134 |
135 | // Unmarshal
136 | err := json.Unmarshal([]byte(tc.input), &authPolicy)
137 | if err == nil {
138 | err = validate.Struct(authPolicy)
139 | }
140 |
141 | if tc.expectsErr {
142 | if err == nil {
143 | t.Errorf("Expected an error but got none")
144 | }
145 | } else {
146 | if err != nil {
147 | t.Errorf("Unexpected error: %v", err)
148 | }
149 |
150 | // Marshal
151 | marshaled, err := json.Marshal(authPolicy)
152 | if err != nil {
153 | t.Errorf("Failed to marshal: %v", err)
154 | }
155 |
156 | if string(marshaled) != tc.expected {
157 | t.Errorf("Expected %s but got %s", tc.expected, marshaled)
158 | }
159 |
160 | fmt.Printf("Test '%s' passed. Marshaled output: %s\n", tc.name, marshaled)
161 | }
162 | })
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/model/authentication_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | "testing"
21 | )
22 |
23 | func TestAuthenticationPolicy(t *testing.T) {
24 | testCases := []struct {
25 | name string
26 | input string
27 | expected string
28 | expectsErr bool
29 | }{
30 | {
31 | name: "Valid Basic Authentication Inline",
32 | input: `{
33 | "basic": {
34 | "username": "john",
35 | "password": "12345"
36 | }
37 | }`,
38 | expected: `{"basic":{"username":"john","password":"12345"}}`,
39 | expectsErr: false,
40 | },
41 | {
42 | name: "Valid Digest Authentication Inline",
43 | input: `{
44 | "digest": {
45 | "username": "digestUser",
46 | "password": "digestPass"
47 | }
48 | }`,
49 | expected: `{"digest":{"username":"digestUser","password":"digestPass"}}`,
50 | expectsErr: false,
51 | },
52 | }
53 |
54 | for _, tc := range testCases {
55 | t.Run(tc.name, func(t *testing.T) {
56 | var authPolicy AuthenticationPolicy
57 |
58 | // Unmarshal
59 | err := json.Unmarshal([]byte(tc.input), &authPolicy)
60 | if err == nil {
61 | if authPolicy.Basic != nil {
62 | err = validate.Struct(authPolicy.Basic)
63 | }
64 | if authPolicy.Bearer != nil {
65 | err = validate.Struct(authPolicy.Bearer)
66 | }
67 | if authPolicy.Digest != nil {
68 | err = validate.Struct(authPolicy.Digest)
69 | }
70 | if authPolicy.OAuth2 != nil {
71 | err = validate.Struct(authPolicy.OAuth2)
72 | }
73 | }
74 |
75 | if tc.expectsErr {
76 | if err == nil {
77 | t.Errorf("Expected an error but got none")
78 | }
79 | } else {
80 | if err != nil {
81 | t.Errorf("Unexpected error: %v", err)
82 | }
83 |
84 | // Marshal
85 | marshaled, err := json.Marshal(authPolicy)
86 | if err != nil {
87 | t.Errorf("Failed to marshal: %v", err)
88 | }
89 |
90 | if string(marshaled) != tc.expected {
91 | t.Errorf("Expected %s but got %s", tc.expected, marshaled)
92 | }
93 |
94 | fmt.Printf("Test '%s' passed. Marshaled output: %s\n", tc.name, marshaled)
95 | }
96 | })
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/model/builder.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 |
20 | "sigs.k8s.io/yaml"
21 | )
22 |
23 | // WorkflowBuilder helps construct and serialize a Workflow object.
24 | type WorkflowBuilder struct {
25 | workflow *Workflow
26 | }
27 |
28 | // NewWorkflowBuilder initializes a new WorkflowBuilder.
29 | func NewWorkflowBuilder() *WorkflowBuilder {
30 | return &WorkflowBuilder{
31 | workflow: &Workflow{
32 | Document: Document{},
33 | Do: &TaskList{},
34 | },
35 | }
36 | }
37 |
38 | // SetDocument sets the Document fields in the Workflow.
39 | func (wb *WorkflowBuilder) SetDocument(dsl, namespace, name, version string) *WorkflowBuilder {
40 | wb.workflow.Document.DSL = dsl
41 | wb.workflow.Document.Namespace = namespace
42 | wb.workflow.Document.Name = name
43 | wb.workflow.Document.Version = version
44 | return wb
45 | }
46 |
47 | // AddTask adds a TaskItem to the Workflow's Do list.
48 | func (wb *WorkflowBuilder) AddTask(key string, task Task) *WorkflowBuilder {
49 | *wb.workflow.Do = append(*wb.workflow.Do, &TaskItem{
50 | Key: key,
51 | Task: task,
52 | })
53 | return wb
54 | }
55 |
56 | // SetInput sets the Input for the Workflow.
57 | func (wb *WorkflowBuilder) SetInput(input *Input) *WorkflowBuilder {
58 | wb.workflow.Input = input
59 | return wb
60 | }
61 |
62 | // SetOutput sets the Output for the Workflow.
63 | func (wb *WorkflowBuilder) SetOutput(output *Output) *WorkflowBuilder {
64 | wb.workflow.Output = output
65 | return wb
66 | }
67 |
68 | // SetTimeout sets the Timeout for the Workflow.
69 | func (wb *WorkflowBuilder) SetTimeout(timeout *TimeoutOrReference) *WorkflowBuilder {
70 | wb.workflow.Timeout = timeout
71 | return wb
72 | }
73 |
74 | // SetUse sets the Use section for the Workflow.
75 | func (wb *WorkflowBuilder) SetUse(use *Use) *WorkflowBuilder {
76 | wb.workflow.Use = use
77 | return wb
78 | }
79 |
80 | // SetSchedule sets the Schedule for the Workflow.
81 | func (wb *WorkflowBuilder) SetSchedule(schedule *Schedule) *WorkflowBuilder {
82 | wb.workflow.Schedule = schedule
83 | return wb
84 | }
85 |
86 | // Build returns the constructed Workflow object.
87 | func (wb *WorkflowBuilder) Build() *Workflow {
88 | return wb.workflow
89 | }
90 |
91 | // ToYAML serializes the Workflow to YAML format.
92 | func (wb *WorkflowBuilder) ToYAML() ([]byte, error) {
93 | return yaml.Marshal(wb.workflow)
94 | }
95 |
96 | // ToJSON serializes the Workflow to JSON format.
97 | func (wb *WorkflowBuilder) ToJSON() ([]byte, error) {
98 | return json.MarshalIndent(wb.workflow, "", " ")
99 | }
100 |
--------------------------------------------------------------------------------
/model/extension.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | )
21 |
22 | // Extension represents the definition of an extension.
23 | type Extension struct {
24 | Extend string `json:"extend" validate:"required,oneof=call composite emit for listen raise run set switch try wait all"`
25 | When *RuntimeExpression `json:"when,omitempty"`
26 | Before *TaskList `json:"before,omitempty" validate:"omitempty,dive"`
27 | After *TaskList `json:"after,omitempty" validate:"omitempty,dive"`
28 | }
29 |
30 | // ExtensionItem represents a named extension and its associated definition.
31 | type ExtensionItem struct {
32 | Key string `json:"-" validate:"required"`
33 | Extension *Extension `json:"-" validate:"required"`
34 | }
35 |
36 | // MarshalJSON for ExtensionItem to serialize as a single-key object.
37 | func (ei *ExtensionItem) MarshalJSON() ([]byte, error) {
38 | if ei == nil {
39 | return nil, fmt.Errorf("cannot marshal a nil ExtensionItem")
40 | }
41 |
42 | extensionJSON, err := json.Marshal(ei.Extension)
43 | if err != nil {
44 | return nil, fmt.Errorf("failed to marshal extension: %w", err)
45 | }
46 |
47 | return json.Marshal(map[string]json.RawMessage{
48 | ei.Key: extensionJSON,
49 | })
50 | }
51 |
52 | // UnmarshalJSON for ExtensionItem to deserialize from a single-key object.
53 | func (ei *ExtensionItem) UnmarshalJSON(data []byte) error {
54 | var raw map[string]json.RawMessage
55 | if err := json.Unmarshal(data, &raw); err != nil {
56 | return fmt.Errorf("failed to unmarshal ExtensionItem: %w", err)
57 | }
58 |
59 | if len(raw) != 1 {
60 | return fmt.Errorf("each ExtensionItem must have exactly one key")
61 | }
62 |
63 | for key, extensionData := range raw {
64 | var ext Extension
65 | if err := json.Unmarshal(extensionData, &ext); err != nil {
66 | return fmt.Errorf("failed to unmarshal extension %q: %w", key, err)
67 | }
68 | ei.Key = key
69 | ei.Extension = &ext
70 | break
71 | }
72 |
73 | return nil
74 | }
75 |
76 | // ExtensionList represents a list of extensions.
77 | type ExtensionList []*ExtensionItem
78 |
79 | // Key retrieves all extensions with the specified key.
80 | func (el *ExtensionList) Key(key string) *Extension {
81 | for _, item := range *el {
82 | if item.Key == key {
83 | return item.Extension
84 | }
85 | }
86 | return nil
87 | }
88 |
89 | // UnmarshalJSON for ExtensionList to deserialize an array of ExtensionItem objects.
90 | func (el *ExtensionList) UnmarshalJSON(data []byte) error {
91 | var rawExtensions []json.RawMessage
92 | if err := json.Unmarshal(data, &rawExtensions); err != nil {
93 | return fmt.Errorf("failed to unmarshal ExtensionList: %w", err)
94 | }
95 |
96 | for _, raw := range rawExtensions {
97 | var item ExtensionItem
98 | if err := json.Unmarshal(raw, &item); err != nil {
99 | return fmt.Errorf("failed to unmarshal extension item: %w", err)
100 | }
101 | *el = append(*el, &item)
102 | }
103 |
104 | return nil
105 | }
106 |
107 | // MarshalJSON for ExtensionList to serialize as an array of ExtensionItem objects.
108 | func (el *ExtensionList) MarshalJSON() ([]byte, error) {
109 | var serializedExtensions []json.RawMessage
110 |
111 | for _, item := range *el {
112 | serialized, err := json.Marshal(item)
113 | if err != nil {
114 | return nil, fmt.Errorf("failed to marshal ExtensionItem: %w", err)
115 | }
116 | serializedExtensions = append(serializedExtensions, serialized)
117 | }
118 |
119 | return json.Marshal(serializedExtensions)
120 | }
121 |
--------------------------------------------------------------------------------
/model/extension_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "errors"
20 | "testing"
21 |
22 | validator "github.com/go-playground/validator/v10"
23 | "github.com/stretchr/testify/assert"
24 | )
25 |
26 | func TestExtension_UnmarshalJSON(t *testing.T) {
27 | jsonData := `{
28 | "extend": "call",
29 | "when": "${condition}",
30 | "before": [
31 | {"task1": {"call": "http", "with": {"method": "GET", "endpoint": "http://example.com"}}}
32 | ],
33 | "after": [
34 | {"task2": {"call": "openapi", "with": {"document": {"name": "doc1"}, "operationId": "op1"}}}
35 | ]
36 | }`
37 |
38 | var extension Extension
39 | err := json.Unmarshal([]byte(jsonData), &extension)
40 | assert.NoError(t, err)
41 | assert.Equal(t, "call", extension.Extend)
42 | assert.Equal(t, NewExpr("${condition}"), extension.When)
43 |
44 | task1 := extension.Before.Key("task1").AsCallHTTPTask()
45 | assert.NotNil(t, task1)
46 | assert.Equal(t, "http", task1.Call)
47 | assert.Equal(t, "GET", task1.With.Method)
48 | assert.Equal(t, "http://example.com", task1.With.Endpoint.String())
49 |
50 | // Check if task2 exists before accessing its fields
51 | task2 := extension.After.Key("task2")
52 | assert.NotNil(t, task2, "task2 should not be nil")
53 | openAPITask := task2.AsCallOpenAPITask()
54 | assert.NotNil(t, openAPITask)
55 | assert.Equal(t, "openapi", openAPITask.Call)
56 | assert.Equal(t, "doc1", openAPITask.With.Document.Name)
57 | assert.Equal(t, "op1", openAPITask.With.OperationID)
58 | }
59 |
60 | func TestExtension_MarshalJSON(t *testing.T) {
61 | extension := Extension{
62 | Extend: "call",
63 | When: NewExpr("${condition}"),
64 | Before: &TaskList{
65 | {Key: "task1", Task: &CallHTTP{
66 | Call: "http",
67 | With: HTTPArguments{
68 | Method: "GET",
69 | Endpoint: NewEndpoint("http://example.com"),
70 | },
71 | }},
72 | },
73 | After: &TaskList{
74 | {Key: "task2", Task: &CallOpenAPI{
75 | Call: "openapi",
76 | With: OpenAPIArguments{
77 | Document: &ExternalResource{Name: "doc1", Endpoint: NewEndpoint("http://example.com")},
78 | OperationID: "op1",
79 | },
80 | }},
81 | },
82 | }
83 |
84 | data, err := json.Marshal(extension)
85 | assert.NoError(t, err)
86 | assert.JSONEq(t, `{
87 | "extend": "call",
88 | "when": "${condition}",
89 | "before": [
90 | {"task1": {"call": "http", "with": {"method": "GET", "endpoint": "http://example.com"}}}
91 | ],
92 | "after": [
93 | {"task2": {"call": "openapi", "with": {"document": {"name": "doc1", "endpoint": "http://example.com"}, "operationId": "op1"}}}
94 | ]
95 | }`, string(data))
96 | }
97 |
98 | func TestExtension_Validation(t *testing.T) {
99 | extension := Extension{
100 | Extend: "call",
101 | When: NewExpr("${condition}"),
102 | Before: &TaskList{
103 | {Key: "task1", Task: &CallHTTP{
104 | Call: "http",
105 | With: HTTPArguments{
106 | Method: "GET",
107 | Endpoint: NewEndpoint("http://example.com"),
108 | },
109 | }},
110 | },
111 | After: &TaskList{
112 | {Key: "task2", Task: &CallOpenAPI{
113 | Call: "openapi",
114 | With: OpenAPIArguments{
115 | Document: &ExternalResource{
116 | Name: "doc1", // Missing Endpoint
117 | },
118 | OperationID: "op1",
119 | },
120 | }},
121 | },
122 | }
123 |
124 | err := validate.Struct(extension)
125 | assert.Error(t, err)
126 |
127 | var validationErrors validator.ValidationErrors
128 | if errors.As(err, &validationErrors) {
129 | for _, validationErr := range validationErrors {
130 | t.Logf("Validation failed on field '%s' with tag '%s': %s",
131 | validationErr.StructNamespace(), validationErr.Tag(), validationErr.Param())
132 | }
133 |
134 | // Assert on specific validation errors
135 | assert.Contains(t, validationErrors.Error(), "After[0].Task.With.Document.Endpoint")
136 | assert.Contains(t, validationErrors.Error(), "required")
137 | } else {
138 | t.Errorf("Unexpected error type: %v", err)
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/model/objects_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestObjectOrRuntimeExpr_UnmarshalJSON(t *testing.T) {
25 | cases := []struct {
26 | Name string
27 | JSON string
28 | Expected interface{}
29 | ShouldErr bool
30 | }{
31 | {
32 | Name: "Unmarshal valid string",
33 | JSON: `"${ expression }"`,
34 | Expected: RuntimeExpression{Value: "${ expression }"},
35 | ShouldErr: false,
36 | },
37 | {
38 | Name: "Unmarshal valid object",
39 | JSON: `{
40 | "key": "value"
41 | }`,
42 | Expected: map[string]interface{}{
43 | "key": "value",
44 | },
45 | ShouldErr: false,
46 | },
47 | {
48 | Name: "Unmarshal invalid type",
49 | JSON: `123`,
50 | ShouldErr: true,
51 | },
52 | }
53 |
54 | for _, tc := range cases {
55 | t.Run(tc.Name, func(t *testing.T) {
56 | var obj ObjectOrRuntimeExpr
57 | err := json.Unmarshal([]byte(tc.JSON), &obj)
58 | if tc.ShouldErr {
59 | assert.Error(t, err, "expected an error, but got none")
60 | } else {
61 | assert.NoError(t, err, "expected no error, but got one")
62 | assert.Equal(t, tc.Expected, obj.Value, "unexpected unmarshalled value")
63 | }
64 | })
65 | }
66 | }
67 |
68 | func TestURITemplateOrRuntimeExprValidation(t *testing.T) {
69 | cases := []struct {
70 | Name string
71 | Input *URITemplateOrRuntimeExpr
72 | ShouldErr bool
73 | }{
74 | {
75 | Name: "Valid URI template",
76 | Input: &URITemplateOrRuntimeExpr{
77 | Value: &LiteralUriTemplate{Value: "http://example.com/{id}"},
78 | },
79 | ShouldErr: false,
80 | },
81 | {
82 | Name: "Valid URI",
83 | Input: &URITemplateOrRuntimeExpr{
84 | Value: &LiteralUri{Value: "http://example.com"},
85 | },
86 | ShouldErr: false,
87 | },
88 | {
89 | Name: "Valid runtime expression",
90 | Input: &URITemplateOrRuntimeExpr{
91 | Value: RuntimeExpression{Value: "${expression}"},
92 | },
93 | ShouldErr: false,
94 | },
95 | {
96 | Name: "Invalid runtime expression",
97 | Input: &URITemplateOrRuntimeExpr{
98 | Value: RuntimeExpression{Value: "123invalid-expression"},
99 | },
100 | ShouldErr: true,
101 | },
102 | {
103 | Name: "Invalid URI format",
104 | Input: &URITemplateOrRuntimeExpr{
105 | Value: &LiteralUri{Value: "invalid-uri"},
106 | },
107 | ShouldErr: true,
108 | },
109 | {
110 | Name: "Unsupported type",
111 | Input: &URITemplateOrRuntimeExpr{
112 | Value: 123,
113 | },
114 | ShouldErr: true,
115 | },
116 | {
117 | Name: "Valid URI as string",
118 | Input: &URITemplateOrRuntimeExpr{
119 | Value: "http://example.com",
120 | },
121 | ShouldErr: false,
122 | },
123 | }
124 |
125 | for _, tc := range cases {
126 | t.Run(tc.Name, func(t *testing.T) {
127 | err := validate.Var(tc.Input, "uri_template_or_runtime_expr")
128 | if tc.ShouldErr {
129 | assert.Error(t, err, "expected an error, but got none")
130 | } else {
131 | assert.NoError(t, err, "expected no error, but got one")
132 | }
133 | })
134 | }
135 | }
136 |
137 | func TestJsonPointerOrRuntimeExpressionValidation(t *testing.T) {
138 | cases := []struct {
139 | Name string
140 | Input JsonPointerOrRuntimeExpression
141 | ShouldErr bool
142 | }{
143 | {
144 | Name: "Valid JSON Pointer",
145 | Input: JsonPointerOrRuntimeExpression{
146 | Value: "/valid/json/pointer",
147 | },
148 | ShouldErr: false,
149 | },
150 | {
151 | Name: "Valid runtime expression",
152 | Input: JsonPointerOrRuntimeExpression{
153 | Value: RuntimeExpression{Value: "${expression}"},
154 | },
155 | ShouldErr: false,
156 | },
157 | {
158 | Name: "Invalid JSON Pointer",
159 | Input: JsonPointerOrRuntimeExpression{
160 | Value: "invalid-json-pointer",
161 | },
162 | ShouldErr: true,
163 | },
164 | {
165 | Name: "Invalid runtime expression",
166 | Input: JsonPointerOrRuntimeExpression{
167 | Value: RuntimeExpression{Value: "123invalid-expression"},
168 | },
169 | ShouldErr: true,
170 | },
171 | {
172 | Name: "Unsupported type",
173 | Input: JsonPointerOrRuntimeExpression{
174 | Value: 123,
175 | },
176 | ShouldErr: true,
177 | },
178 | }
179 |
180 | for _, tc := range cases {
181 | t.Run(tc.Name, func(t *testing.T) {
182 | err := validate.Var(tc.Input, "json_pointer_or_runtime_expr")
183 | if tc.ShouldErr {
184 | assert.Error(t, err, "expected an error, but got none")
185 | } else {
186 | assert.NoError(t, err, "expected no error, but got one")
187 | }
188 | })
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/model/runtime_expression.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | "strings"
21 |
22 | "github.com/itchyny/gojq"
23 | )
24 |
25 | // RuntimeExpression represents a runtime expression.
26 | type RuntimeExpression struct {
27 | Value string `json:"-" validate:"required"`
28 | }
29 |
30 | // NewRuntimeExpression is an alias for NewExpr
31 | var NewRuntimeExpression = NewExpr
32 |
33 | // NewExpr creates a new RuntimeExpression instance
34 | func NewExpr(runtimeExpression string) *RuntimeExpression {
35 | return &RuntimeExpression{Value: runtimeExpression}
36 | }
37 |
38 | // IsStrictExpr returns true if the string is enclosed in `${ }`
39 | func IsStrictExpr(expression string) bool {
40 | return strings.HasPrefix(expression, "${") && strings.HasSuffix(expression, "}")
41 | }
42 |
43 | // SanitizeExpr processes the expression to ensure it's ready for evaluation
44 | // It removes `${}` if present and replaces single quotes with double quotes
45 | func SanitizeExpr(expression string) string {
46 | // Remove `${}` enclosure if present
47 | if IsStrictExpr(expression) {
48 | expression = strings.TrimSpace(expression[2 : len(expression)-1])
49 | }
50 |
51 | // Replace single quotes with double quotes
52 | expression = strings.ReplaceAll(expression, "'", "\"")
53 |
54 | return expression
55 | }
56 |
57 | func IsValidExpr(expression string) bool {
58 | expression = SanitizeExpr(expression)
59 | _, err := gojq.Parse(expression)
60 | return err == nil
61 | }
62 |
63 | // NormalizeExpr adds ${} to the given string
64 | func NormalizeExpr(expr string) string {
65 | if strings.HasPrefix(expr, "${") {
66 | return expr
67 | }
68 | return fmt.Sprintf("${%s}", expr)
69 | }
70 |
71 | // IsValid checks if the RuntimeExpression value is valid, handling both with and without `${}`.
72 | func (r *RuntimeExpression) IsValid() bool {
73 | return IsValidExpr(r.Value)
74 | }
75 |
76 | // UnmarshalJSON implements custom unmarshalling for RuntimeExpression.
77 | func (r *RuntimeExpression) UnmarshalJSON(data []byte) error {
78 | // Decode the input as a string
79 | var raw string
80 | if err := json.Unmarshal(data, &raw); err != nil {
81 | return fmt.Errorf("failed to unmarshal RuntimeExpression: %w", err)
82 | }
83 |
84 | // Assign the value
85 | r.Value = raw
86 |
87 | // Validate the runtime expression
88 | if !r.IsValid() {
89 | return fmt.Errorf("invalid runtime expression format: %s", raw)
90 | }
91 |
92 | return nil
93 | }
94 |
95 | // MarshalJSON implements custom marshalling for RuntimeExpression.
96 | func (r *RuntimeExpression) MarshalJSON() ([]byte, error) {
97 | return json.Marshal(r.Value)
98 | }
99 |
100 | func (r *RuntimeExpression) String() string {
101 | return r.Value
102 | }
103 |
104 | func (r *RuntimeExpression) GetValue() interface{} {
105 | return r.Value
106 | }
107 |
--------------------------------------------------------------------------------
/model/runtime_expression_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestRuntimeExpressionUnmarshalJSON(t *testing.T) {
25 | tests := []struct {
26 | Name string
27 | JSONInput string
28 | Expected string
29 | ExpectErr bool
30 | }{
31 | {
32 | Name: "Valid RuntimeExpression",
33 | JSONInput: `{ "expression": "${runtime.value}" }`,
34 | Expected: "${runtime.value}",
35 | ExpectErr: false,
36 | },
37 | {
38 | Name: "Invalid RuntimeExpression",
39 | JSONInput: `{ "expression": "1234invalid_runtime" }`,
40 | Expected: "",
41 | ExpectErr: true,
42 | },
43 | }
44 |
45 | for _, tc := range tests {
46 | t.Run(tc.Name, func(t *testing.T) {
47 | var acme *RuntimeExpressionAcme
48 | err := json.Unmarshal([]byte(tc.JSONInput), &acme)
49 |
50 | if tc.ExpectErr {
51 | assert.Error(t, err)
52 | } else {
53 | assert.NoError(t, err)
54 | assert.Equal(t, tc.Expected, acme.Expression.Value)
55 | }
56 |
57 | // Test marshalling
58 | if !tc.ExpectErr {
59 | output, err := json.Marshal(acme)
60 | assert.NoError(t, err)
61 | assert.JSONEq(t, tc.JSONInput, string(output))
62 | }
63 | })
64 | }
65 | }
66 |
67 | // EndpointAcme represents a struct using URITemplate.
68 | type RuntimeExpressionAcme struct {
69 | Expression RuntimeExpression `json:"expression"`
70 | }
71 |
72 | func TestIsStrictExpr(t *testing.T) {
73 | tests := []struct {
74 | name string
75 | expression string
76 | want bool
77 | }{
78 | {
79 | name: "StrictExpr with braces",
80 | expression: "${.some.path}",
81 | want: true,
82 | },
83 | {
84 | name: "Missing closing brace",
85 | expression: "${.some.path",
86 | want: false,
87 | },
88 | {
89 | name: "Missing opening brace",
90 | expression: ".some.path}",
91 | want: false,
92 | },
93 | {
94 | name: "Empty string",
95 | expression: "",
96 | want: false,
97 | },
98 | {
99 | name: "No braces at all",
100 | expression: ".some.path",
101 | want: false,
102 | },
103 | {
104 | name: "With spaces but still correct",
105 | expression: "${ .some.path }",
106 | want: true,
107 | },
108 | {
109 | name: "Only braces",
110 | expression: "${}",
111 | want: true, // Technically matches prefix+suffix
112 | },
113 | }
114 |
115 | for _, tc := range tests {
116 | t.Run(tc.name, func(t *testing.T) {
117 | got := IsStrictExpr(tc.expression)
118 | if got != tc.want {
119 | t.Errorf("IsStrictExpr(%q) = %v, want %v", tc.expression, got, tc.want)
120 | }
121 | })
122 | }
123 | }
124 |
125 | func TestSanitize(t *testing.T) {
126 | tests := []struct {
127 | name string
128 | expression string
129 | want string
130 | }{
131 | {
132 | name: "Remove braces and replace single quotes",
133 | expression: "${ 'some.path' }",
134 | want: `"some.path"`,
135 | },
136 | {
137 | name: "Already sanitized string, no braces",
138 | expression: ".some.path",
139 | want: ".some.path",
140 | },
141 | {
142 | name: "Multiple single quotes",
143 | expression: "${ 'foo' + 'bar' }",
144 | want: `"foo" + "bar"`,
145 | },
146 | {
147 | name: "Only braces with spaces",
148 | expression: "${ }",
149 | want: "",
150 | },
151 | {
152 | name: "No braces, just single quotes to be replaced",
153 | expression: "'some.path'",
154 | want: `"some.path"`,
155 | },
156 | {
157 | name: "Nothing to sanitize",
158 | expression: "",
159 | want: "",
160 | },
161 | }
162 |
163 | for _, tc := range tests {
164 | t.Run(tc.name, func(t *testing.T) {
165 | got := SanitizeExpr(tc.expression)
166 | if got != tc.want {
167 | t.Errorf("Sanitize(%q) = %q, want %q", tc.expression, got, tc.want)
168 | }
169 | })
170 | }
171 | }
172 |
173 | func TestIsValid(t *testing.T) {
174 | tests := []struct {
175 | name string
176 | expression string
177 | want bool
178 | }{
179 | {
180 | name: "Valid expression - simple path",
181 | expression: "${ .foo }",
182 | want: true,
183 | },
184 | {
185 | name: "Valid expression - array slice",
186 | expression: "${ .arr[0] }",
187 | want: true,
188 | },
189 | {
190 | name: "Invalid syntax",
191 | expression: "${ .foo( }",
192 | want: false,
193 | },
194 | {
195 | name: "No braces but valid JQ (directly provided)",
196 | expression: ".bar",
197 | want: true,
198 | },
199 | {
200 | name: "Empty expression",
201 | expression: "",
202 | want: true, // empty is parseable but yields an empty query
203 | },
204 | {
205 | name: "Invalid bracket usage",
206 | expression: "${ .arr[ }",
207 | want: false,
208 | },
209 | }
210 |
211 | for _, tc := range tests {
212 | t.Run(tc.name, func(t *testing.T) {
213 | got := IsValidExpr(tc.expression)
214 | if got != tc.want {
215 | t.Errorf("IsValid(%q) = %v, want %v", tc.expression, got, tc.want)
216 | }
217 | })
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/model/task_call.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import "encoding/json"
18 |
19 | type CallHTTP struct {
20 | TaskBase `json:",inline"` // Inline TaskBase fields
21 | Call string `json:"call" validate:"required,eq=http"`
22 | With HTTPArguments `json:"with" validate:"required"`
23 | }
24 |
25 | func (c *CallHTTP) GetBase() *TaskBase {
26 | return &c.TaskBase
27 | }
28 |
29 | type HTTPArguments struct {
30 | Method string `json:"method" validate:"required,oneofci=GET POST PUT DELETE PATCH"`
31 | Endpoint *Endpoint `json:"endpoint" validate:"required"`
32 | Headers map[string]string `json:"headers,omitempty"`
33 | Body json.RawMessage `json:"body,omitempty"`
34 | Query map[string]interface{} `json:"query,omitempty"`
35 | Output string `json:"output,omitempty" validate:"omitempty,oneof=raw content response"`
36 | }
37 |
38 | type CallOpenAPI struct {
39 | TaskBase `json:",inline"` // Inline TaskBase fields
40 | Call string `json:"call" validate:"required,eq=openapi"`
41 | With OpenAPIArguments `json:"with" validate:"required"`
42 | }
43 |
44 | func (c *CallOpenAPI) GetBase() *TaskBase {
45 | return &c.TaskBase
46 | }
47 |
48 | type OpenAPIArguments struct {
49 | Document *ExternalResource `json:"document" validate:"required"`
50 | OperationID string `json:"operationId" validate:"required"`
51 | Parameters map[string]interface{} `json:"parameters,omitempty"`
52 | Authentication *ReferenceableAuthenticationPolicy `json:"authentication,omitempty"`
53 | Output string `json:"output,omitempty" validate:"omitempty,oneof=raw content response"`
54 | }
55 |
56 | type CallGRPC struct {
57 | TaskBase `json:",inline"`
58 | Call string `json:"call" validate:"required,eq=grpc"`
59 | With GRPCArguments `json:"with" validate:"required"`
60 | }
61 |
62 | func (c *CallGRPC) GetBase() *TaskBase {
63 | return &c.TaskBase
64 | }
65 |
66 | type GRPCArguments struct {
67 | Proto *ExternalResource `json:"proto" validate:"required"`
68 | Service GRPCService `json:"service" validate:"required"`
69 | Method string `json:"method" validate:"required"`
70 | Arguments map[string]interface{} `json:"arguments,omitempty"`
71 | Authentication *ReferenceableAuthenticationPolicy `json:"authentication,omitempty" validate:"omitempty"`
72 | }
73 |
74 | type GRPCService struct {
75 | Name string `json:"name" validate:"required"`
76 | Host string `json:"host" validate:"required,hostname_rfc1123"`
77 | Port int `json:"port" validate:"required,min=0,max=65535"`
78 | Authentication *ReferenceableAuthenticationPolicy `json:"authentication,omitempty"`
79 | }
80 |
81 | type CallAsyncAPI struct {
82 | TaskBase `json:",inline"`
83 | Call string `json:"call" validate:"required,eq=asyncapi"`
84 | With AsyncAPIArguments `json:"with" validate:"required"`
85 | }
86 |
87 | func (c *CallAsyncAPI) GetBase() *TaskBase {
88 | return &c.TaskBase
89 | }
90 |
91 | type AsyncAPIArguments struct {
92 | Document *ExternalResource `json:"document" validate:"required"`
93 | Channel string `json:"channel,omitempty"`
94 | Operation string `json:"operation,omitempty"`
95 | Server *AsyncAPIServer `json:"server,omitempty"`
96 | Protocol string `json:"protocol,omitempty" validate:"omitempty,oneof=amqp amqp1 anypointmq googlepubsub http ibmmq jms kafka mercure mqtt mqtt5 nats pulsar redis sns solace sqs stomp ws"`
97 | Message *AsyncAPIOutboundMessage `json:"message,omitempty"`
98 | Subscription *AsyncAPISubscription `json:"subscription,omitempty"`
99 | Authentication *ReferenceableAuthenticationPolicy `json:"authentication,omitempty" validate:"omitempty"`
100 | }
101 |
102 | type AsyncAPIServer struct {
103 | Name string `json:"name" validate:"required"`
104 | Variables map[string]interface{} `json:"variables,omitempty"`
105 | }
106 |
107 | type AsyncAPIOutboundMessage struct {
108 | Payload map[string]interface{} `json:"payload,omitempty" validate:"omitempty"`
109 | Headers map[string]interface{} `json:"headers,omitempty" validate:"omitempty"`
110 | }
111 |
112 | type AsyncAPISubscription struct {
113 | Filter *RuntimeExpression `json:"filter,omitempty"`
114 | Consume *AsyncAPIMessageConsumptionPolicy `json:"consume" validate:"required"`
115 | }
116 |
117 | type AsyncAPIMessageConsumptionPolicy struct {
118 | For *Duration `json:"for,omitempty"`
119 | Amount int `json:"amount,omitempty" validate:"required_without_all=While Until"`
120 | While *RuntimeExpression `json:"while,omitempty" validate:"required_without_all=Amount Until"`
121 | Until *RuntimeExpression `json:"until,omitempty" validate:"required_without_all=Amount While"`
122 | }
123 |
124 | type CallFunction struct {
125 | TaskBase `json:",inline"` // Inline TaskBase fields
126 | Call string `json:"call" validate:"required"`
127 | With map[string]interface{} `json:"with,omitempty"`
128 | }
129 |
130 | func (c *CallFunction) GetBase() *TaskBase {
131 | return &c.TaskBase
132 | }
133 |
--------------------------------------------------------------------------------
/model/task_do.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | // DoTask represents a task configuration to execute tasks sequentially.
18 | type DoTask struct {
19 | TaskBase `json:",inline"` // Inline TaskBase fields
20 | Do *TaskList `json:"do" validate:"required,dive"`
21 | }
22 |
23 | func (d *DoTask) GetBase() *TaskBase {
24 | return &d.TaskBase
25 | }
26 |
--------------------------------------------------------------------------------
/model/task_do_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestDoTask_UnmarshalJSON(t *testing.T) {
25 | jsonData := `{
26 | "do": [
27 | {"task1": {"call": "http", "with": {"method": "GET", "endpoint": "http://example.com"}}},
28 | {"task2": {"call": "openapi", "with": {"document": {"name": "doc1"}, "operationId": "op1"}}}
29 | ]
30 | }`
31 |
32 | var doTask DoTask
33 | err := json.Unmarshal([]byte(jsonData), &doTask)
34 | assert.NoError(t, err)
35 |
36 | task1 := doTask.Do.Key("task1").AsCallHTTPTask()
37 | assert.NotNil(t, task1)
38 | assert.Equal(t, "http", task1.Call)
39 | assert.Equal(t, "GET", task1.With.Method)
40 | assert.Equal(t, "http://example.com", task1.With.Endpoint.String())
41 |
42 | task2 := doTask.Do.Key("task2").AsCallOpenAPITask()
43 | assert.NotNil(t, task2)
44 | assert.Equal(t, "openapi", task2.Call)
45 | assert.Equal(t, "doc1", task2.With.Document.Name)
46 | assert.Equal(t, "op1", task2.With.OperationID)
47 | }
48 |
49 | func TestDoTask_MarshalJSON(t *testing.T) {
50 | doTask := DoTask{
51 | TaskBase: TaskBase{},
52 | Do: &TaskList{
53 | {Key: "task1", Task: &CallHTTP{
54 | Call: "http",
55 | With: HTTPArguments{
56 | Method: "GET",
57 | Endpoint: NewEndpoint("http://example.com"),
58 | },
59 | }},
60 | {Key: "task2", Task: &CallOpenAPI{
61 | Call: "openapi",
62 | With: OpenAPIArguments{
63 | Document: &ExternalResource{Name: "doc1", Endpoint: NewEndpoint("http://example.com")},
64 | OperationID: "op1",
65 | },
66 | }},
67 | },
68 | }
69 |
70 | data, err := json.Marshal(doTask)
71 | assert.NoError(t, err)
72 | assert.JSONEq(t, `{
73 | "do": [
74 | {"task1": {"call": "http", "with": {"method": "GET", "endpoint": "http://example.com"}}},
75 | {"task2": {"call": "openapi", "with": {"document": {"name": "doc1", "endpoint": "http://example.com"}, "operationId": "op1"}}}
76 | ]
77 | }`, string(data))
78 | }
79 |
80 | func TestDoTask_Validation(t *testing.T) {
81 | doTask := DoTask{
82 | TaskBase: TaskBase{},
83 | Do: &TaskList{
84 | {Key: "task1", Task: &CallHTTP{
85 | Call: "http",
86 | With: HTTPArguments{
87 | Method: "GET",
88 | Endpoint: NewEndpoint("http://example.com"),
89 | },
90 | }},
91 | {Key: "task2", Task: &CallOpenAPI{
92 | Call: "openapi",
93 | With: OpenAPIArguments{
94 | Document: &ExternalResource{Name: "doc1"}, //missing endpoint
95 | OperationID: "op1",
96 | },
97 | }},
98 | },
99 | }
100 |
101 | err := validate.Struct(doTask)
102 | assert.Error(t, err)
103 | }
104 |
--------------------------------------------------------------------------------
/model/task_for.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | // ForTask represents a task configuration to iterate over a collection.
18 | type ForTask struct {
19 | TaskBase `json:",inline"` // Inline TaskBase fields
20 | For ForTaskConfiguration `json:"for" validate:"required"`
21 | While string `json:"while,omitempty"`
22 | Do *TaskList `json:"do" validate:"required,dive"`
23 | }
24 |
25 | func (f *ForTask) GetBase() *TaskBase {
26 | return &f.TaskBase
27 | }
28 |
29 | // ForTaskConfiguration defines the loop configuration for iterating over a collection.
30 | type ForTaskConfiguration struct {
31 | Each string `json:"each,omitempty"` // Variable name for the current item
32 | In string `json:"in" validate:"required"` // Runtime expression for the collection
33 | At string `json:"at,omitempty"` // Variable name for the current index
34 | }
35 |
--------------------------------------------------------------------------------
/model/task_for_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "sigs.k8s.io/yaml"
22 |
23 | "github.com/stretchr/testify/assert"
24 | )
25 |
26 | func TestForTask_UnmarshalJSON(t *testing.T) {
27 | jsonData := `{
28 | "for": {"each": "item", "in": "${items}", "at": "index"},
29 | "while": "${condition}",
30 | "do": [
31 | {"task1": {"call": "http", "with": {"method": "GET", "endpoint": "http://example.com"}}},
32 | {"task2": {"call": "openapi", "with": {"document": {"name": "doc1"}, "operationId": "op1"}}}
33 | ]
34 | }`
35 |
36 | var forTask ForTask
37 | err := json.Unmarshal([]byte(jsonData), &forTask)
38 | assert.NoError(t, err)
39 | assert.Equal(t, "item", forTask.For.Each)
40 | assert.Equal(t, "${items}", forTask.For.In)
41 | assert.Equal(t, "index", forTask.For.At)
42 | assert.Equal(t, "${condition}", forTask.While)
43 |
44 | task1 := forTask.Do.Key("task1").AsCallHTTPTask()
45 | assert.NotNil(t, task1)
46 | assert.Equal(t, "http", task1.Call)
47 | assert.Equal(t, "GET", task1.With.Method)
48 | assert.Equal(t, "http://example.com", task1.With.Endpoint.String())
49 |
50 | task2 := forTask.Do.Key("task2").AsCallOpenAPITask()
51 | assert.NotNil(t, task2)
52 | assert.Equal(t, "openapi", task2.Call)
53 | assert.Equal(t, "doc1", task2.With.Document.Name)
54 | assert.Equal(t, "op1", task2.With.OperationID)
55 | }
56 |
57 | func TestForTask_MarshalJSON(t *testing.T) {
58 | forTask := ForTask{
59 | TaskBase: TaskBase{},
60 | For: ForTaskConfiguration{
61 | Each: "item",
62 | In: "${items}",
63 | At: "index",
64 | },
65 | While: "${condition}",
66 | Do: &TaskList{
67 | {Key: "task1", Task: &CallHTTP{
68 | Call: "http",
69 | With: HTTPArguments{
70 | Method: "GET",
71 | Endpoint: NewEndpoint("http://example.com"),
72 | },
73 | }},
74 | {Key: "task2", Task: &CallOpenAPI{
75 | Call: "openapi",
76 | With: OpenAPIArguments{
77 | Document: &ExternalResource{Name: "doc1", Endpoint: NewEndpoint("http://example.com")},
78 | OperationID: "op1",
79 | },
80 | }},
81 | },
82 | }
83 |
84 | data, err := json.Marshal(forTask)
85 | assert.NoError(t, err)
86 | assert.JSONEq(t, `{
87 | "for": {"each": "item", "in": "${items}", "at": "index"},
88 | "while": "${condition}",
89 | "do": [
90 | {"task1": {"call": "http", "with": {"method": "GET", "endpoint": "http://example.com"}}},
91 | {"task2": {"call": "openapi", "with": {"document": {"name": "doc1", "endpoint": "http://example.com"}, "operationId": "op1"}}}
92 | ]
93 | }`, string(data))
94 | }
95 |
96 | func TestForTask_Validation(t *testing.T) {
97 | forTask := ForTask{
98 | TaskBase: TaskBase{},
99 | For: ForTaskConfiguration{
100 | Each: "item",
101 | In: "${items}",
102 | At: "index",
103 | },
104 | While: "${condition}",
105 | Do: &TaskList{
106 | {Key: "task1", Task: &CallHTTP{
107 | Call: "http",
108 | With: HTTPArguments{
109 | Method: "GET",
110 | Endpoint: &Endpoint{URITemplate: &LiteralUri{Value: "http://example.com"}},
111 | },
112 | }},
113 | {Key: "task2", Task: &CallOpenAPI{
114 | Call: "openapi",
115 | With: OpenAPIArguments{
116 | Document: &ExternalResource{Name: "doc1"}, //missing endpoint
117 | OperationID: "op1",
118 | },
119 | }},
120 | },
121 | }
122 |
123 | err := validate.Struct(forTask)
124 | assert.Error(t, err)
125 | }
126 |
127 | func TestForTaskValidation(t *testing.T) {
128 | rawYaml := `
129 | for:
130 | each: pet
131 | in: .pets
132 | at: index
133 | while: .vet != null
134 | do:
135 | - waitForCheckup:
136 | listen:
137 | to:
138 | one:
139 | with:
140 | type: com.fake.petclinic.pets.checkup.completed.v2
141 | output:
142 | as: '.pets + [{ "id": $pet.id }]'
143 | `
144 |
145 | var forTask ForTask
146 | err := yaml.Unmarshal([]byte(rawYaml), &forTask)
147 | assert.NoError(t, err, "Failed to unmarshal ForTask")
148 |
149 | err = validate.Struct(forTask)
150 | assert.NoError(t, err, "Failed to validate ForTask")
151 | }
152 |
--------------------------------------------------------------------------------
/model/task_fork.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | // ForkTask represents a task configuration to execute multiple tasks concurrently.
18 | type ForkTask struct {
19 | TaskBase `json:",inline"` // Inline TaskBase fields
20 | Fork ForkTaskConfiguration `json:"fork" validate:"required"`
21 | }
22 |
23 | func (f *ForkTask) GetBase() *TaskBase {
24 | return &f.TaskBase
25 | }
26 |
27 | // ForkTaskConfiguration defines the configuration for the branches to perform concurrently.
28 | type ForkTaskConfiguration struct {
29 | Branches *TaskList `json:"branches" validate:"required,dive"`
30 | Compete bool `json:"compete,omitempty"`
31 | }
32 |
--------------------------------------------------------------------------------
/model/task_fork_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestForkTask_UnmarshalJSON(t *testing.T) {
25 | jsonData := `{
26 | "fork": {
27 | "branches": [
28 | {"task1": {"call": "http", "with": {"method": "GET", "endpoint": "http://example.com"}}},
29 | {"task2": {"call": "openapi", "with": {"document": {"name": "doc1"}, "operationId": "op1"}}}
30 | ],
31 | "compete": true
32 | }
33 | }`
34 |
35 | var forkTask ForkTask
36 | err := json.Unmarshal([]byte(jsonData), &forkTask)
37 | assert.NoError(t, err)
38 | assert.Equal(t, true, forkTask.Fork.Compete)
39 |
40 | task1 := forkTask.Fork.Branches.Key("task1").AsCallHTTPTask()
41 | assert.NotNil(t, task1)
42 | assert.Equal(t, "http", task1.Call)
43 | assert.Equal(t, "GET", task1.With.Method)
44 | assert.Equal(t, "http://example.com", task1.With.Endpoint.URITemplate.String())
45 |
46 | task2 := forkTask.Fork.Branches.Key("task2").AsCallOpenAPITask()
47 | assert.NotNil(t, task2)
48 | assert.Equal(t, "openapi", task2.Call)
49 | assert.Equal(t, "doc1", task2.With.Document.Name)
50 | assert.Equal(t, "op1", task2.With.OperationID)
51 | }
52 |
53 | func TestForkTask_MarshalJSON(t *testing.T) {
54 | forkTask := ForkTask{
55 | TaskBase: TaskBase{},
56 | Fork: ForkTaskConfiguration{
57 | Branches: &TaskList{
58 | {Key: "task1", Task: &CallHTTP{
59 | Call: "http",
60 | With: HTTPArguments{
61 | Method: "GET",
62 | Endpoint: NewEndpoint("http://example.com"),
63 | },
64 | }},
65 | {Key: "task2", Task: &CallOpenAPI{
66 | Call: "openapi",
67 | With: OpenAPIArguments{
68 | Document: &ExternalResource{Name: "doc1", Endpoint: NewEndpoint("http://example.com")},
69 | OperationID: "op1",
70 | },
71 | }},
72 | },
73 | Compete: true,
74 | },
75 | }
76 |
77 | data, err := json.Marshal(forkTask)
78 | assert.NoError(t, err)
79 | assert.JSONEq(t, `{
80 | "fork": {
81 | "branches": [
82 | {"task1": {"call": "http", "with": {"method": "GET", "endpoint": "http://example.com"}}},
83 | {"task2": {"call": "openapi", "with": {"document": {"name": "doc1", "endpoint": "http://example.com"}, "operationId": "op1"}}}
84 | ],
85 | "compete": true
86 | }
87 | }`, string(data))
88 | }
89 |
90 | func TestForkTask_Validation(t *testing.T) {
91 | forkTask := ForkTask{
92 | TaskBase: TaskBase{},
93 | Fork: ForkTaskConfiguration{
94 | Branches: &TaskList{
95 | {Key: "task1", Task: &CallHTTP{
96 | Call: "http",
97 | With: HTTPArguments{
98 | Method: "GET",
99 | Endpoint: NewEndpoint("http://example.com"),
100 | },
101 | }},
102 | {Key: "task2", Task: &CallOpenAPI{
103 | Call: "openapi",
104 | With: OpenAPIArguments{
105 | Document: &ExternalResource{Name: "doc1"}, //missing endpoint
106 | OperationID: "op1",
107 | },
108 | }},
109 | },
110 | Compete: true,
111 | },
112 | }
113 |
114 | err := validate.Struct(forkTask)
115 | assert.Error(t, err)
116 | }
117 |
--------------------------------------------------------------------------------
/model/task_raise.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "errors"
20 | )
21 |
22 | // RaiseTask represents a task configuration to raise errors.
23 | type RaiseTask struct {
24 | TaskBase `json:",inline"` // Inline TaskBase fields
25 | Raise RaiseTaskConfiguration `json:"raise" validate:"required"`
26 | }
27 |
28 | func (r *RaiseTask) GetBase() *TaskBase {
29 | return &r.TaskBase
30 | }
31 |
32 | type RaiseTaskConfiguration struct {
33 | Error RaiseTaskError `json:"error" validate:"required"`
34 | }
35 |
36 | type RaiseTaskError struct {
37 | Definition *Error `json:"-"`
38 | Ref *string `json:"-"`
39 | }
40 |
41 | // UnmarshalJSON for RaiseTaskError to enforce "oneOf" behavior.
42 | func (rte *RaiseTaskError) UnmarshalJSON(data []byte) error {
43 | // Try to unmarshal into a string (Ref)
44 | var ref string
45 | if err := json.Unmarshal(data, &ref); err == nil {
46 | rte.Ref = &ref
47 | rte.Definition = nil
48 | return nil
49 | }
50 |
51 | // Try to unmarshal into an Error (Definition)
52 | var def Error
53 | if err := json.Unmarshal(data, &def); err == nil {
54 | rte.Definition = &def
55 | rte.Ref = nil
56 | return nil
57 | }
58 |
59 | // If neither worked, return an error
60 | return errors.New("invalid RaiseTaskError: data must be either a string (reference) or an object (definition)")
61 | }
62 |
63 | // MarshalJSON for RaiseTaskError to ensure proper serialization.
64 | func (rte *RaiseTaskError) MarshalJSON() ([]byte, error) {
65 | if rte.Definition != nil {
66 | return json.Marshal(rte.Definition)
67 | }
68 | if rte.Ref != nil {
69 | return json.Marshal(*rte.Ref)
70 | }
71 | return nil, errors.New("invalid RaiseTaskError: neither 'definition' nor 'reference' is set")
72 | }
73 |
--------------------------------------------------------------------------------
/model/task_raise_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestRaiseTask_MarshalJSON(t *testing.T) {
25 | raiseTask := &RaiseTask{
26 | TaskBase: TaskBase{
27 | If: &RuntimeExpression{Value: "${condition}"},
28 | Input: &Input{From: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"key": "value"}}},
29 | Output: &Output{As: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"result": "output"}}},
30 | Timeout: &TimeoutOrReference{Timeout: &Timeout{After: NewDurationExpr("10s")}},
31 | Then: &FlowDirective{Value: "continue"},
32 | Metadata: map[string]interface{}{
33 | "meta": "data",
34 | },
35 | },
36 | Raise: RaiseTaskConfiguration{
37 | Error: RaiseTaskError{
38 | Definition: &Error{
39 | Type: &URITemplateOrRuntimeExpr{Value: "http://example.com/error"},
40 | Status: 500,
41 | Title: NewStringOrRuntimeExpr("Internal Server Error"),
42 | Detail: NewStringOrRuntimeExpr("An unexpected error occurred."),
43 | },
44 | },
45 | },
46 | }
47 |
48 | data, err := json.Marshal(raiseTask)
49 | assert.NoError(t, err)
50 | assert.JSONEq(t, `{
51 | "if": "${condition}",
52 | "input": { "from": {"key": "value"} },
53 | "output": { "as": {"result": "output"} },
54 | "timeout": { "after": "10s" },
55 | "then": "continue",
56 | "metadata": {"meta": "data"},
57 | "raise": {
58 | "error": {
59 | "type": "http://example.com/error",
60 | "status": 500,
61 | "title": "Internal Server Error",
62 | "detail": "An unexpected error occurred."
63 | }
64 | }
65 | }`, string(data))
66 | }
67 |
68 | func TestRaiseTask_UnmarshalJSON(t *testing.T) {
69 | jsonData := `{
70 | "if": "${condition}",
71 | "input": { "from": {"key": "value"} },
72 | "output": { "as": {"result": "output"} },
73 | "timeout": { "after": "10s" },
74 | "then": "continue",
75 | "metadata": {"meta": "data"},
76 | "raise": {
77 | "error": {
78 | "type": "http://example.com/error",
79 | "status": 500,
80 | "title": "Internal Server Error",
81 | "detail": "An unexpected error occurred."
82 | }
83 | }
84 | }`
85 |
86 | var raiseTask *RaiseTask
87 | err := json.Unmarshal([]byte(jsonData), &raiseTask)
88 | assert.NoError(t, err)
89 | assert.Equal(t, &RuntimeExpression{Value: "${condition}"}, raiseTask.If)
90 | assert.Equal(t, &Input{From: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"key": "value"}}}, raiseTask.Input)
91 | assert.Equal(t, &Output{As: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"result": "output"}}}, raiseTask.Output)
92 | assert.Equal(t, &TimeoutOrReference{Timeout: &Timeout{After: NewDurationExpr("10s")}}, raiseTask.Timeout)
93 | assert.Equal(t, &FlowDirective{Value: "continue"}, raiseTask.Then)
94 | assert.Equal(t, map[string]interface{}{"meta": "data"}, raiseTask.Metadata)
95 | assert.Equal(t, "http://example.com/error", raiseTask.Raise.Error.Definition.Type.String())
96 | assert.Equal(t, 500, raiseTask.Raise.Error.Definition.Status)
97 | assert.Equal(t, "Internal Server Error", raiseTask.Raise.Error.Definition.Title.String())
98 | assert.Equal(t, "An unexpected error occurred.", raiseTask.Raise.Error.Definition.Detail.String())
99 | }
100 |
--------------------------------------------------------------------------------
/model/task_run.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "errors"
20 | )
21 |
22 | // RunTask represents a task configuration to execute external processes.
23 | type RunTask struct {
24 | TaskBase `json:",inline"` // Inline TaskBase fields
25 | Run RunTaskConfiguration `json:"run" validate:"required"`
26 | }
27 |
28 | func (r *RunTask) GetBase() *TaskBase {
29 | return &r.TaskBase
30 | }
31 |
32 | type RunTaskConfiguration struct {
33 | Await *bool `json:"await,omitempty"`
34 | Container *Container `json:"container,omitempty"`
35 | Script *Script `json:"script,omitempty"`
36 | Shell *Shell `json:"shell,omitempty"`
37 | Workflow *RunWorkflow `json:"workflow,omitempty"`
38 | }
39 |
40 | type Container struct {
41 | Image string `json:"image" validate:"required"`
42 | Command string `json:"command,omitempty"`
43 | Ports map[string]interface{} `json:"ports,omitempty"`
44 | Volumes map[string]interface{} `json:"volumes,omitempty"`
45 | Environment map[string]string `json:"environment,omitempty"`
46 | }
47 |
48 | type Script struct {
49 | Language string `json:"language" validate:"required"`
50 | Arguments map[string]interface{} `json:"arguments,omitempty"`
51 | Environment map[string]string `json:"environment,omitempty"`
52 | InlineCode *string `json:"code,omitempty"`
53 | External *ExternalResource `json:"source,omitempty"`
54 | }
55 |
56 | type Shell struct {
57 | Command string `json:"command" validate:"required"`
58 | Arguments map[string]interface{} `json:"arguments,omitempty"`
59 | Environment map[string]string `json:"environment,omitempty"`
60 | }
61 |
62 | type RunWorkflow struct {
63 | Namespace string `json:"namespace" validate:"required,hostname_rfc1123"`
64 | Name string `json:"name" validate:"required,hostname_rfc1123"`
65 | Version string `json:"version" validate:"required,semver_pattern"`
66 | Input map[string]interface{} `json:"input,omitempty"`
67 | }
68 |
69 | // UnmarshalJSON for RunTaskConfiguration to enforce "oneOf" behavior.
70 | func (rtc *RunTaskConfiguration) UnmarshalJSON(data []byte) error {
71 | temp := struct {
72 | Await *bool `json:"await"`
73 | Container *Container `json:"container"`
74 | Script *Script `json:"script"`
75 | Shell *Shell `json:"shell"`
76 | Workflow *RunWorkflow `json:"workflow"`
77 | }{}
78 |
79 | if err := json.Unmarshal(data, &temp); err != nil {
80 | return err
81 | }
82 |
83 | // Count non-nil fields
84 | count := 0
85 | if temp.Container != nil {
86 | count++
87 | rtc.Container = temp.Container
88 | }
89 | if temp.Script != nil {
90 | count++
91 | rtc.Script = temp.Script
92 | }
93 | if temp.Shell != nil {
94 | count++
95 | rtc.Shell = temp.Shell
96 | }
97 | if temp.Workflow != nil {
98 | count++
99 | rtc.Workflow = temp.Workflow
100 | }
101 |
102 | // Ensure only one of the options is set
103 | if count != 1 {
104 | return errors.New("invalid RunTaskConfiguration: only one of 'container', 'script', 'shell', or 'workflow' must be specified")
105 | }
106 |
107 | rtc.Await = temp.Await
108 | return nil
109 | }
110 |
111 | // MarshalJSON for RunTaskConfiguration to ensure proper serialization.
112 | func (rtc *RunTaskConfiguration) MarshalJSON() ([]byte, error) {
113 | temp := struct {
114 | Await *bool `json:"await,omitempty"`
115 | Container *Container `json:"container,omitempty"`
116 | Script *Script `json:"script,omitempty"`
117 | Shell *Shell `json:"shell,omitempty"`
118 | Workflow *RunWorkflow `json:"workflow,omitempty"`
119 | }{
120 | Await: rtc.Await,
121 | Container: rtc.Container,
122 | Script: rtc.Script,
123 | Shell: rtc.Shell,
124 | Workflow: rtc.Workflow,
125 | }
126 |
127 | return json.Marshal(temp)
128 | }
129 |
--------------------------------------------------------------------------------
/model/task_set.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import "encoding/json"
18 |
19 | // SetTask represents a task used to set data.
20 | type SetTask struct {
21 | TaskBase `json:",inline"` // Inline TaskBase fields
22 | Set map[string]interface{} `json:"set" validate:"required,min=1,dive"`
23 | }
24 |
25 | func (st *SetTask) GetBase() *TaskBase {
26 | return &st.TaskBase
27 | }
28 |
29 | // MarshalJSON for SetTask to ensure proper serialization.
30 | func (st *SetTask) MarshalJSON() ([]byte, error) {
31 | type Alias SetTask
32 | return json.Marshal((*Alias)(st))
33 | }
34 |
35 | // UnmarshalJSON for SetTask to ensure proper deserialization.
36 | func (st *SetTask) UnmarshalJSON(data []byte) error {
37 | type Alias SetTask
38 | alias := (*Alias)(st)
39 | return json.Unmarshal(data, alias)
40 | }
41 |
--------------------------------------------------------------------------------
/model/task_set_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestSetTask_MarshalJSON(t *testing.T) {
25 | setTask := SetTask{
26 | TaskBase: TaskBase{
27 | If: &RuntimeExpression{Value: "${condition}"},
28 | Input: &Input{From: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"key": "value"}}},
29 | Output: &Output{As: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"result": "output"}}},
30 | Timeout: &TimeoutOrReference{Timeout: &Timeout{After: NewDurationExpr("10s")}},
31 | Then: &FlowDirective{Value: "continue"},
32 | Metadata: map[string]interface{}{
33 | "meta": "data",
34 | },
35 | },
36 | Set: map[string]interface{}{
37 | "key1": "value1",
38 | "key2": 42,
39 | },
40 | }
41 |
42 | data, err := json.Marshal(setTask)
43 | assert.NoError(t, err)
44 | assert.JSONEq(t, `{
45 | "if": "${condition}",
46 | "input": { "from": {"key": "value"} },
47 | "output": { "as": {"result": "output"} },
48 | "timeout": { "after": "10s" },
49 | "then": "continue",
50 | "metadata": {"meta": "data"},
51 | "set": {
52 | "key1": "value1",
53 | "key2": 42
54 | }
55 | }`, string(data))
56 | }
57 |
58 | func TestSetTask_UnmarshalJSON(t *testing.T) {
59 | jsonData := `{
60 | "if": "${condition}",
61 | "input": { "from": {"key": "value"} },
62 | "output": { "as": {"result": "output"} },
63 | "timeout": { "after": "10s" },
64 | "then": "continue",
65 | "metadata": {"meta": "data"},
66 | "set": {
67 | "key1": "value1",
68 | "key2": 42
69 | }
70 | }`
71 |
72 | var setTask SetTask
73 | err := json.Unmarshal([]byte(jsonData), &setTask)
74 | assert.NoError(t, err)
75 | assert.Equal(t, &RuntimeExpression{Value: "${condition}"}, setTask.If)
76 | assert.Equal(t, &Input{From: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"key": "value"}}}, setTask.Input)
77 | assert.Equal(t, &Output{As: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"result": "output"}}}, setTask.Output)
78 | assert.Equal(t, &TimeoutOrReference{Timeout: &Timeout{After: NewDurationExpr("10s")}}, setTask.Timeout)
79 | assert.Equal(t, &FlowDirective{Value: "continue"}, setTask.Then)
80 | assert.Equal(t, map[string]interface{}{"meta": "data"}, setTask.Metadata)
81 | expectedSet := map[string]interface{}{
82 | "key1": "value1",
83 | "key2": float64(42), // Match JSON unmarshaling behavior
84 | }
85 | assert.Equal(t, expectedSet, setTask.Set)
86 | }
87 |
88 | func TestSetTask_Validation(t *testing.T) {
89 | // Valid SetTask
90 | setTask := SetTask{
91 | TaskBase: TaskBase{},
92 | Set: map[string]interface{}{
93 | "key": "value",
94 | },
95 | }
96 | assert.NoError(t, validate.Struct(setTask))
97 |
98 | // Invalid SetTask (empty set)
99 | invalidSetTask := SetTask{
100 | TaskBase: TaskBase{},
101 | Set: map[string]interface{}{},
102 | }
103 | assert.Error(t, validate.Struct(invalidSetTask))
104 | }
105 |
--------------------------------------------------------------------------------
/model/task_switch.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import "encoding/json"
18 |
19 | // SwitchTask represents a task configuration for conditional branching.
20 | type SwitchTask struct {
21 | TaskBase `json:",inline"` // Inline TaskBase fields
22 | Switch []SwitchItem `json:"switch" validate:"required,min=1,dive,switch_item"`
23 | }
24 |
25 | func (st *SwitchTask) GetBase() *TaskBase {
26 | return &st.TaskBase
27 | }
28 |
29 | type SwitchItem map[string]SwitchCase
30 |
31 | // SwitchCase defines a condition and the corresponding outcome for a switch task.
32 | type SwitchCase struct {
33 | When *RuntimeExpression `json:"when,omitempty"`
34 | Then *FlowDirective `json:"then" validate:"required"`
35 | }
36 |
37 | // MarshalJSON for SwitchTask to ensure proper serialization.
38 | func (st *SwitchTask) MarshalJSON() ([]byte, error) {
39 | type Alias SwitchTask
40 | return json.Marshal((*Alias)(st))
41 | }
42 |
43 | // UnmarshalJSON for SwitchTask to ensure proper deserialization.
44 | func (st *SwitchTask) UnmarshalJSON(data []byte) error {
45 | type Alias SwitchTask
46 | alias := (*Alias)(st)
47 | return json.Unmarshal(data, alias)
48 | }
49 |
--------------------------------------------------------------------------------
/model/task_switch_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestSwitchTask_MarshalJSON(t *testing.T) {
25 | switchTask := &SwitchTask{
26 | TaskBase: TaskBase{
27 | If: &RuntimeExpression{Value: "${condition}"},
28 | Input: &Input{From: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"key": "value"}}},
29 | Output: &Output{As: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"result": "output"}}},
30 | Timeout: &TimeoutOrReference{Timeout: &Timeout{After: NewDurationExpr("10s")}},
31 | Then: &FlowDirective{Value: "continue"},
32 | Metadata: map[string]interface{}{
33 | "meta": "data",
34 | },
35 | },
36 | Switch: []SwitchItem{
37 | {
38 | "case1": SwitchCase{
39 | When: &RuntimeExpression{Value: "${condition1}"},
40 | Then: &FlowDirective{Value: "next"},
41 | },
42 | },
43 | {
44 | "case2": SwitchCase{
45 | When: &RuntimeExpression{Value: "${condition2}"},
46 | Then: &FlowDirective{Value: "end"},
47 | },
48 | },
49 | },
50 | }
51 |
52 | data, err := json.Marshal(switchTask)
53 | assert.NoError(t, err)
54 | assert.JSONEq(t, `{
55 | "if": "${condition}",
56 | "input": { "from": {"key": "value"} },
57 | "output": { "as": {"result": "output"} },
58 | "timeout": { "after": "10s" },
59 | "then": "continue",
60 | "metadata": {"meta": "data"},
61 | "switch": [
62 | {
63 | "case1": {
64 | "when": "${condition1}",
65 | "then": "next"
66 | }
67 | },
68 | {
69 | "case2": {
70 | "when": "${condition2}",
71 | "then": "end"
72 | }
73 | }
74 | ]
75 | }`, string(data))
76 | }
77 |
78 | func TestSwitchTask_UnmarshalJSON(t *testing.T) {
79 | jsonData := `{
80 | "if": "${condition}",
81 | "input": { "from": {"key": "value"} },
82 | "output": { "as": {"result": "output"} },
83 | "timeout": { "after": "10s" },
84 | "then": "continue",
85 | "metadata": {"meta": "data"},
86 | "switch": [
87 | {
88 | "case1": {
89 | "when": "${condition1}",
90 | "then": "next"
91 | }
92 | },
93 | {
94 | "case2": {
95 | "when": "${condition2}",
96 | "then": "end"
97 | }
98 | }
99 | ]
100 | }`
101 |
102 | var switchTask SwitchTask
103 | err := json.Unmarshal([]byte(jsonData), &switchTask)
104 | assert.NoError(t, err)
105 | assert.Equal(t, &RuntimeExpression{Value: "${condition}"}, switchTask.If)
106 | assert.Equal(t, &Input{From: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"key": "value"}}}, switchTask.Input)
107 | assert.Equal(t, &Output{As: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"result": "output"}}}, switchTask.Output)
108 | assert.Equal(t, &TimeoutOrReference{Timeout: &Timeout{After: NewDurationExpr("10s")}}, switchTask.Timeout)
109 | assert.Equal(t, &FlowDirective{Value: "continue"}, switchTask.Then)
110 | assert.Equal(t, map[string]interface{}{"meta": "data"}, switchTask.Metadata)
111 | assert.Equal(t, 2, len(switchTask.Switch))
112 | assert.Equal(t, &RuntimeExpression{Value: "${condition1}"}, switchTask.Switch[0]["case1"].When)
113 | assert.Equal(t, &FlowDirective{Value: "next"}, switchTask.Switch[0]["case1"].Then)
114 | assert.Equal(t, &RuntimeExpression{Value: "${condition2}"}, switchTask.Switch[1]["case2"].When)
115 | assert.Equal(t, &FlowDirective{Value: "end"}, switchTask.Switch[1]["case2"].Then)
116 | }
117 |
118 | func TestSwitchTask_Validation(t *testing.T) {
119 | // Valid SwitchTask
120 | switchTask := SwitchTask{
121 | TaskBase: TaskBase{},
122 | Switch: []SwitchItem{
123 | {
124 | "case1": SwitchCase{
125 | When: &RuntimeExpression{Value: "${condition1}"},
126 | Then: &FlowDirective{Value: "next"},
127 | },
128 | },
129 | },
130 | }
131 | assert.NoError(t, validate.Struct(switchTask))
132 |
133 | // Invalid SwitchTask (empty switch)
134 | invalidSwitchTask := SwitchTask{
135 | TaskBase: TaskBase{},
136 | Switch: []SwitchItem{},
137 | }
138 | assert.Error(t, validate.Struct(invalidSwitchTask))
139 |
140 | // Invalid SwitchTask (SwitchItem with multiple keys)
141 | invalidSwitchItemTask := SwitchTask{
142 | TaskBase: TaskBase{},
143 | Switch: []SwitchItem{
144 | {
145 | "case1": SwitchCase{When: &RuntimeExpression{Value: "${condition1}"}, Then: &FlowDirective{Value: "next"}},
146 | "case2": SwitchCase{When: &RuntimeExpression{Value: "${condition2}"}, Then: &FlowDirective{Value: "end"}},
147 | },
148 | },
149 | }
150 | assert.Error(t, validate.Struct(invalidSwitchItemTask))
151 | }
152 |
--------------------------------------------------------------------------------
/model/task_try_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestRetryPolicy_MarshalJSON(t *testing.T) {
25 | retryPolicy := RetryPolicy{
26 | When: &RuntimeExpression{"${someCondition}"},
27 | ExceptWhen: &RuntimeExpression{"${someOtherCondition}"},
28 | Delay: NewDurationExpr("PT5S"),
29 | Backoff: &RetryBackoff{
30 | Exponential: &BackoffDefinition{
31 | Definition: map[string]interface{}{"factor": 2},
32 | },
33 | },
34 | Limit: RetryLimit{
35 | Attempt: &RetryLimitAttempt{
36 | Count: 3,
37 | Duration: NewDurationExpr("PT1M"),
38 | },
39 | Duration: NewDurationExpr("PT10M"),
40 | },
41 | Jitter: &RetryPolicyJitter{
42 | From: NewDurationExpr("PT1S"),
43 | To: NewDurationExpr("PT3S"),
44 | },
45 | }
46 |
47 | data, err := json.Marshal(retryPolicy)
48 | assert.NoError(t, err)
49 | assert.JSONEq(t, `{
50 | "when": "${someCondition}",
51 | "exceptWhen": "${someOtherCondition}",
52 | "delay": "PT5S",
53 | "backoff": {"exponential": {"factor": 2}},
54 | "limit": {
55 | "attempt": {"count": 3, "duration": "PT1M"},
56 | "duration": "PT10M"
57 | },
58 | "jitter": {"from": "PT1S", "to": "PT3S"}
59 | }`, string(data))
60 | }
61 |
62 | func TestRetryPolicy_UnmarshalJSON(t *testing.T) {
63 | jsonData := `{
64 | "when": "${someCondition}",
65 | "exceptWhen": "${someOtherCondition}",
66 | "delay": "PT5S",
67 | "backoff": {"exponential": {"factor": 2}},
68 | "limit": {
69 | "attempt": {"count": 3, "duration": "PT1M"},
70 | "duration": "PT10M"
71 | },
72 | "jitter": {"from": "PT1S", "to": "PT3S"}
73 | }`
74 |
75 | var retryPolicy RetryPolicy
76 | err := json.Unmarshal([]byte(jsonData), &retryPolicy)
77 | assert.NoError(t, err)
78 | assert.Equal(t, &RuntimeExpression{"${someCondition}"}, retryPolicy.When)
79 | assert.Equal(t, &RuntimeExpression{"${someOtherCondition}"}, retryPolicy.ExceptWhen)
80 | assert.Equal(t, NewDurationExpr("PT5S"), retryPolicy.Delay)
81 | assert.NotNil(t, retryPolicy.Backoff.Exponential)
82 | assert.Equal(t, map[string]interface{}{"factor": float64(2)}, retryPolicy.Backoff.Exponential.Definition)
83 | assert.Equal(t, 3, retryPolicy.Limit.Attempt.Count)
84 | assert.Equal(t, NewDurationExpr("PT1M"), retryPolicy.Limit.Attempt.Duration)
85 | assert.Equal(t, NewDurationExpr("PT10M"), retryPolicy.Limit.Duration)
86 | assert.Equal(t, NewDurationExpr("PT1S"), retryPolicy.Jitter.From)
87 | assert.Equal(t, NewDurationExpr("PT3S"), retryPolicy.Jitter.To)
88 | }
89 |
90 | func TestRetryPolicy_Validation(t *testing.T) {
91 | // Valid RetryPolicy
92 | retryPolicy := RetryPolicy{
93 | When: &RuntimeExpression{"${someCondition}"},
94 | ExceptWhen: &RuntimeExpression{"${someOtherCondition}"},
95 | Delay: NewDurationExpr("PT5S"),
96 | Backoff: &RetryBackoff{
97 | Constant: &BackoffDefinition{
98 | Definition: map[string]interface{}{"delay": 5},
99 | },
100 | },
101 | Limit: RetryLimit{
102 | Attempt: &RetryLimitAttempt{
103 | Count: 3,
104 | Duration: NewDurationExpr("PT1M"),
105 | },
106 | Duration: NewDurationExpr("PT10M"),
107 | },
108 | Jitter: &RetryPolicyJitter{
109 | From: NewDurationExpr("PT1S"),
110 | To: NewDurationExpr("PT3S"),
111 | },
112 | }
113 | assert.NoError(t, validate.Struct(retryPolicy))
114 |
115 | // Invalid RetryPolicy (missing required fields in Jitter)
116 | invalidRetryPolicy := RetryPolicy{
117 | Jitter: &RetryPolicyJitter{
118 | From: NewDurationExpr("PT1S"),
119 | },
120 | }
121 | assert.Error(t, validate.Struct(invalidRetryPolicy))
122 | }
123 |
124 | func TestRetryPolicy_UnmarshalJSON_WithReference(t *testing.T) {
125 | retries := map[string]*RetryPolicy{
126 | "default": {
127 | Delay: &Duration{DurationInline{Seconds: 3}},
128 | Backoff: &RetryBackoff{
129 | Exponential: &BackoffDefinition{},
130 | },
131 | Limit: RetryLimit{
132 | Attempt: &RetryLimitAttempt{Count: 5},
133 | },
134 | },
135 | }
136 |
137 | jsonData := `{
138 | "retry": "default"
139 | }`
140 |
141 | var task TryTaskCatch
142 | err := json.Unmarshal([]byte(jsonData), &task)
143 | assert.NoError(t, err)
144 |
145 | // Resolve the reference
146 | err = task.Retry.ResolveReference(retries)
147 | assert.NoError(t, err)
148 |
149 | assert.Equal(t, retries["default"].Delay, task.Retry.Delay)
150 | assert.Equal(t, retries["default"].Backoff, task.Retry.Backoff)
151 | assert.Equal(t, retries["default"].Limit, task.Retry.Limit)
152 | }
153 |
154 | func TestRetryPolicy_UnmarshalJSON_Inline(t *testing.T) {
155 | jsonData := `{
156 | "retry": {
157 | "delay": { "seconds": 3 },
158 | "backoff": { "exponential": {} },
159 | "limit": { "attempt": { "count": 5 } }
160 | }
161 | }`
162 |
163 | var task TryTaskCatch
164 | err := json.Unmarshal([]byte(jsonData), &task)
165 | assert.NoError(t, err)
166 |
167 | assert.NotNil(t, task.Retry)
168 | assert.Equal(t, int32(3), task.Retry.Delay.AsInline().Seconds)
169 | assert.NotNil(t, task.Retry.Backoff.Exponential)
170 | assert.Equal(t, 5, task.Retry.Limit.Attempt.Count)
171 | }
172 |
--------------------------------------------------------------------------------
/model/task_wait.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | )
21 |
22 | // WaitTask represents a task configuration to delay execution for a specified duration.
23 | type WaitTask struct {
24 | TaskBase `json:",inline"`
25 | Wait *Duration `json:"wait" validate:"required"`
26 | }
27 |
28 | func (wt *WaitTask) GetBase() *TaskBase {
29 | return &wt.TaskBase
30 | }
31 |
32 | // MarshalJSON for WaitTask to ensure proper serialization.
33 | func (wt *WaitTask) MarshalJSON() ([]byte, error) {
34 | type Alias WaitTask
35 | waitData, err := json.Marshal(wt.Wait)
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | alias := struct {
41 | Alias
42 | Wait json.RawMessage `json:"wait"`
43 | }{
44 | Alias: (Alias)(*wt),
45 | Wait: waitData,
46 | }
47 |
48 | return json.Marshal(alias)
49 | }
50 |
51 | // UnmarshalJSON for WaitTask to ensure proper deserialization.
52 | func (wt *WaitTask) UnmarshalJSON(data []byte) error {
53 | type Alias WaitTask
54 | alias := struct {
55 | *Alias
56 | Wait json.RawMessage `json:"wait"`
57 | }{
58 | Alias: (*Alias)(wt),
59 | }
60 |
61 | // Unmarshal data into alias
62 | if err := json.Unmarshal(data, &alias); err != nil {
63 | return fmt.Errorf("failed to unmarshal WaitTask: %w", err)
64 | }
65 |
66 | // Unmarshal Wait field
67 | if err := json.Unmarshal(alias.Wait, &wt.Wait); err != nil {
68 | return fmt.Errorf("failed to unmarshal Wait field: %w", err)
69 | }
70 |
71 | return nil
72 | }
73 |
--------------------------------------------------------------------------------
/model/task_wait_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestWaitTask_MarshalJSON(t *testing.T) {
25 | waitTask := &WaitTask{
26 | TaskBase: TaskBase{
27 | If: &RuntimeExpression{Value: "${condition}"},
28 | Input: &Input{From: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"key": "value"}}},
29 | Output: &Output{As: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"result": "output"}}},
30 | Timeout: &TimeoutOrReference{Timeout: &Timeout{After: NewDurationExpr("10s")}},
31 | Then: &FlowDirective{Value: "continue"},
32 | Metadata: map[string]interface{}{
33 | "meta": "data",
34 | },
35 | },
36 | Wait: NewDurationExpr("P1DT1H"),
37 | }
38 |
39 | data, err := json.Marshal(waitTask)
40 | assert.NoError(t, err)
41 | assert.JSONEq(t, `{
42 | "if": "${condition}",
43 | "input": { "from": {"key": "value"} },
44 | "output": { "as": {"result": "output"} },
45 | "timeout": { "after": "10s" },
46 | "then": "continue",
47 | "metadata": {"meta": "data"},
48 | "wait": "P1DT1H"
49 | }`, string(data))
50 | }
51 |
52 | func TestWaitTask_UnmarshalJSON(t *testing.T) {
53 | jsonData := `{
54 | "if": "${condition}",
55 | "input": { "from": {"key": "value"} },
56 | "output": { "as": {"result": "output"} },
57 | "timeout": { "after": "10s" },
58 | "then": "continue",
59 | "metadata": {"meta": "data"},
60 | "wait": "P1DT1H"
61 | }`
62 |
63 | waitTask := &WaitTask{}
64 | err := json.Unmarshal([]byte(jsonData), waitTask)
65 | assert.NoError(t, err)
66 | assert.Equal(t, &RuntimeExpression{Value: "${condition}"}, waitTask.If)
67 | assert.Equal(t, &Input{From: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"key": "value"}}}, waitTask.Input)
68 | assert.Equal(t, &Output{As: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"result": "output"}}}, waitTask.Output)
69 | assert.Equal(t, &TimeoutOrReference{Timeout: &Timeout{After: NewDurationExpr("10s")}}, waitTask.Timeout)
70 | assert.Equal(t, &FlowDirective{Value: "continue"}, waitTask.Then)
71 | assert.Equal(t, map[string]interface{}{"meta": "data"}, waitTask.Metadata)
72 | assert.Equal(t, NewDurationExpr("P1DT1H"), waitTask.Wait)
73 | }
74 |
75 | func TestWaitTask_Validation(t *testing.T) {
76 | // Valid WaitTask
77 | waitTask := &WaitTask{
78 | TaskBase: TaskBase{},
79 | Wait: NewDurationExpr("P1DT1H"),
80 | }
81 | assert.NoError(t, validate.Struct(waitTask))
82 |
83 | // Invalid WaitTask (empty wait)
84 | invalidWaitTask := &WaitTask{
85 | TaskBase: TaskBase{},
86 | }
87 | assert.Error(t, validate.Struct(invalidWaitTask))
88 | }
89 |
--------------------------------------------------------------------------------
/model/timeout_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "encoding/json"
19 | "testing"
20 |
21 | "github.com/stretchr/testify/assert"
22 | )
23 |
24 | func TestTimeout_UnmarshalJSON(t *testing.T) {
25 | // Test cases for Timeout unmarshalling
26 | tests := []struct {
27 | name string
28 | jsonStr string
29 | expect *Timeout
30 | err bool
31 | }{
32 | {
33 | name: "Valid inline duration",
34 | jsonStr: `{"after": {"days": 1, "hours": 2}}`,
35 | expect: &Timeout{
36 | After: &Duration{DurationInline{
37 | Days: 1,
38 | Hours: 2,
39 | }},
40 | },
41 | err: false,
42 | },
43 | {
44 | name: "Valid ISO 8601 duration",
45 | jsonStr: `{"after": "P1Y2M3DT4H5M6S"}`,
46 | expect: &Timeout{
47 | After: NewDurationExpr("P1Y2M3DT4H5M6S"),
48 | },
49 | err: false,
50 | },
51 | {
52 | name: "Invalid duration type",
53 | jsonStr: `{"after": {"unknown": "value"}}`,
54 | expect: nil,
55 | err: true,
56 | },
57 | {
58 | name: "Missing after key",
59 | jsonStr: `{}`,
60 | expect: nil,
61 | err: true,
62 | },
63 | }
64 |
65 | for _, test := range tests {
66 | t.Run(test.name, func(t *testing.T) {
67 | var timeout Timeout
68 | err := json.Unmarshal([]byte(test.jsonStr), &timeout)
69 | if test.err {
70 | assert.Error(t, err)
71 | } else {
72 | assert.NoError(t, err)
73 | assert.Equal(t, test.expect, &timeout)
74 | }
75 | })
76 | }
77 | }
78 |
79 | func TestTimeout_MarshalJSON(t *testing.T) {
80 | tests := []struct {
81 | name string
82 | input *Timeout
83 | expected string
84 | wantErr bool
85 | }{
86 | {
87 | name: "ISO 8601 Duration",
88 | input: &Timeout{
89 | After: &Duration{
90 | Value: DurationExpression{Expression: "PT1H"},
91 | },
92 | },
93 | expected: `{"after":"PT1H"}`,
94 | wantErr: false,
95 | },
96 | {
97 | name: "Inline Duration",
98 | input: &Timeout{
99 | After: &Duration{
100 | Value: DurationInline{
101 | Days: 1,
102 | Hours: 2,
103 | Minutes: 30,
104 | },
105 | },
106 | },
107 | expected: `{"after":{"days":1,"hours":2,"minutes":30}}`,
108 | wantErr: false,
109 | },
110 | {
111 | name: "Invalid Duration",
112 | input: &Timeout{After: &Duration{Value: 123}},
113 | expected: "",
114 | wantErr: true,
115 | },
116 | }
117 |
118 | for _, tt := range tests {
119 | t.Run(tt.name, func(t *testing.T) {
120 | data, err := json.Marshal(tt.input)
121 | if tt.wantErr {
122 | assert.Error(t, err)
123 | } else {
124 | assert.NoError(t, err)
125 | assert.JSONEq(t, tt.expected, string(data))
126 | }
127 | })
128 | }
129 | }
130 |
131 | func TestTimeoutOrReference_UnmarshalJSON(t *testing.T) {
132 | // Test cases for TimeoutOrReference unmarshalling
133 | tests := []struct {
134 | name string
135 | jsonStr string
136 | expect *TimeoutOrReference
137 | err bool
138 | }{
139 | {
140 | name: "Valid Timeout",
141 | jsonStr: `{"after": {"days": 1, "hours": 2}}`,
142 | expect: &TimeoutOrReference{
143 | Timeout: &Timeout{
144 | After: &Duration{DurationInline{
145 | Days: 1,
146 | Hours: 2,
147 | }},
148 | },
149 | },
150 | err: false,
151 | },
152 | {
153 | name: "Valid Ref",
154 | jsonStr: `"some-timeout-reference"`,
155 | expect: &TimeoutOrReference{
156 | Reference: ptrString("some-timeout-reference"),
157 | },
158 | err: false,
159 | },
160 | {
161 | name: "Invalid JSON",
162 | jsonStr: `{"invalid": }`,
163 | expect: nil,
164 | err: true,
165 | },
166 | }
167 |
168 | for _, test := range tests {
169 | t.Run(test.name, func(t *testing.T) {
170 | var tor TimeoutOrReference
171 | err := json.Unmarshal([]byte(test.jsonStr), &tor)
172 | if test.err {
173 | assert.Error(t, err)
174 | } else {
175 | assert.NoError(t, err)
176 | assert.Equal(t, test.expect, &tor)
177 | }
178 | })
179 | }
180 | }
181 |
182 | func ptrString(s string) *string {
183 | return &s
184 | }
185 |
186 | func TestTimeoutOrReference_MarshalJSON(t *testing.T) {
187 | // Test cases for TimeoutOrReference marshalling
188 | tests := []struct {
189 | name string
190 | input *TimeoutOrReference
191 | expect string
192 | err bool
193 | }{
194 | {
195 | name: "Valid Timeout",
196 | input: &TimeoutOrReference{
197 | Timeout: &Timeout{
198 | After: &Duration{DurationInline{
199 | Days: 1,
200 | Hours: 2,
201 | }},
202 | },
203 | },
204 | expect: `{"after":{"days":1,"hours":2}}`,
205 | err: false,
206 | },
207 | {
208 | name: "Valid Ref",
209 | input: &TimeoutOrReference{
210 | Reference: ptrString("some-timeout-reference"),
211 | },
212 | expect: `"some-timeout-reference"`,
213 | err: false,
214 | },
215 | }
216 |
217 | for _, test := range tests {
218 | t.Run(test.name, func(t *testing.T) {
219 | data, err := json.Marshal(test.input)
220 | if test.err {
221 | assert.Error(t, err)
222 | } else {
223 | assert.NoError(t, err)
224 | assert.JSONEq(t, test.expect, string(data))
225 | }
226 | })
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/model/validator_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package model
16 |
17 | import (
18 | "testing"
19 | )
20 |
21 | func TestRegexValidators(t *testing.T) {
22 | testCases := []struct {
23 | name string
24 | validate func(string) bool
25 | input string
26 | expected bool
27 | }{
28 | // ISO 8601 Duration Tests
29 | {"ISO 8601 Duration Valid 1", isISO8601DurationValid, "P2Y", true},
30 | {"ISO 8601 Duration Valid 2", isISO8601DurationValid, "P1DT12H30M", true},
31 | {"ISO 8601 Duration Valid 3", isISO8601DurationValid, "P1Y2M3D", true},
32 | {"ISO 8601 Duration Valid 4", isISO8601DurationValid, "P1Y2M3D4H", false},
33 | {"ISO 8601 Duration Valid 5", isISO8601DurationValid, "P1Y", true},
34 | {"ISO 8601 Duration Valid 6", isISO8601DurationValid, "PT1H", true},
35 | {"ISO 8601 Duration Valid 7", isISO8601DurationValid, "P1Y2M3D4H5M6S", false},
36 | {"ISO 8601 Duration Invalid 1", isISO8601DurationValid, "P", false},
37 | {"ISO 8601 Duration Invalid 2", isISO8601DurationValid, "P1Y2M3D4H5M6S7", false},
38 | {"ISO 8601 Duration Invalid 3", isISO8601DurationValid, "1Y", false},
39 |
40 | // Semantic Versioning Tests
41 | {"Semantic Version Valid 1", isSemanticVersionValid, "1.0.0", true},
42 | {"Semantic Version Valid 2", isSemanticVersionValid, "1.2.3", true},
43 | {"Semantic Version Valid 3", isSemanticVersionValid, "1.2.3-beta", true},
44 | {"Semantic Version Valid 4", isSemanticVersionValid, "1.2.3-beta.1", true},
45 | {"Semantic Version Valid 5", isSemanticVersionValid, "1.2.3-beta.1+build.123", true},
46 | {"Semantic Version Invalid 1", isSemanticVersionValid, "v1.2.3", false},
47 | {"Semantic Version Invalid 2", isSemanticVersionValid, "1.2", false},
48 | {"Semantic Version Invalid 3", isSemanticVersionValid, "1.2.3-beta.x", true},
49 |
50 | // RFC 1123 Hostname Tests
51 | {"RFC 1123 Hostname Valid 1", isHostnameValid, "example.com", true},
52 | {"RFC 1123 Hostname Valid 2", isHostnameValid, "my-hostname", true},
53 | {"RFC 1123 Hostname Valid 3", isHostnameValid, "subdomain.example.com", true},
54 | {"RFC 1123 Hostname Invalid 1", isHostnameValid, "127.0.0.1", false},
55 | {"RFC 1123 Hostname Invalid 2", isHostnameValid, "example.com.", false},
56 | {"RFC 1123 Hostname Invalid 3", isHostnameValid, "example..com", false},
57 | {"RFC 1123 Hostname Invalid 4", isHostnameValid, "example.com-", false},
58 | }
59 |
60 | for _, tc := range testCases {
61 | t.Run(tc.name, func(t *testing.T) {
62 | result := tc.validate(tc.input)
63 | if result != tc.expected {
64 | t.Errorf("Validation failed for '%s': input='%s', expected=%v, got=%v", tc.name, tc.input, tc.expected, result)
65 | }
66 | })
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/parser/cmd/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "fmt"
19 | "os"
20 | "path/filepath"
21 |
22 | "github.com/serverlessworkflow/sdk-go/v3/parser"
23 | )
24 |
25 | func main() {
26 | if len(os.Args) < 2 {
27 | fmt.Println("Usage: go run main.go ")
28 | os.Exit(1)
29 | }
30 |
31 | baseDir := os.Args[1]
32 | supportedExt := []string{".json", ".yaml", ".yml"}
33 | errCount := 0
34 |
35 | err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
36 | if err != nil {
37 | return err
38 | }
39 | if !info.IsDir() {
40 | for _, ext := range supportedExt {
41 | if filepath.Ext(path) == ext {
42 | fmt.Printf("Validating: %s\n", path)
43 | _, err := parser.FromFile(path)
44 | if err != nil {
45 | fmt.Printf("Validation failed for %s: %v\n", path, err)
46 | errCount++
47 | } else {
48 | fmt.Printf("Validation succeeded for %s\n", path)
49 | }
50 | break
51 | }
52 | }
53 | }
54 | return nil
55 | })
56 |
57 | if err != nil {
58 | fmt.Printf("Error walking the path %s: %v\n", baseDir, err)
59 | os.Exit(1)
60 | }
61 |
62 | if errCount > 0 {
63 | fmt.Printf("Validation failed for %d file(s).\n", errCount)
64 | os.Exit(1)
65 | }
66 |
67 | fmt.Println("All workflows validated successfully.")
68 | }
69 |
--------------------------------------------------------------------------------
/parser/parser.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package parser
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | "os"
21 | "path/filepath"
22 | "strings"
23 |
24 | "github.com/serverlessworkflow/sdk-go/v3/model"
25 |
26 | "sigs.k8s.io/yaml"
27 | )
28 |
29 | const (
30 | extJSON = ".json"
31 | extYAML = ".yaml"
32 | extYML = ".yml"
33 | )
34 |
35 | var supportedExt = []string{extYAML, extYML, extJSON}
36 |
37 | // FromYAMLSource parses the given Serverless Workflow YAML source into the Workflow type.
38 | func FromYAMLSource(source []byte) (workflow *model.Workflow, err error) {
39 | var jsonBytes []byte
40 | if jsonBytes, err = yaml.YAMLToJSON(source); err != nil {
41 | return nil, err
42 | }
43 | return FromJSONSource(jsonBytes)
44 | }
45 |
46 | // FromJSONSource parses the given Serverless Workflow JSON source into the Workflow type.
47 | func FromJSONSource(source []byte) (workflow *model.Workflow, err error) {
48 | workflow = &model.Workflow{}
49 | if err := json.Unmarshal(source, workflow); err != nil {
50 | return nil, err
51 | }
52 |
53 | err = model.GetValidator().Struct(workflow)
54 | if err != nil {
55 | return nil, err
56 | }
57 | return workflow, nil
58 | }
59 |
60 | // FromFile parses the given Serverless Workflow file into the Workflow type.
61 | func FromFile(path string) (*model.Workflow, error) {
62 | if err := checkFilePath(path); err != nil {
63 | return nil, err
64 | }
65 | fileBytes, err := os.ReadFile(filepath.Clean(path))
66 | if err != nil {
67 | return nil, err
68 | }
69 | if strings.HasSuffix(path, extYAML) || strings.HasSuffix(path, extYML) {
70 | return FromYAMLSource(fileBytes)
71 | }
72 | return FromJSONSource(fileBytes)
73 | }
74 |
75 | // checkFilePath verifies if the file exists in the given path and if it's supported by the parser package
76 | func checkFilePath(path string) error {
77 | info, err := os.Stat(path)
78 | if err != nil {
79 | return err
80 | }
81 | if info.IsDir() {
82 | return fmt.Errorf("file path '%s' must stand to a file", path)
83 | }
84 | for _, ext := range supportedExt {
85 | if strings.HasSuffix(path, ext) {
86 | return nil
87 | }
88 | }
89 | return fmt.Errorf("file extension not supported for '%s'. supported formats are %s", path, supportedExt)
90 | }
91 |
--------------------------------------------------------------------------------
/parser/parser_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package parser
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/stretchr/testify/assert"
21 | )
22 |
23 | func TestFromYAMLSource(t *testing.T) {
24 | source := []byte(`
25 | document:
26 | dsl: 1.0.0
27 | namespace: examples
28 | name: example-workflow
29 | version: 1.0.0
30 | do:
31 | - task1:
32 | call: http
33 | with:
34 | method: GET
35 | endpoint: http://example.com
36 | `)
37 | workflow, err := FromYAMLSource(source)
38 | assert.NoError(t, err)
39 | assert.NotNil(t, workflow)
40 | assert.Equal(t, "example-workflow", workflow.Document.Name)
41 | }
42 |
43 | func TestFromJSONSource(t *testing.T) {
44 | source := []byte(`{
45 | "document": {
46 | "dsl": "1.0.0",
47 | "namespace": "examples",
48 | "name": "example-workflow",
49 | "version": "1.0.0"
50 | },
51 | "do": [
52 | {
53 | "task1": {
54 | "call": "http",
55 | "with": {
56 | "method": "GET",
57 | "endpoint": "http://example.com"
58 | }
59 | }
60 | }
61 | ]
62 | }`)
63 | workflow, err := FromJSONSource(source)
64 | assert.NoError(t, err)
65 | assert.NotNil(t, workflow)
66 | assert.Equal(t, "example-workflow", workflow.Document.Name)
67 | }
68 |
69 | func TestFromFile(t *testing.T) {
70 | tests := []struct {
71 | name string
72 | filePath string
73 | expectError bool
74 | }{
75 | {
76 | name: "Valid YAML File",
77 | filePath: "testdata/valid_workflow.yaml",
78 | expectError: false,
79 | },
80 | {
81 | name: "Invalid YAML File",
82 | filePath: "testdata/invalid_workflow.yaml",
83 | expectError: true,
84 | },
85 | {
86 | name: "Unsupported File Extension",
87 | filePath: "testdata/unsupported_workflow.txt",
88 | expectError: true,
89 | },
90 | {
91 | name: "Non-existent File",
92 | filePath: "testdata/nonexistent_workflow.yaml",
93 | expectError: true,
94 | },
95 | }
96 |
97 | for _, tt := range tests {
98 | t.Run(tt.name, func(t *testing.T) {
99 | workflow, err := FromFile(tt.filePath)
100 | if tt.expectError {
101 | assert.Error(t, err)
102 | assert.Nil(t, workflow)
103 | } else {
104 | assert.NoError(t, err)
105 | assert.NotNil(t, workflow)
106 | assert.Equal(t, "example-workflow", workflow.Document.Name)
107 | }
108 | })
109 | }
110 | }
111 |
112 | func TestCheckFilePath(t *testing.T) {
113 | tests := []struct {
114 | name string
115 | filePath string
116 | expectError bool
117 | }{
118 | {
119 | name: "Valid YAML File Path",
120 | filePath: "testdata/valid_workflow.yaml",
121 | expectError: false,
122 | },
123 | {
124 | name: "Unsupported File Extension",
125 | filePath: "testdata/unsupported_workflow.txt",
126 | expectError: true,
127 | },
128 | {
129 | name: "Directory Path",
130 | filePath: "testdata",
131 | expectError: true,
132 | },
133 | }
134 |
135 | for _, tt := range tests {
136 | t.Run(tt.name, func(t *testing.T) {
137 | err := checkFilePath(tt.filePath)
138 | if tt.expectError {
139 | assert.Error(t, err)
140 | } else {
141 | assert.NoError(t, err)
142 | }
143 | })
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/parser/testdata/invalid_workflow.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: 1.0.0
17 | namespace: examples
18 | name: example-workflow
19 | version: 1.0.0
20 | do:
21 | - task1:
22 | call: http
23 | with:
24 | method: GET
25 | # Missing "endpoint" field, making it invalid
--------------------------------------------------------------------------------
/parser/testdata/valid_workflow.json:
--------------------------------------------------------------------------------
1 | {
2 | "document": {
3 | "dsl": "1.0.0",
4 | "namespace": "examples",
5 | "name": "example-workflow",
6 | "version": "1.0.0"
7 | },
8 | "do": [
9 | {
10 | "task1": {
11 | "call": "http",
12 | "with": {
13 | "method": "GET",
14 | "endpoint": "http://example.com"
15 | }
16 | }
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/parser/testdata/valid_workflow.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2025 The Serverless Workflow Specification Authors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | document:
16 | dsl: 1.0.0
17 | namespace: examples
18 | name: example-workflow
19 | version: 1.0.0
20 | do:
21 | - task1:
22 | call: http
23 | with:
24 | method: GET
25 | endpoint: http://example.com
--------------------------------------------------------------------------------
/test/utils.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 The Serverless Workflow Specification Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package test
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/stretchr/testify/assert"
21 | "sigs.k8s.io/yaml"
22 | )
23 |
24 | func AssertYAMLEq(t *testing.T, expected, actual string) {
25 | var expectedMap, actualMap map[string]interface{}
26 |
27 | // Unmarshal the expected YAML
28 | err := yaml.Unmarshal([]byte(expected), &expectedMap)
29 | assert.NoError(t, err, "failed to unmarshal expected YAML")
30 |
31 | // Unmarshal the actual YAML
32 | err = yaml.Unmarshal([]byte(actual), &actualMap)
33 | assert.NoError(t, err, "failed to unmarshal actual YAML")
34 |
35 | // Assert equality of the two maps
36 | assert.Equal(t, expectedMap, actualMap, "YAML structures do not match")
37 | }
38 |
--------------------------------------------------------------------------------
/tools.mod:
--------------------------------------------------------------------------------
1 | module github.com/serverlessworkflow/sdk-go/v3
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/google/addlicense v0.0.0-20210428195630-6d92264d7170 // indirect
7 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
8 | )
9 |
--------------------------------------------------------------------------------