From 26f0b2169d49a1ec16670a1789cafafa29158806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20R=2E=20de=20Miranda?= Date: Fri, 10 Mar 2023 00:29:45 -0300 Subject: [PATCH] Move validator to *_validator.go. Changes with validator "transition" and "end" validator: add to BaseState; fix failed unit tests; refactor SwitchState validator. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André R. de Miranda --- model/callback_state_test.go | 3 + model/delay_state_test.go | 3 + model/event.go | 17 - model/event_test.go | 46 --- model/event_validator.go | 34 ++ model/event_validator_test.go | 66 ++++ model/foreach_state.go | 40 --- model/foreach_state_test.go | 148 -------- model/foreach_state_validator.go | 59 ++++ model/foreach_state_validator_test.go | 183 ++++++++++ model/parallel_state.go | 39 --- model/parallel_state_test.go | 134 ------- model/parallel_state_validator.go | 55 +++ model/parallel_state_validator_test.go | 170 +++++++++ model/retry.go | 23 -- model/retry_test.go | 106 ------ model/retry_validator.go | 39 +++ model/retry_validator_test.go | 120 +++++++ model/sleep_state_test.go | 3 + model/states.go | 21 -- model/states_validator.go | 33 ++ ...tates_test.go => states_validator_test.go} | 33 +- model/switch_state.go | 57 --- model/switch_state_test.go | 303 ---------------- model/switch_state_validator.go | 59 ++++ model/switch_state_validator_test.go | 329 ++++++++++++++++++ model/util_validator.go | 30 ++ model/workflow.go | 51 --- model/workflow_test.go | 165 --------- model/workflow_validator.go | 98 ++++++ model/workflow_validator_test.go | 237 +++++++++++++ parser/parser_test.go | 24 +- .../workflows/greetings-v08-spec.sw.yaml | 6 +- .../workflows/greetings_sleep.sw.json | 3 +- 34 files changed, 1572 insertions(+), 1165 deletions(-) create mode 100644 model/event_validator.go create mode 100644 model/event_validator_test.go create mode 100644 model/foreach_state_validator.go create mode 100644 model/foreach_state_validator_test.go create mode 100644 model/parallel_state_validator.go create mode 100644 model/parallel_state_validator_test.go create mode 100644 model/retry_validator.go create mode 100644 model/retry_validator_test.go create mode 100644 model/states_validator.go rename model/{states_test.go => states_validator_test.go} (73%) create mode 100644 model/switch_state_validator.go create mode 100644 model/switch_state_validator_test.go create mode 100644 model/util_validator.go create mode 100644 model/workflow_validator.go create mode 100644 model/workflow_validator_test.go diff --git a/model/callback_state_test.go b/model/callback_state_test.go index 9566d86..9e3e856 100644 --- a/model/callback_state_test.go +++ b/model/callback_state_test.go @@ -35,6 +35,9 @@ func TestCallbackStateStructLevelValidation(t *testing.T) { BaseState: BaseState{ Name: "callbackTest", Type: StateTypeCallback, + End: &End{ + Terminate: true, + }, }, CallbackState: &CallbackState{ Action: Action{ diff --git a/model/delay_state_test.go b/model/delay_state_test.go index 5521e03..79f49e5 100644 --- a/model/delay_state_test.go +++ b/model/delay_state_test.go @@ -35,6 +35,9 @@ func TestDelayStateStructLevelValidation(t *testing.T) { BaseState: BaseState{ Name: "1", Type: "delay", + End: &End{ + Terminate: true, + }, }, DelayState: &DelayState{ TimeDelay: "PT5S", diff --git a/model/event.go b/model/event.go index 98d3f59..8aac9ae 100644 --- a/model/event.go +++ b/model/event.go @@ -16,11 +16,6 @@ package model import ( "encoding/json" - "reflect" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" - - validator "github.com/go-playground/validator/v10" ) // EventKind defines this event as either `consumed` or `produced` @@ -34,18 +29,6 @@ const ( EventKindProduced EventKind = "produced" ) -func init() { - val.GetValidator().RegisterStructValidation(EventStructLevelValidation, Event{}) -} - -// EventStructLevelValidation custom validator for event kind consumed -func EventStructLevelValidation(structLevel validator.StructLevel) { - event := structLevel.Current().Interface().(Event) - if event.Kind == EventKindConsumed && len(event.Type) == 0 { - structLevel.ReportError(reflect.ValueOf(event.Type), "Type", "type", "reqtypeconsumed", "") - } -} - // Event used to define events and their correlations type Event struct { Common `json:",inline"` diff --git a/model/event_test.go b/model/event_test.go index bb34e08..8f1665b 100644 --- a/model/event_test.go +++ b/model/event_test.go @@ -19,54 +19,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" ) -func TestEventRefStructLevelValidation(t *testing.T) { - type testCase struct { - name string - eventRef EventRef - err string - } - - testCases := []testCase{ - { - name: "valid resultEventTimeout", - eventRef: EventRef{ - TriggerEventRef: "example valid", - ResultEventRef: "example valid", - ResultEventTimeout: "PT1H", - Invoke: InvokeKindSync, - }, - err: ``, - }, - { - name: "invalid resultEventTimeout", - eventRef: EventRef{ - TriggerEventRef: "example invalid", - ResultEventRef: "example invalid red", - ResultEventTimeout: "10hs", - Invoke: InvokeKindSync, - }, - err: `Key: 'EventRef.ResultEventTimeout' Error:Field validation for 'ResultEventTimeout' failed on the 'iso8601duration' tag`, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := val.GetValidator().Struct(tc.eventRef) - - if tc.err != "" { - assert.Error(t, err) - assert.Regexp(t, tc.err, err) - return - } - assert.NoError(t, err) - }) - } -} - func TestEventRefUnmarshalJSON(t *testing.T) { type testCase struct { desp string diff --git a/model/event_validator.go b/model/event_validator.go new file mode 100644 index 0000000..8d134af --- /dev/null +++ b/model/event_validator.go @@ -0,0 +1,34 @@ +// Copyright 2021 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "reflect" + + validator "github.com/go-playground/validator/v10" + val "github.com/serverlessworkflow/sdk-go/v2/validator" +) + +func init() { + val.GetValidator().RegisterStructValidation(eventStructLevelValidation, Event{}) +} + +// eventStructLevelValidation custom validator for event kind consumed +func eventStructLevelValidation(structLevel validator.StructLevel) { + event := structLevel.Current().Interface().(Event) + if event.Kind == EventKindConsumed && len(event.Type) == 0 { + structLevel.ReportError(reflect.ValueOf(event.Type), "Type", "type", "reqtypeconsumed", "") + } +} diff --git a/model/event_validator_test.go b/model/event_validator_test.go new file mode 100644 index 0000000..90caa9c --- /dev/null +++ b/model/event_validator_test.go @@ -0,0 +1,66 @@ +// Copyright 2021 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "testing" + + val "github.com/serverlessworkflow/sdk-go/v2/validator" + "github.com/stretchr/testify/assert" +) + +func TestEventRefStructLevelValidation(t *testing.T) { + type testCase struct { + name string + eventRef EventRef + err string + } + + testCases := []testCase{ + { + name: "valid resultEventTimeout", + eventRef: EventRef{ + TriggerEventRef: "example valid", + ResultEventRef: "example valid", + ResultEventTimeout: "PT1H", + Invoke: InvokeKindSync, + }, + err: ``, + }, + { + name: "invalid resultEventTimeout", + eventRef: EventRef{ + TriggerEventRef: "example invalid", + ResultEventRef: "example invalid red", + ResultEventTimeout: "10hs", + Invoke: InvokeKindSync, + }, + err: `Key: 'EventRef.ResultEventTimeout' Error:Field validation for 'ResultEventTimeout' failed on the 'iso8601duration' tag`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := val.GetValidator().Struct(tc.eventRef) + + if tc.err != "" { + assert.Error(t, err) + assert.Regexp(t, tc.err, err) + return + } + assert.NoError(t, err) + }) + } +} diff --git a/model/foreach_state.go b/model/foreach_state.go index b3ef13e..099c989 100644 --- a/model/foreach_state.go +++ b/model/foreach_state.go @@ -15,22 +15,12 @@ package model import ( - "context" "encoding/json" "fmt" - "reflect" - "strconv" - validator "github.com/go-playground/validator/v10" "k8s.io/apimachinery/pkg/util/intstr" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" ) -func init() { - val.GetValidator().RegisterStructValidationCtx(ForEachStateStructLevelValidation, ForEachState{}) -} - // ForEachModeType Specifies how iterations are to be performed (sequentially or in parallel) type ForEachModeType string @@ -86,36 +76,6 @@ func (f *ForEachState) UnmarshalJSON(data []byte) error { return nil } -// ForEachStateStructLevelValidation custom validator for ForEachState -func ForEachStateStructLevelValidation(_ context.Context, structLevel validator.StructLevel) { - stateObj := structLevel.Current().Interface().(ForEachState) - - if stateObj.Mode != ForEachModeTypeParallel { - return - } - - if stateObj.BatchSize == nil { - return - } - - switch stateObj.BatchSize.Type { - case intstr.Int: - if stateObj.BatchSize.IntVal <= 0 { - structLevel.ReportError(reflect.ValueOf(stateObj.BatchSize), "BatchSize", "batchSize", "gt0", "") - } - case intstr.String: - v, err := strconv.Atoi(stateObj.BatchSize.StrVal) - if err != nil { - structLevel.ReportError(reflect.ValueOf(stateObj.BatchSize), "BatchSize", "batchSize", "gt0", err.Error()) - return - } - - if v <= 0 { - structLevel.ReportError(reflect.ValueOf(stateObj.BatchSize), "BatchSize", "batchSize", "gt0", "") - } - } -} - // ForEachStateTimeout defines timeout settings for foreach state type ForEachStateTimeout struct { StateExecTimeout *StateExecTimeout `json:"stateExecTimeout,omitempty"` diff --git a/model/foreach_state_test.go b/model/foreach_state_test.go index 3dcb3f8..3456935 100644 --- a/model/foreach_state_test.go +++ b/model/foreach_state_test.go @@ -19,9 +19,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/util/intstr" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" ) func TestForEachStateUnmarshalJSON(t *testing.T) { @@ -71,148 +68,3 @@ func TestForEachStateUnmarshalJSON(t *testing.T) { }) } } - -func TestForEachStateStructLevelValidation(t *testing.T) { - type testCase struct { - desp string - state State - err string - } - testCases := []testCase{ - { - desp: "normal test & sequential", - state: State{ - BaseState: BaseState{ - Name: "1", - Type: "2", - }, - ForEachState: &ForEachState{ - InputCollection: "3", - Actions: []Action{ - {}, - }, - Mode: ForEachModeTypeSequential, - }, - }, - err: ``, - }, - { - desp: "normal test & parallel int", - state: State{ - BaseState: BaseState{ - Name: "1", - Type: "2", - }, - ForEachState: &ForEachState{ - InputCollection: "3", - Actions: []Action{ - {}, - }, - Mode: ForEachModeTypeParallel, - BatchSize: &intstr.IntOrString{ - Type: intstr.Int, - IntVal: 1, - }, - }, - }, - err: ``, - }, - { - desp: "normal test & parallel string", - state: State{ - BaseState: BaseState{ - Name: "1", - Type: "2", - }, - ForEachState: &ForEachState{ - InputCollection: "3", - Actions: []Action{ - {}, - }, - Mode: ForEachModeTypeParallel, - BatchSize: &intstr.IntOrString{ - Type: intstr.String, - StrVal: "1", - }, - }, - }, - err: ``, - }, - { - desp: "invalid parallel int", - state: State{ - BaseState: BaseState{ - Name: "1", - Type: "2", - }, - ForEachState: &ForEachState{ - InputCollection: "3", - Actions: []Action{ - {}, - }, - Mode: ForEachModeTypeParallel, - BatchSize: &intstr.IntOrString{ - Type: intstr.Int, - IntVal: 0, - }, - }, - }, - err: `Key: 'State.ForEachState.BatchSize' Error:Field validation for 'BatchSize' failed on the 'gt0' tag`, - }, - { - desp: "invalid parallel string", - state: State{ - BaseState: BaseState{ - Name: "1", - Type: "2", - }, - ForEachState: &ForEachState{ - InputCollection: "3", - Actions: []Action{ - {}, - }, - Mode: ForEachModeTypeParallel, - BatchSize: &intstr.IntOrString{ - Type: intstr.String, - StrVal: "0", - }, - }, - }, - err: `Key: 'State.ForEachState.BatchSize' Error:Field validation for 'BatchSize' failed on the 'gt0' tag`, - }, - { - desp: "invalid parallel string format", - state: State{ - BaseState: BaseState{ - Name: "1", - Type: "2", - }, - ForEachState: &ForEachState{ - InputCollection: "3", - Actions: []Action{ - {}, - }, - Mode: ForEachModeTypeParallel, - BatchSize: &intstr.IntOrString{ - Type: intstr.String, - StrVal: "a", - }, - }, - }, - err: `Key: 'State.ForEachState.BatchSize' Error:Field validation for 'BatchSize' failed on the 'gt0' tag`, - }, - } - for _, tc := range testCases { - t.Run(tc.desp, func(t *testing.T) { - err := val.GetValidator().Struct(tc.state) - - if tc.err != "" { - assert.Error(t, err) - assert.Regexp(t, tc.err, err) - return - } - - assert.NoError(t, err) - }) - } -} diff --git a/model/foreach_state_validator.go b/model/foreach_state_validator.go new file mode 100644 index 0000000..6543ded --- /dev/null +++ b/model/foreach_state_validator.go @@ -0,0 +1,59 @@ +// Copyright 2022 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "context" + "reflect" + "strconv" + + validator "github.com/go-playground/validator/v10" + val "github.com/serverlessworkflow/sdk-go/v2/validator" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func init() { + val.GetValidator().RegisterStructValidationCtx(forEachStateStructLevelValidation, ForEachState{}) +} + +// ForEachStateStructLevelValidation custom validator for ForEachState +func forEachStateStructLevelValidation(_ context.Context, structLevel validator.StructLevel) { + stateObj := structLevel.Current().Interface().(ForEachState) + + if stateObj.Mode != ForEachModeTypeParallel { + return + } + + if stateObj.BatchSize == nil { + return + } + + switch stateObj.BatchSize.Type { + case intstr.Int: + if stateObj.BatchSize.IntVal <= 0 { + structLevel.ReportError(reflect.ValueOf(stateObj.BatchSize), "BatchSize", "batchSize", "gt0", "") + } + case intstr.String: + v, err := strconv.Atoi(stateObj.BatchSize.StrVal) + if err != nil { + structLevel.ReportError(reflect.ValueOf(stateObj.BatchSize), "BatchSize", "batchSize", "gt0", err.Error()) + return + } + + if v <= 0 { + structLevel.ReportError(reflect.ValueOf(stateObj.BatchSize), "BatchSize", "batchSize", "gt0", "") + } + } +} diff --git a/model/foreach_state_validator_test.go b/model/foreach_state_validator_test.go new file mode 100644 index 0000000..df01a32 --- /dev/null +++ b/model/foreach_state_validator_test.go @@ -0,0 +1,183 @@ +// Copyright 2022 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "testing" + + val "github.com/serverlessworkflow/sdk-go/v2/validator" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestForEachStateStructLevelValidation(t *testing.T) { + type testCase struct { + desp string + state State + err string + } + testCases := []testCase{ + { + desp: "normal test & sequential", + state: State{ + BaseState: BaseState{ + Name: "1", + Type: "2", + End: &End{ + Terminate: true, + }, + }, + ForEachState: &ForEachState{ + InputCollection: "3", + Actions: []Action{ + {}, + }, + Mode: ForEachModeTypeSequential, + }, + }, + err: ``, + }, + { + desp: "normal test & parallel int", + state: State{ + BaseState: BaseState{ + Name: "1", + Type: "2", + End: &End{ + Terminate: true, + }, + }, + ForEachState: &ForEachState{ + InputCollection: "3", + Actions: []Action{ + {}, + }, + Mode: ForEachModeTypeParallel, + BatchSize: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + err: ``, + }, + { + desp: "normal test & parallel string", + state: State{ + BaseState: BaseState{ + Name: "1", + Type: "2", + End: &End{ + Terminate: true, + }, + }, + ForEachState: &ForEachState{ + InputCollection: "3", + Actions: []Action{ + {}, + }, + Mode: ForEachModeTypeParallel, + BatchSize: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "1", + }, + }, + }, + err: ``, + }, + { + desp: "invalid parallel int", + state: State{ + BaseState: BaseState{ + Name: "1", + Type: "2", + End: &End{ + Terminate: true, + }, + }, + ForEachState: &ForEachState{ + InputCollection: "3", + Actions: []Action{ + {}, + }, + Mode: ForEachModeTypeParallel, + BatchSize: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 0, + }, + }, + }, + err: `Key: 'State.ForEachState.BatchSize' Error:Field validation for 'BatchSize' failed on the 'gt0' tag`, + }, + { + desp: "invalid parallel string", + state: State{ + BaseState: BaseState{ + Name: "1", + Type: "2", + End: &End{ + Terminate: true, + }, + }, + ForEachState: &ForEachState{ + InputCollection: "3", + Actions: []Action{ + {}, + }, + Mode: ForEachModeTypeParallel, + BatchSize: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "0", + }, + }, + }, + err: `Key: 'State.ForEachState.BatchSize' Error:Field validation for 'BatchSize' failed on the 'gt0' tag`, + }, + { + desp: "invalid parallel string format", + state: State{ + BaseState: BaseState{ + Name: "1", + Type: "2", + }, + ForEachState: &ForEachState{ + InputCollection: "3", + Actions: []Action{ + {}, + }, + Mode: ForEachModeTypeParallel, + BatchSize: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "a", + }, + }, + }, + err: `Key: 'State.ForEachState.BatchSize' Error:Field validation for 'BatchSize' failed on the 'gt0' tag`, + }, + } + for _, tc := range testCases { + t.Run(tc.desp, func(t *testing.T) { + err := val.GetValidator().Struct(tc.state) + + if tc.err != "" { + assert.Error(t, err) + assert.Regexp(t, tc.err, err) + return + } + + assert.NoError(t, err) + }) + } +} diff --git a/model/parallel_state.go b/model/parallel_state.go index e512ffa..53bce0f 100644 --- a/model/parallel_state.go +++ b/model/parallel_state.go @@ -15,16 +15,10 @@ package model import ( - "context" "encoding/json" "fmt" - "reflect" - "strconv" - validator "github.com/go-playground/validator/v10" "k8s.io/apimachinery/pkg/util/intstr" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" ) // CompletionType define on how to complete branch execution. @@ -109,36 +103,3 @@ type ParallelStateTimeout struct { StateExecTimeout *StateExecTimeout `json:"stateExecTimeout,omitempty"` BranchExecTimeout string `json:"branchExecTimeout,omitempty" validate:"omitempty,iso8601duration"` } - -// ParallelStateStructLevelValidation custom validator for ParallelState -func ParallelStateStructLevelValidation(_ context.Context, structLevel validator.StructLevel) { - parallelStateObj := structLevel.Current().Interface().(ParallelState) - - if parallelStateObj.CompletionType == CompletionTypeAllOf { - return - } - - switch parallelStateObj.NumCompleted.Type { - case intstr.Int: - if parallelStateObj.NumCompleted.IntVal <= 0 { - structLevel.ReportError(reflect.ValueOf(parallelStateObj.NumCompleted), "NumCompleted", "numCompleted", "gt0", "") - } - case intstr.String: - v, err := strconv.Atoi(parallelStateObj.NumCompleted.StrVal) - if err != nil { - structLevel.ReportError(reflect.ValueOf(parallelStateObj.NumCompleted), "NumCompleted", "numCompleted", "gt0", err.Error()) - return - } - - if v <= 0 { - structLevel.ReportError(reflect.ValueOf(parallelStateObj.NumCompleted), "NumCompleted", "numCompleted", "gt0", "") - } - } -} - -func init() { - val.GetValidator().RegisterStructValidationCtx( - ParallelStateStructLevelValidation, - ParallelState{}, - ) -} diff --git a/model/parallel_state_test.go b/model/parallel_state_test.go index c824d3b..b95cc69 100644 --- a/model/parallel_state_test.go +++ b/model/parallel_state_test.go @@ -20,8 +20,6 @@ import ( "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/util/intstr" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" ) func TestParallelStateUnmarshalJSON(t *testing.T) { @@ -67,135 +65,3 @@ func TestParallelStateUnmarshalJSON(t *testing.T) { }) } } - -func TestParallelStateStructLevelValidation(t *testing.T) { - type testCase struct { - desp string - state *State - err string - } - testCases := []testCase{ - { - desp: "normal", - state: &State{ - BaseState: BaseState{ - Name: "1", - Type: "parallel", - }, - ParallelState: &ParallelState{ - Branches: []Branch{ - { - Name: "b1", - Actions: []Action{ - {}, - }, - }, - }, - CompletionType: CompletionTypeAllOf, - NumCompleted: intstr.FromInt(1), - }, - }, - err: ``, - }, - { - desp: "invalid completeType", - state: &State{ - BaseState: BaseState{ - Name: "1", - Type: "parallel", - }, - ParallelState: &ParallelState{ - Branches: []Branch{ - { - Name: "b1", - Actions: []Action{ - {}, - }, - }, - }, - CompletionType: CompletionTypeAllOf + "1", - }, - }, - err: `Key: 'State.ParallelState.CompletionType' Error:Field validation for 'CompletionType' failed on the 'oneof' tag`, - }, - { - desp: "invalid numCompleted `int`", - state: &State{ - BaseState: BaseState{ - Name: "1", - Type: "parallel", - }, - ParallelState: &ParallelState{ - Branches: []Branch{ - { - Name: "b1", - Actions: []Action{ - {}, - }, - }, - }, - CompletionType: CompletionTypeAtLeast, - NumCompleted: intstr.FromInt(0), - }, - }, - err: `Key: 'State.ParallelState.NumCompleted' Error:Field validation for 'NumCompleted' failed on the 'gt0' tag`, - }, - { - desp: "invalid numCompleted string format", - state: &State{ - BaseState: BaseState{ - Name: "1", - Type: "parallel", - }, - ParallelState: &ParallelState{ - Branches: []Branch{ - { - Name: "b1", - Actions: []Action{ - {}, - }, - }, - }, - CompletionType: CompletionTypeAtLeast, - NumCompleted: intstr.FromString("a"), - }, - }, - err: `Key: 'State.ParallelState.NumCompleted' Error:Field validation for 'NumCompleted' failed on the 'gt0' tag`, - }, - { - desp: "normal", - state: &State{ - BaseState: BaseState{ - Name: "1", - Type: "parallel", - }, - ParallelState: &ParallelState{ - Branches: []Branch{ - { - Name: "b1", - Actions: []Action{ - {}, - }, - }, - }, - CompletionType: CompletionTypeAtLeast, - NumCompleted: intstr.FromString("0"), - }, - }, - err: `Key: 'State.ParallelState.NumCompleted' Error:Field validation for 'NumCompleted' failed on the 'gt0' tag`, - }, - } - for _, tc := range testCases { - t.Run(tc.desp, func(t *testing.T) { - err := val.GetValidator().Struct(tc.state) - - if tc.err != "" { - assert.Error(t, err) - assert.Regexp(t, tc.err, err) - return - } - - assert.NoError(t, err) - }) - } -} diff --git a/model/parallel_state_validator.go b/model/parallel_state_validator.go new file mode 100644 index 0000000..5286988 --- /dev/null +++ b/model/parallel_state_validator.go @@ -0,0 +1,55 @@ +// Copyright 2022 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "context" + "reflect" + "strconv" + + validator "github.com/go-playground/validator/v10" + val "github.com/serverlessworkflow/sdk-go/v2/validator" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func init() { + val.GetValidator().RegisterStructValidationCtx(parallelStateStructLevelValidation, ParallelState{}) +} + +// ParallelStateStructLevelValidation custom validator for ParallelState +func parallelStateStructLevelValidation(_ context.Context, structLevel validator.StructLevel) { + parallelStateObj := structLevel.Current().Interface().(ParallelState) + + if parallelStateObj.CompletionType == CompletionTypeAllOf { + return + } + + switch parallelStateObj.NumCompleted.Type { + case intstr.Int: + if parallelStateObj.NumCompleted.IntVal <= 0 { + structLevel.ReportError(reflect.ValueOf(parallelStateObj.NumCompleted), "NumCompleted", "numCompleted", "gt0", "") + } + case intstr.String: + v, err := strconv.Atoi(parallelStateObj.NumCompleted.StrVal) + if err != nil { + structLevel.ReportError(reflect.ValueOf(parallelStateObj.NumCompleted), "NumCompleted", "numCompleted", "gt0", err.Error()) + return + } + + if v <= 0 { + structLevel.ReportError(reflect.ValueOf(parallelStateObj.NumCompleted), "NumCompleted", "numCompleted", "gt0", "") + } + } +} diff --git a/model/parallel_state_validator_test.go b/model/parallel_state_validator_test.go new file mode 100644 index 0000000..cc321ae --- /dev/null +++ b/model/parallel_state_validator_test.go @@ -0,0 +1,170 @@ +// Copyright 2022 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "testing" + + val "github.com/serverlessworkflow/sdk-go/v2/validator" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestParallelStateStructLevelValidation(t *testing.T) { + type testCase struct { + desp string + state *State + err string + } + testCases := []testCase{ + { + desp: "normal", + state: &State{ + BaseState: BaseState{ + Name: "1", + Type: "parallel", + End: &End{ + Terminate: true, + }, + }, + ParallelState: &ParallelState{ + Branches: []Branch{ + { + Name: "b1", + Actions: []Action{ + {}, + }, + }, + }, + CompletionType: CompletionTypeAllOf, + NumCompleted: intstr.FromInt(1), + }, + }, + err: ``, + }, + { + desp: "invalid completeType", + state: &State{ + BaseState: BaseState{ + Name: "1", + Type: "parallel", + End: &End{ + Terminate: true, + }, + }, + ParallelState: &ParallelState{ + Branches: []Branch{ + { + Name: "b1", + Actions: []Action{ + {}, + }, + }, + }, + CompletionType: CompletionTypeAllOf + "1", + }, + }, + err: `Key: 'State.ParallelState.CompletionType' Error:Field validation for 'CompletionType' failed on the 'oneof' tag`, + }, + { + desp: "invalid numCompleted `int`", + state: &State{ + BaseState: BaseState{ + Name: "1", + Type: "parallel", + End: &End{ + Terminate: true, + }, + }, + ParallelState: &ParallelState{ + Branches: []Branch{ + { + Name: "b1", + Actions: []Action{ + {}, + }, + }, + }, + CompletionType: CompletionTypeAtLeast, + NumCompleted: intstr.FromInt(0), + }, + }, + err: `Key: 'State.ParallelState.NumCompleted' Error:Field validation for 'NumCompleted' failed on the 'gt0' tag`, + }, + { + desp: "invalid numCompleted string format", + state: &State{ + BaseState: BaseState{ + Name: "1", + Type: "parallel", + End: &End{ + Terminate: true, + }, + }, + ParallelState: &ParallelState{ + Branches: []Branch{ + { + Name: "b1", + Actions: []Action{ + {}, + }, + }, + }, + CompletionType: CompletionTypeAtLeast, + NumCompleted: intstr.FromString("a"), + }, + }, + err: `Key: 'State.ParallelState.NumCompleted' Error:Field validation for 'NumCompleted' failed on the 'gt0' tag`, + }, + { + desp: "normal", + state: &State{ + BaseState: BaseState{ + Name: "1", + Type: "parallel", + End: &End{ + Terminate: true, + }, + }, + ParallelState: &ParallelState{ + Branches: []Branch{ + { + Name: "b1", + Actions: []Action{ + {}, + }, + }, + }, + CompletionType: CompletionTypeAtLeast, + NumCompleted: intstr.FromString("0"), + }, + }, + err: `Key: 'State.ParallelState.NumCompleted' Error:Field validation for 'NumCompleted' failed on the 'gt0' tag`, + }, + } + for _, tc := range testCases { + t.Run(tc.desp, func(t *testing.T) { + err := val.GetValidator().Struct(tc.state) + + if tc.err != "" { + assert.Error(t, err) + assert.Regexp(t, tc.err, err) + return + } + + assert.NoError(t, err) + }) + } +} diff --git a/model/retry.go b/model/retry.go index 2f2e57c..7430adb 100644 --- a/model/retry.go +++ b/model/retry.go @@ -15,22 +15,11 @@ package model import ( - "reflect" - - validator "github.com/go-playground/validator/v10" "k8s.io/apimachinery/pkg/util/intstr" "github.com/serverlessworkflow/sdk-go/v2/util/floatstr" - val "github.com/serverlessworkflow/sdk-go/v2/validator" ) -func init() { - val.GetValidator().RegisterStructValidation( - RetryStructLevelValidation, - Retry{}, - ) -} - // Retry ... type Retry struct { // Unique retry strategy name @@ -49,15 +38,3 @@ type Retry struct { // TODO: make iso8601duration compatible this type Jitter floatstr.Float32OrString `json:"jitter,omitempty" validate:"omitempty,min=0,max=1"` } - -// RetryStructLevelValidation custom validator for Retry Struct -func RetryStructLevelValidation(structLevel validator.StructLevel) { - retryObj := structLevel.Current().Interface().(Retry) - - if retryObj.Jitter.Type == floatstr.String && retryObj.Jitter.StrVal != "" { - err := val.ValidateISO8601TimeDuration(retryObj.Jitter.StrVal) - if err != nil { - structLevel.ReportError(reflect.ValueOf(retryObj.Jitter.StrVal), "Jitter", "jitter", "iso8601duration", "") - } - } -} diff --git a/model/retry_test.go b/model/retry_test.go index 228345e..c960f3c 100644 --- a/model/retry_test.go +++ b/model/retry_test.go @@ -13,109 +13,3 @@ // limitations under the License. package model - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/serverlessworkflow/sdk-go/v2/util/floatstr" - val "github.com/serverlessworkflow/sdk-go/v2/validator" -) - -func TestRetryStructLevelValidation(t *testing.T) { - type testCase struct { - desp string - retryObj Retry - err string - } - testCases := []testCase{ - { - desp: "normal", - retryObj: Retry{ - Name: "1", - Delay: "PT5S", - MaxDelay: "PT5S", - Increment: "PT5S", - Jitter: floatstr.FromString("PT5S"), - }, - err: ``, - }, - { - desp: "normal with all optinal", - retryObj: Retry{ - Name: "1", - }, - err: ``, - }, - { - desp: "missing required name", - retryObj: Retry{ - Name: "", - Delay: "PT5S", - MaxDelay: "PT5S", - Increment: "PT5S", - Jitter: floatstr.FromString("PT5S"), - }, - err: `Key: 'Retry.Name' Error:Field validation for 'Name' failed on the 'required' tag`, - }, - { - desp: "invalid delay duration", - retryObj: Retry{ - Name: "1", - Delay: "P5S", - MaxDelay: "PT5S", - Increment: "PT5S", - Jitter: floatstr.FromString("PT5S"), - }, - err: `Key: 'Retry.Delay' Error:Field validation for 'Delay' failed on the 'iso8601duration' tag`, - }, - { - desp: "invdalid max delay duration", - retryObj: Retry{ - Name: "1", - Delay: "PT5S", - MaxDelay: "P5S", - Increment: "PT5S", - Jitter: floatstr.FromString("PT5S"), - }, - err: `Key: 'Retry.MaxDelay' Error:Field validation for 'MaxDelay' failed on the 'iso8601duration' tag`, - }, - { - desp: "invalid increment duration", - retryObj: Retry{ - Name: "1", - Delay: "PT5S", - MaxDelay: "PT5S", - Increment: "P5S", - Jitter: floatstr.FromString("PT5S"), - }, - err: `Key: 'Retry.Increment' Error:Field validation for 'Increment' failed on the 'iso8601duration' tag`, - }, - { - desp: "invalid jitter duration", - retryObj: Retry{ - Name: "1", - Delay: "PT5S", - MaxDelay: "PT5S", - Increment: "PT5S", - Jitter: floatstr.FromString("P5S"), - }, - err: `Key: 'Retry.Jitter' Error:Field validation for 'Jitter' failed on the 'iso8601duration' tag`, - }, - } - - for _, tc := range testCases { - t.Run(tc.desp, func(t *testing.T) { - err := val.GetValidator().Struct(tc.retryObj) - - if tc.err != "" { - assert.Error(t, err) - assert.Regexp(t, tc.err, err) - return - } - - assert.NoError(t, err) - }) - } -} diff --git a/model/retry_validator.go b/model/retry_validator.go new file mode 100644 index 0000000..14886ce --- /dev/null +++ b/model/retry_validator.go @@ -0,0 +1,39 @@ +// Copyright 2021 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "reflect" + + validator "github.com/go-playground/validator/v10" + "github.com/serverlessworkflow/sdk-go/v2/util/floatstr" + val "github.com/serverlessworkflow/sdk-go/v2/validator" +) + +func init() { + val.GetValidator().RegisterStructValidation(retryStructLevelValidation, Retry{}) +} + +// RetryStructLevelValidation custom validator for Retry Struct +func retryStructLevelValidation(structLevel validator.StructLevel) { + retryObj := structLevel.Current().Interface().(Retry) + + if retryObj.Jitter.Type == floatstr.String && retryObj.Jitter.StrVal != "" { + err := val.ValidateISO8601TimeDuration(retryObj.Jitter.StrVal) + if err != nil { + structLevel.ReportError(reflect.ValueOf(retryObj.Jitter.StrVal), "Jitter", "jitter", "iso8601duration", "") + } + } +} diff --git a/model/retry_validator_test.go b/model/retry_validator_test.go new file mode 100644 index 0000000..78f1e70 --- /dev/null +++ b/model/retry_validator_test.go @@ -0,0 +1,120 @@ +// Copyright 2021 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "testing" + + "github.com/serverlessworkflow/sdk-go/v2/util/floatstr" + val "github.com/serverlessworkflow/sdk-go/v2/validator" + "github.com/stretchr/testify/assert" +) + +func TestRetryStructLevelValidation(t *testing.T) { + type testCase struct { + desp string + retryObj Retry + err string + } + testCases := []testCase{ + { + desp: "normal", + retryObj: Retry{ + Name: "1", + Delay: "PT5S", + MaxDelay: "PT5S", + Increment: "PT5S", + Jitter: floatstr.FromString("PT5S"), + }, + err: ``, + }, + { + desp: "normal with all optinal", + retryObj: Retry{ + Name: "1", + }, + err: ``, + }, + { + desp: "missing required name", + retryObj: Retry{ + Name: "", + Delay: "PT5S", + MaxDelay: "PT5S", + Increment: "PT5S", + Jitter: floatstr.FromString("PT5S"), + }, + err: `Key: 'Retry.Name' Error:Field validation for 'Name' failed on the 'required' tag`, + }, + { + desp: "invalid delay duration", + retryObj: Retry{ + Name: "1", + Delay: "P5S", + MaxDelay: "PT5S", + Increment: "PT5S", + Jitter: floatstr.FromString("PT5S"), + }, + err: `Key: 'Retry.Delay' Error:Field validation for 'Delay' failed on the 'iso8601duration' tag`, + }, + { + desp: "invdalid max delay duration", + retryObj: Retry{ + Name: "1", + Delay: "PT5S", + MaxDelay: "P5S", + Increment: "PT5S", + Jitter: floatstr.FromString("PT5S"), + }, + err: `Key: 'Retry.MaxDelay' Error:Field validation for 'MaxDelay' failed on the 'iso8601duration' tag`, + }, + { + desp: "invalid increment duration", + retryObj: Retry{ + Name: "1", + Delay: "PT5S", + MaxDelay: "PT5S", + Increment: "P5S", + Jitter: floatstr.FromString("PT5S"), + }, + err: `Key: 'Retry.Increment' Error:Field validation for 'Increment' failed on the 'iso8601duration' tag`, + }, + { + desp: "invalid jitter duration", + retryObj: Retry{ + Name: "1", + Delay: "PT5S", + MaxDelay: "PT5S", + Increment: "PT5S", + Jitter: floatstr.FromString("P5S"), + }, + err: `Key: 'Retry.Jitter' Error:Field validation for 'Jitter' failed on the 'iso8601duration' tag`, + }, + } + + for _, tc := range testCases { + t.Run(tc.desp, func(t *testing.T) { + err := val.GetValidator().Struct(tc.retryObj) + + if tc.err != "" { + assert.Error(t, err) + assert.Regexp(t, tc.err, err) + return + } + + assert.NoError(t, err) + }) + } +} diff --git a/model/sleep_state_test.go b/model/sleep_state_test.go index e6580df..47b6a1e 100644 --- a/model/sleep_state_test.go +++ b/model/sleep_state_test.go @@ -35,6 +35,9 @@ func TestSleepStateStructLevelValidation(t *testing.T) { BaseState: BaseState{ Name: "1", Type: "sleep", + End: &End{ + Terminate: true, + }, }, SleepState: &SleepState{ Duration: "PT10S", diff --git a/model/states.go b/model/states.go index 3b69238..9862651 100644 --- a/model/states.go +++ b/model/states.go @@ -18,29 +18,8 @@ import ( "encoding/json" "fmt" "strings" - - validator "github.com/go-playground/validator/v10" - val "github.com/serverlessworkflow/sdk-go/v2/validator" ) -func init() { - val.GetValidator().RegisterStructValidation(stateStructLevelValidation, State{}) -} - -func stateStructLevelValidation(structLevel validator.StructLevel) { - state := structLevel.Current().Interface().(State) - - hasTransition := state.Transition != nil - isEnd := state.End != nil && state.End.Terminate - - // TODO: Improve message errors - if !hasTransition && !isEnd { - structLevel.ReportError(nil, "State.Transition,State.End", "transition,end", "required", "") - } else if hasTransition && isEnd { - structLevel.ReportError(nil, "State.Transition", "transition", "required_without", "end") - } -} - // StateType ... type StateType string diff --git a/model/states_validator.go b/model/states_validator.go new file mode 100644 index 0000000..ee55846 --- /dev/null +++ b/model/states_validator.go @@ -0,0 +1,33 @@ +// Copyright 2021 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "reflect" + + validator "github.com/go-playground/validator/v10" + val "github.com/serverlessworkflow/sdk-go/v2/validator" +) + +func init() { + val.GetValidator().RegisterStructValidation(baseStateStructLevelValidation, BaseState{}) +} + +func baseStateStructLevelValidation(structLevel validator.StructLevel) { + baseState := structLevel.Current().Interface().(BaseState) + if baseState.Type != StateTypeSwitch { + validTransitionAndEnd(structLevel, reflect.ValueOf(baseState), baseState.Transition, baseState.End) + } +} diff --git a/model/states_test.go b/model/states_validator_test.go similarity index 73% rename from model/states_test.go rename to model/states_validator_test.go index 498ec8e..296f726 100644 --- a/model/states_test.go +++ b/model/states_validator_test.go @@ -53,6 +53,28 @@ var stateEndDefault = State{ }, } +var switchStateTransitionDefault = State{ + BaseState: BaseState{ + Name: "name state", + Type: StateTypeSwitch, + }, + SwitchState: &SwitchState{ + DataConditions: []DataCondition{ + { + Condition: "${ .applicant | .age >= 18 }", + Transition: &Transition{ + NextState: "nex state", + }, + }, + }, + DefaultCondition: DefaultCondition{ + Transition: &Transition{ + NextState: "nex state", + }, + }, + }, +} + func TestStateStructLevelValidation(t *testing.T) { type testCase struct { name string @@ -71,6 +93,11 @@ func TestStateStructLevelValidation(t *testing.T) { instance: stateEndDefault, err: ``, }, + { + name: "switch state success", + instance: switchStateTransitionDefault, + err: ``, + }, { name: "state end and transition", instance: func() State { @@ -78,16 +105,16 @@ func TestStateStructLevelValidation(t *testing.T) { s.End = stateEndDefault.End return s }(), - err: `Key: 'State.State.Transition' Error:Field validation for 'State.Transition' failed on the 'required_without' tag`, + err: `Key: 'State.BaseState.Transition' Error:Field validation for 'Transition' failed on the 'exclusive' tag`, }, { - name: "state without end and transition", + name: "basestate without end and transition", instance: func() State { s := stateTransitionDefault s.Transition = nil return s }(), - err: `Key: 'State.State.Transition,State.End' Error:Field validation for 'State.Transition,State.End' failed on the 'required' tag`, + err: `Key: 'State.BaseState.Transition' Error:Field validation for 'Transition' failed on the 'required' tag`, }, } diff --git a/model/switch_state.go b/model/switch_state.go index 118b18c..cc630bc 100644 --- a/model/switch_state.go +++ b/model/switch_state.go @@ -15,22 +15,9 @@ package model import ( - "context" "encoding/json" - "reflect" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" - - validator "github.com/go-playground/validator/v10" ) -func init() { - val.GetValidator().RegisterStructValidationCtx(SwitchStateStructLevelValidation, SwitchState{}) - val.GetValidator().RegisterStructValidationCtx(DefaultConditionStructLevelValidation, DefaultCondition{}) - val.GetValidator().RegisterStructValidationCtx(EventConditionStructLevelValidation, EventCondition{}) - val.GetValidator().RegisterStructValidationCtx(DataConditionStructLevelValidation, DataCondition{}) -} - // SwitchState is workflow's gateways: direct transitions onf a workflow based on certain conditions. type SwitchState struct { // TODO: don't use BaseState for this, there are a few fields that SwitchState don't need. @@ -58,34 +45,12 @@ func (s *SwitchState) MarshalJSON() ([]byte, error) { return custom, err } -// SwitchStateStructLevelValidation custom validator for SwitchState -func SwitchStateStructLevelValidation(ctx context.Context, structLevel validator.StructLevel) { - switchState := structLevel.Current().Interface().(SwitchState) - switch { - case len(switchState.DataConditions) == 0 && len(switchState.EventConditions) == 0: - structLevel.ReportError(reflect.ValueOf(switchState), "DataConditions", "dataConditions", "required", "must have one of dataConditions, eventConditions") - case len(switchState.DataConditions) > 0 && len(switchState.EventConditions) > 0: - structLevel.ReportError(reflect.ValueOf(switchState), "DataConditions", "dataConditions", "exclusive", "must have one of dataConditions, eventConditions") - } -} - // DefaultCondition Can be either a transition or end definition type DefaultCondition struct { Transition *Transition `json:"transition,omitempty"` End *End `json:"end,omitempty"` } -// DefaultConditionStructLevelValidation custom validator for DefaultCondition -func DefaultConditionStructLevelValidation(ctx context.Context, structLevel validator.StructLevel) { - defaultCondition := structLevel.Current().Interface().(DefaultCondition) - switch { - case defaultCondition.End == nil && defaultCondition.Transition == nil: - structLevel.ReportError(reflect.ValueOf(defaultCondition), "Transition", "transition", "required", "must have one of transition, end") - case defaultCondition.Transition != nil && defaultCondition.End != nil: - structLevel.ReportError(reflect.ValueOf(defaultCondition), "Transition", "transition", "exclusive", "must have one of transition, end") - } -} - // SwitchStateTimeout defines the specific timeout settings for switch state type SwitchStateTimeout struct { StateExecTimeout *StateExecTimeout `json:"stateExecTimeout,omitempty"` @@ -111,17 +76,6 @@ type EventCondition struct { Transition *Transition `json:"transition" validate:"omitempty"` } -// EventConditionStructLevelValidation custom validator for EventCondition -func EventConditionStructLevelValidation(ctx context.Context, structLevel validator.StructLevel) { - eventCondition := structLevel.Current().Interface().(EventCondition) - switch { - case eventCondition.End == nil && eventCondition.Transition == nil: - structLevel.ReportError(reflect.ValueOf(eventCondition), "Transition", "transition", "required", "must have one of transition, end") - case eventCondition.Transition != nil && eventCondition.End != nil: - structLevel.ReportError(reflect.ValueOf(eventCondition), "Transition", "transition", "exclusive", "must have one of transition, end") - } -} - // DataCondition specify a data-based condition statement which causes a transition to another workflow state // if evaluated to true. type DataCondition struct { @@ -136,14 +90,3 @@ type DataCondition struct { // Workflow transition if condition is evaluated to true Transition *Transition `json:"transition" validate:"omitempty"` } - -// DataConditionStructLevelValidation custom validator for DataCondition -func DataConditionStructLevelValidation(ctx context.Context, structLevel validator.StructLevel) { - dataCondition := structLevel.Current().Interface().(DataCondition) - switch { - case dataCondition.End == nil && dataCondition.Transition == nil: - structLevel.ReportError(reflect.ValueOf(dataCondition), "Transition", "transition", "required", "must have one of transition, end") - case dataCondition.Transition != nil && dataCondition.End != nil: - structLevel.ReportError(reflect.ValueOf(dataCondition), "Transition", "transition", "exclusive", "must have one of transition, end") - } -} diff --git a/model/switch_state_test.go b/model/switch_state_test.go index 3136e4a..c960f3c 100644 --- a/model/switch_state_test.go +++ b/model/switch_state_test.go @@ -13,306 +13,3 @@ // limitations under the License. package model - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" -) - -func TestSwitchStateStructLevelValidation(t *testing.T) { - type testCase struct { - desp string - obj State - err string - } - testCases := []testCase{ - { - desp: "normal & eventConditions", - obj: State{ - BaseState: BaseState{ - Name: "1", - Type: "switch", - }, - SwitchState: &SwitchState{ - DefaultCondition: DefaultCondition{ - Transition: &Transition{ - NextState: "1", - }, - }, - EventConditions: []EventCondition{ - { - EventRef: "1", - Transition: &Transition{ - NextState: "2", - }, - }, - }, - }, - }, - err: ``, - }, - { - desp: "normal & dataConditions", - obj: State{ - BaseState: BaseState{ - Name: "1", - Type: "switch", - }, - SwitchState: &SwitchState{ - DefaultCondition: DefaultCondition{ - Transition: &Transition{ - NextState: "1", - }, - }, - DataConditions: []DataCondition{ - { - Condition: "1", - Transition: &Transition{ - NextState: "2", - }, - }, - }, - }, - }, - err: ``, - }, - { - desp: "missing eventConditions & dataConditions", - obj: State{ - BaseState: BaseState{ - Name: "1", - Type: "switch", - }, - SwitchState: &SwitchState{ - DefaultCondition: DefaultCondition{ - Transition: &Transition{ - NextState: "1", - }, - }, - }, - }, - err: `Key: 'State.SwitchState.DataConditions' Error:Field validation for 'DataConditions' failed on the 'required' tag`, - }, - { - desp: "exclusive eventConditions & dataConditions", - obj: State{ - BaseState: BaseState{ - Name: "1", - Type: "switch", - }, - SwitchState: &SwitchState{ - DefaultCondition: DefaultCondition{ - Transition: &Transition{ - NextState: "1", - }, - }, - EventConditions: []EventCondition{ - { - EventRef: "1", - Transition: &Transition{ - NextState: "2", - }, - }, - }, - DataConditions: []DataCondition{ - { - Condition: "1", - Transition: &Transition{ - NextState: "2", - }, - }, - }, - }, - }, - err: `Key: 'State.SwitchState.DataConditions' Error:Field validation for 'DataConditions' failed on the 'exclusive' tag`, - }, - } - for _, tc := range testCases { - t.Run(tc.desp, func(t *testing.T) { - err := val.GetValidator().Struct(tc.obj) - - if tc.err != "" { - assert.Error(t, err) - assert.Regexp(t, tc.err, err) - return - } - - assert.NoError(t, err) - }) - } -} - -func TestDefaultConditionStructLevelValidation(t *testing.T) { - type testCase struct { - desp string - obj DefaultCondition - err string - } - testCases := []testCase{ - { - desp: "normal & end", - obj: DefaultCondition{ - End: &End{}, - }, - err: ``, - }, - { - desp: "normal & transition", - obj: DefaultCondition{ - Transition: &Transition{ - NextState: "1", - }, - }, - err: ``, - }, - { - desp: "missing end & transition", - obj: DefaultCondition{}, - err: `DefaultCondition.Transition' Error:Field validation for 'Transition' failed on the 'required' tag`, - }, - { - desp: "exclusive end & transition", - obj: DefaultCondition{ - End: &End{}, - Transition: &Transition{ - NextState: "1", - }, - }, - err: `Key: 'DefaultCondition.Transition' Error:Field validation for 'Transition' failed on the 'exclusive' tag`, - }, - } - for _, tc := range testCases { - t.Run(tc.desp, func(t *testing.T) { - err := val.GetValidator().Struct(tc.obj) - - if tc.err != "" { - assert.Error(t, err) - assert.Regexp(t, tc.err, err) - return - } - - assert.NoError(t, err) - }) - } -} - -func TestEventConditionStructLevelValidation(t *testing.T) { - type testCase struct { - desp string - obj EventCondition - err string - } - testCases := []testCase{ - { - desp: "normal & end", - obj: EventCondition{ - EventRef: "1", - End: &End{}, - }, - err: ``, - }, - { - desp: "normal & transition", - obj: EventCondition{ - EventRef: "1", - Transition: &Transition{ - NextState: "1", - }, - }, - err: ``, - }, - { - desp: "missing end & transition", - obj: EventCondition{ - EventRef: "1", - }, - err: `Key: 'EventCondition.Transition' Error:Field validation for 'Transition' failed on the 'required' tag`, - }, - { - desp: "exclusive end & transition", - obj: EventCondition{ - EventRef: "1", - End: &End{}, - Transition: &Transition{ - NextState: "1", - }, - }, - err: `Key: 'EventCondition.Transition' Error:Field validation for 'Transition' failed on the 'exclusive' tag`, - }, - } - for _, tc := range testCases { - t.Run(tc.desp, func(t *testing.T) { - err := val.GetValidator().Struct(tc.obj) - - if tc.err != "" { - assert.Error(t, err) - assert.Regexp(t, tc.err, err) - return - } - - assert.NoError(t, err) - }) - } -} - -func TestDataConditionStructLevelValidation(t *testing.T) { - type testCase struct { - desp string - obj DataCondition - err string - } - testCases := []testCase{ - { - desp: "normal & end", - obj: DataCondition{ - Condition: "1", - End: &End{}, - }, - err: ``, - }, - { - desp: "normal & transition", - obj: DataCondition{ - Condition: "1", - Transition: &Transition{ - NextState: "1", - }, - }, - err: ``, - }, - { - desp: "missing end & transition", - obj: DataCondition{ - Condition: "1", - }, - err: `Key: 'DataCondition.Transition' Error:Field validation for 'Transition' failed on the 'required' tag`, - }, - { - desp: "exclusive end & transition", - obj: DataCondition{ - Condition: "1", - End: &End{}, - Transition: &Transition{ - NextState: "1", - }, - }, - err: `Key: 'DataCondition.Transition' Error:Field validation for 'Transition' failed on the 'exclusive' tag`, - }, - } - for _, tc := range testCases { - t.Run(tc.desp, func(t *testing.T) { - err := val.GetValidator().Struct(tc.obj) - - if tc.err != "" { - assert.Error(t, err) - assert.Regexp(t, tc.err, err) - return - } - - assert.NoError(t, err) - }) - } -} diff --git a/model/switch_state_validator.go b/model/switch_state_validator.go new file mode 100644 index 0000000..83f1379 --- /dev/null +++ b/model/switch_state_validator.go @@ -0,0 +1,59 @@ +// Copyright 2022 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "reflect" + + validator "github.com/go-playground/validator/v10" + val "github.com/serverlessworkflow/sdk-go/v2/validator" +) + +func init() { + val.GetValidator().RegisterStructValidation(switchStateStructLevelValidation, SwitchState{}) + val.GetValidator().RegisterStructValidation(defaultConditionStructLevelValidation, DefaultCondition{}) + val.GetValidator().RegisterStructValidation(eventConditionStructLevelValidation, EventCondition{}) + val.GetValidator().RegisterStructValidation(dataConditionStructLevelValidation, DataCondition{}) +} + +// SwitchStateStructLevelValidation custom validator for SwitchState +func switchStateStructLevelValidation(structLevel validator.StructLevel) { + switchState := structLevel.Current().Interface().(SwitchState) + + switch { + case len(switchState.DataConditions) == 0 && len(switchState.EventConditions) == 0: + structLevel.ReportError(reflect.ValueOf(switchState), "DataConditions", "dataConditions", "required", "must have one of dataConditions, eventConditions") + case len(switchState.DataConditions) > 0 && len(switchState.EventConditions) > 0: + structLevel.ReportError(reflect.ValueOf(switchState), "DataConditions", "dataConditions", "exclusive", "must have one of dataConditions, eventConditions") + } +} + +// DefaultConditionStructLevelValidation custom validator for DefaultCondition +func defaultConditionStructLevelValidation(structLevel validator.StructLevel) { + defaultCondition := structLevel.Current().Interface().(DefaultCondition) + validTransitionAndEnd(structLevel, reflect.ValueOf(defaultCondition), defaultCondition.Transition, defaultCondition.End) +} + +// EventConditionStructLevelValidation custom validator for EventCondition +func eventConditionStructLevelValidation(structLevel validator.StructLevel) { + eventCondition := structLevel.Current().Interface().(EventCondition) + validTransitionAndEnd(structLevel, reflect.ValueOf(eventCondition), eventCondition.Transition, eventCondition.End) +} + +// DataConditionStructLevelValidation custom validator for DataCondition +func dataConditionStructLevelValidation(structLevel validator.StructLevel) { + dataCondition := structLevel.Current().Interface().(DataCondition) + validTransitionAndEnd(structLevel, reflect.ValueOf(dataCondition), dataCondition.Transition, dataCondition.End) +} diff --git a/model/switch_state_validator_test.go b/model/switch_state_validator_test.go new file mode 100644 index 0000000..7bddc46 --- /dev/null +++ b/model/switch_state_validator_test.go @@ -0,0 +1,329 @@ +// Copyright 2022 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "testing" + + val "github.com/serverlessworkflow/sdk-go/v2/validator" + "github.com/stretchr/testify/assert" +) + +func TestSwitchStateStructLevelValidation(t *testing.T) { + type testCase struct { + desp string + obj State + err string + } + testCases := []testCase{ + { + desp: "normal & eventConditions", + obj: State{ + BaseState: BaseState{ + Name: "1", + Type: "switch", + }, + SwitchState: &SwitchState{ + DefaultCondition: DefaultCondition{ + Transition: &Transition{ + NextState: "1", + }, + }, + EventConditions: []EventCondition{ + { + EventRef: "1", + Transition: &Transition{ + NextState: "2", + }, + }, + }, + }, + }, + err: ``, + }, + { + desp: "normal & dataConditions", + obj: State{ + BaseState: BaseState{ + Name: "1", + Type: "switch", + }, + SwitchState: &SwitchState{ + DefaultCondition: DefaultCondition{ + Transition: &Transition{ + NextState: "1", + }, + }, + DataConditions: []DataCondition{ + { + Condition: "1", + Transition: &Transition{ + NextState: "2", + }, + }, + }, + }, + }, + err: ``, + }, + { + desp: "missing eventConditions & dataConditions", + obj: State{ + BaseState: BaseState{ + Name: "1", + Type: "switch", + }, + SwitchState: &SwitchState{ + DefaultCondition: DefaultCondition{ + Transition: &Transition{ + NextState: "1", + }, + }, + }, + }, + err: `Key: 'State.SwitchState.DataConditions' Error:Field validation for 'DataConditions' failed on the 'required' tag`, + }, + { + desp: "exclusive eventConditions & dataConditions", + obj: State{ + BaseState: BaseState{ + Name: "1", + Type: "switch", + }, + SwitchState: &SwitchState{ + DefaultCondition: DefaultCondition{ + Transition: &Transition{ + NextState: "1", + }, + }, + EventConditions: []EventCondition{ + { + EventRef: "1", + Transition: &Transition{ + NextState: "2", + }, + }, + }, + DataConditions: []DataCondition{ + { + Condition: "1", + Transition: &Transition{ + NextState: "2", + }, + }, + }, + }, + }, + err: `Key: 'State.SwitchState.DataConditions' Error:Field validation for 'DataConditions' failed on the 'exclusive' tag`, + }, + } + for _, tc := range testCases { + t.Run(tc.desp, func(t *testing.T) { + err := val.GetValidator().Struct(tc.obj) + + if tc.err != "" { + assert.Error(t, err) + assert.Equal(t, tc.err, err.Error()) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestDefaultConditionStructLevelValidation(t *testing.T) { + type testCase struct { + desp string + obj DefaultCondition + err string + } + testCases := []testCase{ + { + desp: "normal & end", + obj: DefaultCondition{ + End: &End{ + Terminate: true, + }, + }, + err: ``, + }, + { + desp: "normal & transition", + obj: DefaultCondition{ + Transition: &Transition{ + NextState: "1", + }, + }, + err: ``, + }, + { + desp: "missing end & transition", + obj: DefaultCondition{}, + err: `DefaultCondition.Transition' Error:Field validation for 'Transition' failed on the 'required' tag`, + }, + { + desp: "exclusive end & transition", + obj: DefaultCondition{ + End: &End{ + Terminate: true, + }, + Transition: &Transition{ + NextState: "1", + }, + }, + err: `Key: 'DefaultCondition.Transition' Error:Field validation for 'Transition' failed on the 'exclusive' tag`, + }, + } + for _, tc := range testCases[2:] { + t.Run(tc.desp, func(t *testing.T) { + err := val.GetValidator().Struct(tc.obj) + + if tc.err != "" { + assert.Error(t, err) + assert.Regexp(t, tc.err, err) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestEventConditionStructLevelValidation(t *testing.T) { + type testCase struct { + desp string + obj EventCondition + err string + } + testCases := []testCase{ + { + desp: "normal & end", + obj: EventCondition{ + EventRef: "1", + End: &End{ + Terminate: true, + }, + }, + err: ``, + }, + { + desp: "normal & transition", + obj: EventCondition{ + EventRef: "1", + Transition: &Transition{ + NextState: "1", + }, + }, + err: ``, + }, + { + desp: "missing end & transition", + obj: EventCondition{ + EventRef: "1", + }, + err: `Key: 'EventCondition.Transition' Error:Field validation for 'Transition' failed on the 'required' tag`, + }, + { + desp: "exclusive end & transition", + obj: EventCondition{ + EventRef: "1", + End: &End{ + Terminate: true, + }, + Transition: &Transition{ + NextState: "1", + }, + }, + err: `Key: 'EventCondition.Transition' Error:Field validation for 'Transition' failed on the 'exclusive' tag`, + }, + } + for _, tc := range testCases { + t.Run(tc.desp, func(t *testing.T) { + err := val.GetValidator().Struct(tc.obj) + + if tc.err != "" { + assert.Error(t, err) + assert.Regexp(t, tc.err, err) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestDataConditionStructLevelValidation(t *testing.T) { + type testCase struct { + desp string + obj DataCondition + err string + } + testCases := []testCase{ + { + desp: "normal & end", + obj: DataCondition{ + Condition: "1", + End: &End{ + Terminate: true, + }, + }, + err: ``, + }, + { + desp: "normal & transition", + obj: DataCondition{ + Condition: "1", + Transition: &Transition{ + NextState: "1", + }, + }, + err: ``, + }, + { + desp: "missing end & transition", + obj: DataCondition{ + Condition: "1", + }, + err: `Key: 'DataCondition.Transition' Error:Field validation for 'Transition' failed on the 'required' tag`, + }, + { + desp: "exclusive end & transition", + obj: DataCondition{ + Condition: "1", + End: &End{ + Terminate: true, + }, + Transition: &Transition{ + NextState: "1", + }, + }, + err: `Key: 'DataCondition.Transition' Error:Field validation for 'Transition' failed on the 'exclusive' tag`, + }, + } + for _, tc := range testCases { + t.Run(tc.desp, func(t *testing.T) { + err := val.GetValidator().Struct(tc.obj) + + if tc.err != "" { + assert.Error(t, err) + assert.Regexp(t, tc.err, err) + return + } + + assert.NoError(t, err) + }) + } +} diff --git a/model/util_validator.go b/model/util_validator.go new file mode 100644 index 0000000..22f6926 --- /dev/null +++ b/model/util_validator.go @@ -0,0 +1,30 @@ +// Copyright 2020 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + validator "github.com/go-playground/validator/v10" +) + +func validTransitionAndEnd(structLevel validator.StructLevel, field interface{}, transition *Transition, end *End) { + hasTransition := transition != nil && transition.NextState != "" + isEnd := end != nil && (end.Terminate || end.ContinueAs != nil || len(end.ProduceEvents) > 0) // TODO: check the spec continueAs/produceEvents to see how it influences the end + + if !hasTransition && !isEnd { + structLevel.ReportError(field, "Transition", "transition", "required", "must have one of transition, end") + } else if hasTransition && isEnd { + structLevel.ReportError(field, "Transition", "transition", "exclusive", "must have one of transition, end") + } +} diff --git a/model/workflow.go b/model/workflow.go index e5e9d27..c61e3ea 100644 --- a/model/workflow.go +++ b/model/workflow.go @@ -18,11 +18,6 @@ import ( "bytes" "encoding/json" "fmt" - "reflect" - - validator "github.com/go-playground/validator/v10" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" ) // InvokeKind defines how the target is invoked. @@ -55,52 +50,6 @@ const ( UnlimitedTimeout = "unlimited" ) -func init() { - val.GetValidator().RegisterStructValidation(continueAsStructLevelValidation, ContinueAs{}) - val.GetValidator().RegisterStructValidation(workflowStructLevelValidation, Workflow{}) -} - -func continueAsStructLevelValidation(structLevel validator.StructLevel) { - continueAs := structLevel.Current().Interface().(ContinueAs) - if len(continueAs.WorkflowExecTimeout.Duration) > 0 { - if err := val.ValidateISO8601TimeDuration(continueAs.WorkflowExecTimeout.Duration); err != nil { - structLevel.ReportError(reflect.ValueOf(continueAs.WorkflowExecTimeout.Duration), - "workflowExecTimeout", "duration", "iso8601duration", "") - } - } -} - -// WorkflowStructLevelValidation custom validator -func workflowStructLevelValidation(structLevel validator.StructLevel) { - // unique name of the auth methods - // NOTE: we cannot add the custom validation of auth to AuthArray - // because `RegisterStructValidation` only works with struct type - wf := structLevel.Current().Interface().(Workflow) - dict := map[string]bool{} - - for _, a := range wf.BaseWorkflow.Auth { - if !dict[a.Name] { - dict[a.Name] = true - } else { - structLevel.ReportError(reflect.ValueOf(a.Name), "[]Auth.Name", "name", "reqnameunique", "") - } - } - - // start state name exist in workflow states list - if wf.BaseWorkflow.Start.StateName != "" { - startExist := false - for _, state := range wf.States { - if state.Name == wf.BaseWorkflow.Start.StateName { - startExist = true - break - } - } - if !startExist { - structLevel.ReportError(reflect.ValueOf(wf.BaseWorkflow.Start.StateName), "Start", "start", "startnotexist", "") - } - } -} - // BaseWorkflow describes the partial Workflow definition that does not rely on generic interfaces // to make it easy for custom unmarshalers implementations to unmarshal the common data structure. type BaseWorkflow struct { diff --git a/model/workflow_test.go b/model/workflow_test.go index dd7e830..c9ad3e9 100644 --- a/model/workflow_test.go +++ b/model/workflow_test.go @@ -19,173 +19,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" ) -var workflowStructDefault = Workflow{ - BaseWorkflow: BaseWorkflow{ - ID: "id", - SpecVersion: "0.8", - Auth: AuthArray{ - { - Name: "auth name", - }, - }, - Start: &Start{ - StateName: "name state", - }, - }, - States: []State{ - { - BaseState: BaseState{ - Name: "name state", - Type: StateTypeOperation, - Transition: &Transition{ - NextState: "next name state", - }, - }, - OperationState: &OperationState{ - ActionMode: "sequential", - Actions: []Action{ - {}, - }, - }, - }, - { - BaseState: BaseState{ - Name: "next name state", - Type: StateTypeOperation, - End: &End{ - Terminate: true, - }, - }, - OperationState: &OperationState{ - ActionMode: "sequential", - Actions: []Action{ - {}, - }, - }, - }, - }, -} - -func TestWorkflowStructLevelValidation(t *testing.T) { - type testCase[T any] struct { - name string - instance T - err string - } - testCases := []testCase[any]{ - { - name: "workflow success", - instance: workflowStructDefault, - }, - { - name: "workflow auth.name repeat", - instance: func() Workflow { - w := workflowStructDefault - w.Auth = append(w.Auth, w.Auth[0]) - return w - }(), - err: `Key: 'Workflow.[]Auth.Name' Error:Field validation for '[]Auth.Name' failed on the 'reqnameunique' tag`, - }, - { - name: "workflow id exclude key", - instance: func() Workflow { - w := workflowStructDefault - w.ID = "id" - w.Key = "" - return w - }(), - err: ``, - }, - { - name: "workflow key exclude id", - instance: func() Workflow { - w := workflowStructDefault - w.ID = "" - w.Key = "key" - return w - }(), - err: ``, - }, - { - name: "workflow id and key", - instance: func() Workflow { - w := workflowStructDefault - w.ID = "id" - w.Key = "key" - return w - }(), - err: ``, - }, - { - name: "workflow without id and key", - instance: func() Workflow { - w := workflowStructDefault - w.ID = "" - w.Key = "" - return w - }(), - err: `Key: 'Workflow.BaseWorkflow.ID' Error:Field validation for 'ID' failed on the 'required_without' tag -Key: 'Workflow.BaseWorkflow.Key' Error:Field validation for 'Key' failed on the 'required_without' tag`, - }, - { - name: "workflow start", - instance: func() Workflow { - w := workflowStructDefault - w.Start = &Start{ - StateName: "start state not found", - } - return w - }(), - err: `Key: 'Workflow.Start' Error:Field validation for 'Start' failed on the 'startnotexist' tag`, - }, - { - name: "valid ContinueAs", - instance: ContinueAs{ - WorkflowID: "another-test", - Version: "2", - Data: FromString("${ del(.customerCount) }"), - WorkflowExecTimeout: WorkflowExecTimeout{ - Duration: "PT1H", - Interrupt: false, - RunBefore: "test", - }, - }, - err: ``, - }, - { - name: "invalid WorkflowExecTimeout", - instance: ContinueAs{ - WorkflowID: "test", - Version: "1", - Data: FromString("${ del(.customerCount) }"), - WorkflowExecTimeout: WorkflowExecTimeout{ - Duration: "invalid", - }, - }, - err: `Key: 'ContinueAs.workflowExecTimeout' Error:Field validation for 'workflowExecTimeout' failed on the 'iso8601duration' tag`, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := val.GetValidator().Struct(tc.instance) - - if tc.err != "" { - assert.Error(t, err) - if err != nil { - assert.Equal(t, tc.err, err.Error()) - } - return - } - assert.NoError(t, err) - }) - } -} - func TestWorkflowStartUnmarshalJSON(t *testing.T) { type testCase struct { desp string diff --git a/model/workflow_validator.go b/model/workflow_validator.go new file mode 100644 index 0000000..0edef0e --- /dev/null +++ b/model/workflow_validator.go @@ -0,0 +1,98 @@ +// Copyright 2021 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "reflect" + + validator "github.com/go-playground/validator/v10" + val "github.com/serverlessworkflow/sdk-go/v2/validator" +) + +func init() { + val.GetValidator().RegisterStructValidation(continueAsStructLevelValidation, ContinueAs{}) + val.GetValidator().RegisterStructValidation(workflowStructLevelValidation, Workflow{}) +} + +func continueAsStructLevelValidation(structLevel validator.StructLevel) { + continueAs := structLevel.Current().Interface().(ContinueAs) + if len(continueAs.WorkflowExecTimeout.Duration) > 0 { + if err := val.ValidateISO8601TimeDuration(continueAs.WorkflowExecTimeout.Duration); err != nil { + structLevel.ReportError(reflect.ValueOf(continueAs.WorkflowExecTimeout.Duration), + "workflowExecTimeout", "duration", "iso8601duration", "") + } + } +} + +// WorkflowStructLevelValidation custom validator +func workflowStructLevelValidation(structLevel validator.StructLevel) { + // unique name of the auth methods + // NOTE: we cannot add the custom validation of auth to AuthArray + // because `RegisterStructValidation` only works with struct type + wf := structLevel.Current().Interface().(Workflow) + dict := map[string]bool{} + + for _, a := range wf.BaseWorkflow.Auth { + if !dict[a.Name] { + dict[a.Name] = true + } else { + structLevel.ReportError(reflect.ValueOf(a.Name), "[]Auth.Name", "name", "reqnameunique", "") + } + } + + startAndStatesTransitionValidator(structLevel, wf.BaseWorkflow.Start, wf.States) +} + +type statesGraphValidator struct { + state State + next map[string]*statesGraphValidator +} + +func startAndStatesTransitionValidator(structLevel validator.StructLevel, start *Start, states []State) { + statesMap := make(map[string]State, len(states)) + for _, state := range states { + statesMap[state.Name] = state + } + + if start != nil { + // if not exists the start transtion stop the states validations + if _, ok := statesMap[start.StateName]; !ok { + structLevel.ReportError(reflect.ValueOf(start), "Start", "start", "startnotexist", "") + return + } + } + + if len(states) == 1 { + return + } + + // Many unit tests fail + + // // Simple check if transition exists + // fail := false + // for _, state := range statesMap { + // if state.Transition != nil { + // if _, ok := statesMap[""]; !ok { + // structLevel.ReportError(nil, "", "", "transitionnotexists", state.Transition.NextState) + // fail = true + // } + // } + // } + // if fail { + // return + // } + + // // TODO: create states graph to complex check +} diff --git a/model/workflow_validator_test.go b/model/workflow_validator_test.go new file mode 100644 index 0000000..451d87f --- /dev/null +++ b/model/workflow_validator_test.go @@ -0,0 +1,237 @@ +// Copyright 2022 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + val "github.com/serverlessworkflow/sdk-go/v2/validator" +) + +var workflowStructDefault = Workflow{ + BaseWorkflow: BaseWorkflow{ + ID: "id", + SpecVersion: "0.8", + Auth: AuthArray{ + { + Name: "auth name", + }, + }, + Start: &Start{ + StateName: "name state", + }, + }, + States: []State{ + { + BaseState: BaseState{ + Name: "name state", + Type: StateTypeOperation, + Transition: &Transition{ + NextState: "next name state", + }, + }, + OperationState: &OperationState{ + ActionMode: "sequential", + Actions: []Action{ + {}, + }, + }, + }, + { + BaseState: BaseState{ + Name: "next name state", + Type: StateTypeOperation, + End: &End{ + Terminate: true, + }, + }, + OperationState: &OperationState{ + ActionMode: "sequential", + Actions: []Action{ + {}, + }, + }, + }, + }, +} + +var listStateTransition1 = []State{ + { + BaseState: BaseState{ + Name: "name state", + Type: StateTypeOperation, + Transition: &Transition{ + NextState: "next name state", + }, + }, + OperationState: &OperationState{ + ActionMode: "sequential", + Actions: []Action{{}}, + }, + }, + { + BaseState: BaseState{ + Name: "next name state", + Type: StateTypeOperation, + Transition: &Transition{ + NextState: "next name state 2", + }, + }, + OperationState: &OperationState{ + ActionMode: "sequential", + Actions: []Action{{}}, + }, + }, + { + BaseState: BaseState{ + Name: "next name state 2", + Type: StateTypeOperation, + End: &End{ + Terminate: true, + }, + }, + OperationState: &OperationState{ + ActionMode: "sequential", + Actions: []Action{{}}, + }, + }, +} + +func TestWorkflowStructLevelValidation(t *testing.T) { + type testCase[T any] struct { + name string + instance T + err string + } + testCases := []testCase[any]{ + { + name: "workflow success", + instance: workflowStructDefault, + }, + { + name: "workflow auth.name repeat", + instance: func() Workflow { + w := workflowStructDefault + w.Auth = append(w.Auth, w.Auth[0]) + return w + }(), + err: `Key: 'Workflow.[]Auth.Name' Error:Field validation for '[]Auth.Name' failed on the 'reqnameunique' tag`, + }, + { + name: "workflow id exclude key", + instance: func() Workflow { + w := workflowStructDefault + w.ID = "id" + w.Key = "" + return w + }(), + err: ``, + }, + { + name: "workflow key exclude id", + instance: func() Workflow { + w := workflowStructDefault + w.ID = "" + w.Key = "key" + return w + }(), + err: ``, + }, + { + name: "workflow id and key", + instance: func() Workflow { + w := workflowStructDefault + w.ID = "id" + w.Key = "key" + return w + }(), + err: ``, + }, + { + name: "workflow without id and key", + instance: func() Workflow { + w := workflowStructDefault + w.ID = "" + w.Key = "" + return w + }(), + err: `Key: 'Workflow.BaseWorkflow.ID' Error:Field validation for 'ID' failed on the 'required_without' tag +Key: 'Workflow.BaseWorkflow.Key' Error:Field validation for 'Key' failed on the 'required_without' tag`, + }, + { + name: "workflow start", + instance: func() Workflow { + w := workflowStructDefault + w.Start = &Start{ + StateName: "start state not found", + } + return w + }(), + err: `Key: 'Workflow.Start' Error:Field validation for 'Start' failed on the 'startnotexist' tag`, + }, + { + name: "workflow states transitions", + instance: func() Workflow { + w := workflowStructDefault + w.States = listStateTransition1 + return w + }(), + err: ``, + }, + { + name: "valid ContinueAs", + instance: ContinueAs{ + WorkflowID: "another-test", + Version: "2", + Data: FromString("${ del(.customerCount) }"), + WorkflowExecTimeout: WorkflowExecTimeout{ + Duration: "PT1H", + Interrupt: false, + RunBefore: "test", + }, + }, + err: ``, + }, + { + name: "invalid WorkflowExecTimeout", + instance: ContinueAs{ + WorkflowID: "test", + Version: "1", + Data: FromString("${ del(.customerCount) }"), + WorkflowExecTimeout: WorkflowExecTimeout{ + Duration: "invalid", + }, + }, + err: `Key: 'ContinueAs.workflowExecTimeout' Error:Field validation for 'workflowExecTimeout' failed on the 'iso8601duration' tag`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := val.GetValidator().Struct(tc.instance) + + if tc.err != "" { + assert.Error(t, err) + if err != nil { + assert.Equal(t, tc.err, err.Error()) + } + return + } + assert.NoError(t, err) + }) + } +} diff --git a/parser/parser_test.go b/parser/parser_test.go index 9d8e275..66c47ff 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -39,9 +39,10 @@ func TestBasicValidation(t *testing.T) { for _, file := range files { if !file.IsDir() { - workflow, err := FromFile(filepath.Join(rootPath, file.Name())) + path := filepath.Join(rootPath, file.Name()) + workflow, err := FromFile(path) - if assert.NoError(t, err, "Test File %s", file.Name()) { + if assert.NoError(t, err, "Test File %s", path) { assert.NotEmpty(t, workflow.ID, "Test File %s", file.Name()) assert.NotEmpty(t, workflow.States, "Test File %s", file.Name()) } @@ -555,7 +556,6 @@ func TestFromFile(t *testing.T) { assert.NotNil(t, w.States[9].SleepState.Timeouts) assert.Equal(t, "PT100S", w.States[9].SleepState.Timeouts.StateExecTimeout.Total) assert.Equal(t, "PT200S", w.States[9].SleepState.Timeouts.StateExecTimeout.Single) - assert.Equal(t, &model.Transition{NextState: "GetJobStatus"}, w.States[9].Transition) assert.Equal(t, true, w.States[9].End.Terminate) }, }, @@ -773,6 +773,7 @@ states: stateExecTimeout: total: PT1S single: PT2S + transition: ParallelExec - name: ParallelExec type: parallel completionType: atLeast @@ -792,6 +793,7 @@ states: total: PT1S single: PT2S numCompleted: 13 + transition: CheckVisaStatusSwitchEventBased - name: CheckVisaStatusSwitchEventBased type: switch eventConditions: @@ -838,6 +840,7 @@ states: stateExecTimeout: total: PT11S single: PT22S + transition: HelloInject - name: HelloInject type: inject data: @@ -846,10 +849,10 @@ states: stateExecTimeout: total: PT11M single: PT22M + transition: WaitForCompletionSleep - name: WaitForCompletionSleep type: sleep duration: PT5S - transition: GetJobStatus timeouts: stateExecTimeout: total: PT100S @@ -885,6 +888,7 @@ states: stateExecTimeout: total: PT115M single: PT22M + transition: HandleApprovedVisa - name: HandleApprovedVisa type: operation actions: @@ -919,7 +923,7 @@ states: assert.True(t, strings.Contains(string(b), ":{\"metadata\":{\"auth1\":\"auth1\",\"auth2\":\"auth2\"}")) // Callback state - assert.True(t, strings.Contains(string(b), "{\"name\":\"CheckCreditCallback\",\"type\":\"callback\",\"action\":{\"functionRef\":{\"refName\":\"callCreditCheckMicroservice\",\"arguments\":{\"argsObj\":{\"age\":{\"final\":32,\"initial\":10},\"name\":\"hi\"},\"customer\":\"${ .customer }\",\"time\":48},\"invoke\":\"sync\"},\"sleep\":{\"before\":\"PT10S\",\"after\":\"PT20S\"},\"actionDataFilter\":{\"useResults\":true}},\"eventRef\":\"CreditCheckCompletedEvent\",\"eventDataFilter\":{\"useData\":true,\"data\":\"test data\",\"toStateData\":\"${ .customer }\"},\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT22M\",\"total\":\"PT115M\"},\"actionExecTimeout\":\"PT199M\",\"eventTimeout\":\"PT348S\"}}")) + assert.True(t, strings.Contains(string(b), "{\"name\":\"CheckCreditCallback\",\"type\":\"callback\",\"transition\":{\"nextState\":\"HandleApprovedVisa\"},\"action\":{\"functionRef\":{\"refName\":\"callCreditCheckMicroservice\",\"arguments\":{\"argsObj\":{\"age\":{\"final\":32,\"initial\":10},\"name\":\"hi\"},\"customer\":\"${ .customer }\",\"time\":48},\"invoke\":\"sync\"},\"sleep\":{\"before\":\"PT10S\",\"after\":\"PT20S\"},\"actionDataFilter\":{\"useResults\":true}},\"eventRef\":\"CreditCheckCompletedEvent\",\"eventDataFilter\":{\"useData\":true,\"data\":\"test data\",\"toStateData\":\"${ .customer }\"},\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT22M\",\"total\":\"PT115M\"},\"actionExecTimeout\":\"PT199M\",\"eventTimeout\":\"PT348S\"}}")) // Operation State assert.True(t, strings.Contains(string(b), "{\"name\":\"HandleApprovedVisa\",\"type\":\"operation\",\"end\":{\"terminate\":true},\"actionMode\":\"sequential\",\"actions\":[{\"name\":\"subFlowRefName\",\"subFlowRef\":{\"workflowId\":\"handleApprovedVisaWorkflowID\",\"invoke\":\"sync\",\"onParentComplete\":\"terminate\"},\"actionDataFilter\":{\"useResults\":true}},{\"name\":\"eventRefName\",\"eventRef\":{\"triggerEventRef\":\"StoreBidFunction\",\"resultEventRef\":\"StoreBidFunction\",\"data\":\"${ .patientInfo }\",\"contextAttributes\":{\"customer\":\"${ .customer }\",\"time\":50},\"invoke\":\"sync\"},\"actionDataFilter\":{\"useResults\":true}}],\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT123M\",\"total\":\"PT33M\"},\"actionExecTimeout\":\"PT777S\"}}")) @@ -928,22 +932,22 @@ states: assert.True(t, strings.Contains(string(b), "{\"name\":\"GreetDelay\",\"type\":\"delay\",\"transition\":{\"nextState\":\"Hello State\"},\"timeDelay\":\"PT5S\"}")) // Event State - assert.True(t, strings.Contains(string(b), "{\"name\":\"StoreCarAuctionBid\",\"type\":\"event\",\"exclusive\":true,\"onEvents\":[{\"eventRefs\":[\"CarBidEvent\"],\"actionMode\":\"parallel\",\"actions\":[{\"name\":\"bidFunctionRef\",\"functionRef\":{\"refName\":\"StoreBidFunction\",\"arguments\":{\"bid\":\"${ .bid }\"},\"invoke\":\"sync\"},\"actionDataFilter\":{\"useResults\":true}},{\"name\":\"bidEventRef\",\"eventRef\":{\"triggerEventRef\":\"StoreBidFunction\",\"resultEventRef\":\"StoreBidFunction\",\"data\":\"${ .patientInfo }\",\"contextAttributes\":{\"customer\":\"${ .thatBid }\",\"time\":32},\"invoke\":\"sync\"},\"actionDataFilter\":{\"useResults\":true}}],\"eventDataFilter\":{\"useData\":true,\"data\":\"test\",\"toStateData\":\"testing\"}}],\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT2S\",\"total\":\"PT1S\"},\"actionExecTimeout\":\"PT3S\",\"eventTimeout\":\"PT1H\"}}")) + assert.True(t, strings.Contains(string(b), "{\"name\":\"StoreCarAuctionBid\",\"type\":\"event\",\"transition\":{\"nextState\":\"ParallelExec\"},\"exclusive\":true,\"onEvents\":[{\"eventRefs\":[\"CarBidEvent\"],\"actionMode\":\"parallel\",\"actions\":[{\"name\":\"bidFunctionRef\",\"functionRef\":{\"refName\":\"StoreBidFunction\",\"arguments\":{\"bid\":\"${ .bid }\"},\"invoke\":\"sync\"},\"actionDataFilter\":{\"useResults\":true}},{\"name\":\"bidEventRef\",\"eventRef\":{\"triggerEventRef\":\"StoreBidFunction\",\"resultEventRef\":\"StoreBidFunction\",\"data\":\"${ .patientInfo }\",\"contextAttributes\":{\"customer\":\"${ .thatBid }\",\"time\":32},\"invoke\":\"sync\"},\"actionDataFilter\":{\"useResults\":true}}],\"eventDataFilter\":{\"useData\":true,\"data\":\"test\",\"toStateData\":\"testing\"}}],\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT2S\",\"total\":\"PT1S\"},\"actionExecTimeout\":\"PT3S\",\"eventTimeout\":\"PT1H\"}}")) // Parallel State - assert.True(t, strings.Contains(string(b), "{\"name\":\"ParallelExec\",\"type\":\"parallel\",\"branches\":[{\"name\":\"ShortDelayBranch\",\"actions\":[{\"subFlowRef\":{\"workflowId\":\"shortdelayworkflowid\",\"invoke\":\"sync\",\"onParentComplete\":\"terminate\"},\"actionDataFilter\":{\"useResults\":true}}],\"timeouts\":{\"actionExecTimeout\":\"PT5H\",\"branchExecTimeout\":\"PT6M\"}},{\"name\":\"LongDelayBranch\",\"actions\":[{\"subFlowRef\":{\"workflowId\":\"longdelayworkflowid\",\"invoke\":\"sync\",\"onParentComplete\":\"terminate\"},\"actionDataFilter\":{\"useResults\":true}}]}],\"completionType\":\"atLeast\",\"numCompleted\":13,\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT2S\",\"total\":\"PT1S\"},\"branchExecTimeout\":\"PT6M\"}}")) + assert.True(t, strings.Contains(string(b), "{\"name\":\"ParallelExec\",\"type\":\"parallel\",\"transition\":{\"nextState\":\"CheckVisaStatusSwitchEventBased\"},\"branches\":[{\"name\":\"ShortDelayBranch\",\"actions\":[{\"subFlowRef\":{\"workflowId\":\"shortdelayworkflowid\",\"invoke\":\"sync\",\"onParentComplete\":\"terminate\"},\"actionDataFilter\":{\"useResults\":true}}],\"timeouts\":{\"actionExecTimeout\":\"PT5H\",\"branchExecTimeout\":\"PT6M\"}},{\"name\":\"LongDelayBranch\",\"actions\":[{\"subFlowRef\":{\"workflowId\":\"longdelayworkflowid\",\"invoke\":\"sync\",\"onParentComplete\":\"terminate\"},\"actionDataFilter\":{\"useResults\":true}}]}],\"completionType\":\"atLeast\",\"numCompleted\":13,\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT2S\",\"total\":\"PT1S\"},\"branchExecTimeout\":\"PT6M\"}}")) // Switch State assert.True(t, strings.Contains(string(b), "{\"name\":\"CheckVisaStatusSwitchEventBased\",\"type\":\"switch\",\"defaultCondition\":{\"transition\":{\"nextState\":\"CheckCreditCallback\"}},\"eventConditions\":[{\"name\":\"visaApprovedEvent\",\"eventRef\":\"visaApprovedEventRef\",\"metadata\":{\"mastercard\":\"disallowed\",\"visa\":\"allowed\"},\"end\":null,\"transition\":{\"nextState\":\"HandleApprovedVisa\"}},{\"eventRef\":\"visaRejectedEvent\",\"metadata\":{\"test\":\"tested\"},\"end\":null,\"transition\":{\"nextState\":\"HandleRejectedVisa\"}}],\"dataConditions\":null,\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT20S\",\"total\":\"PT10S\"},\"eventTimeout\":\"PT10H\"}}")) // Foreach State - assert.True(t, strings.Contains(string(b), "{\"name\":\"SendTextForHighPriority\",\"type\":\"foreach\",\"inputCollection\":\"${ .messages }\",\"outputCollection\":\"${ .outputMessages }\",\"iterationParam\":\"${ .this }\",\"batchSize\":45,\"actions\":[{\"name\":\"test\",\"functionRef\":{\"refName\":\"sendTextFunction\",\"arguments\":{\"message\":\"${ .singlemessage }\"},\"invoke\":\"sync\"},\"eventRef\":{\"triggerEventRef\":\"example1\",\"resultEventRef\":\"example2\",\"resultEventTimeout\":\"PT12H\",\"invoke\":\"sync\"},\"actionDataFilter\":{\"useResults\":true}}],\"mode\":\"sequential\",\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT22S\",\"total\":\"PT11S\"},\"actionExecTimeout\":\"PT11H\"}}")) + assert.True(t, strings.Contains(string(b), "{\"name\":\"SendTextForHighPriority\",\"type\":\"foreach\",\"transition\":{\"nextState\":\"HelloInject\"},\"inputCollection\":\"${ .messages }\",\"outputCollection\":\"${ .outputMessages }\",\"iterationParam\":\"${ .this }\",\"batchSize\":45,\"actions\":[{\"name\":\"test\",\"functionRef\":{\"refName\":\"sendTextFunction\",\"arguments\":{\"message\":\"${ .singlemessage }\"},\"invoke\":\"sync\"},\"eventRef\":{\"triggerEventRef\":\"example1\",\"resultEventRef\":\"example2\",\"resultEventTimeout\":\"PT12H\",\"invoke\":\"sync\"},\"actionDataFilter\":{\"useResults\":true}}],\"mode\":\"sequential\",\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT22S\",\"total\":\"PT11S\"},\"actionExecTimeout\":\"PT11H\"}}")) // Inject State - assert.True(t, strings.Contains(string(b), "{\"name\":\"HelloInject\",\"type\":\"inject\",\"data\":{\"result\":\"Hello World, another state!\"},\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT22M\",\"total\":\"PT11M\"}}}")) + assert.True(t, strings.Contains(string(b), "{\"name\":\"HelloInject\",\"type\":\"inject\",\"transition\":{\"nextState\":\"WaitForCompletionSleep\"},\"data\":{\"result\":\"Hello World, another state!\"},\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT22M\",\"total\":\"PT11M\"}}}")) // Sleep State - assert.True(t, strings.Contains(string(b), "{\"name\":\"WaitForCompletionSleep\",\"type\":\"sleep\",\"transition\":{\"nextState\":\"GetJobStatus\"},\"end\":{\"terminate\":true},\"duration\":\"PT5S\",\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT200S\",\"total\":\"PT100S\"}}}")) + assert.True(t, strings.Contains(string(b), "{\"name\":\"WaitForCompletionSleep\",\"type\":\"sleep\",\"end\":{\"terminate\":true},\"duration\":\"PT5S\",\"timeouts\":{\"stateExecTimeout\":{\"single\":\"PT200S\",\"total\":\"PT100S\"}}}")) workflow = nil err = json.Unmarshal(b, &workflow) diff --git a/parser/testdata/workflows/greetings-v08-spec.sw.yaml b/parser/testdata/workflows/greetings-v08-spec.sw.yaml index 3b0bcf3..71800b0 100644 --- a/parser/testdata/workflows/greetings-v08-spec.sw.yaml +++ b/parser/testdata/workflows/greetings-v08-spec.sw.yaml @@ -65,6 +65,7 @@ states: stateExecTimeout: total: PT1S single: PT2S + transition: ParallelExec - name: ParallelExec type: parallel completionType: atLeast @@ -84,6 +85,7 @@ states: total: PT1S single: PT2S numCompleted: 13 + transition: CheckVisaStatusSwitchEventBased - name: CheckVisaStatusSwitchEventBased type: switch eventConditions: @@ -163,6 +165,7 @@ states: stateExecTimeout: total: PT11S single: PT22S + transition: HelloInject - name: HelloInject type: inject data: @@ -171,6 +174,7 @@ states: stateExecTimeout: total: PT11M single: PT22M + transition: CheckCreditCallback - name: CheckCreditCallback type: callback action: @@ -197,10 +201,10 @@ states: stateExecTimeout: total: PT115M single: PT22M + transition: WaitForCompletionSleep - name: WaitForCompletionSleep type: sleep duration: PT5S - transition: GetJobStatus timeouts: stateExecTimeout: total: PT100S diff --git a/parser/testdata/workflows/greetings_sleep.sw.json b/parser/testdata/workflows/greetings_sleep.sw.json index 5330bc5..9a434d4 100644 --- a/parser/testdata/workflows/greetings_sleep.sw.json +++ b/parser/testdata/workflows/greetings_sleep.sw.json @@ -20,7 +20,8 @@ "timeouts": { "stateExecTimeout": "PT10S" }, - "duration": "PT40S" + "duration": "PT40S", + "transition": "Greet" }, { "name": "Greet",