├── .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') 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 | --------------------------------------------------------------------------------