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 bc3c5df..9862651 100644 --- a/model/states.go +++ b/model/states.go @@ -53,7 +53,7 @@ type BaseState struct { // State type Type StateType `json:"type" validate:"required"` // States error handling and retries definitions - OnErrors []OnError `json:"onErrors,omitempty" validate:"omitempty,dive"` + OnErrors []OnError `json:"onErrors,omitempty" validate:"omitempty,dive"` // Next transition of the workflow after the time delay Transition *Transition `json:"transition,omitempty"` // State data filter @@ -187,7 +187,6 @@ func (s *State) MarshalJSON() ([]byte, error) { } func (s *State) UnmarshalJSON(data []byte) error { - if err := json.Unmarshal(data, &s.BaseState); err != nil { return err } @@ -204,7 +203,6 @@ func (s *State) UnmarshalJSON(data []byte) error { return err } s.DelayState = state - return nil case string(StateTypeEvent): state := &EventState{} @@ -212,7 +210,6 @@ func (s *State) UnmarshalJSON(data []byte) error { return err } s.EventState = state - return nil case string(StateTypeOperation): state := &OperationState{} @@ -220,7 +217,6 @@ func (s *State) UnmarshalJSON(data []byte) error { return err } s.OperationState = state - return nil case string(StateTypeParallel): state := &ParallelState{} @@ -228,7 +224,6 @@ func (s *State) UnmarshalJSON(data []byte) error { return err } s.ParallelState = state - return nil case string(StateTypeSwitch): state := &SwitchState{} @@ -236,7 +231,6 @@ func (s *State) UnmarshalJSON(data []byte) error { return err } s.SwitchState = state - return nil case string(StateTypeForEach): state := &ForEachState{} @@ -244,7 +238,6 @@ func (s *State) UnmarshalJSON(data []byte) error { return err } s.ForEachState = state - return nil case string(StateTypeInject): state := &InjectState{} @@ -252,7 +245,6 @@ func (s *State) UnmarshalJSON(data []byte) error { return err } s.InjectState = state - return nil case string(StateTypeCallback): state := &CallbackState{} @@ -260,7 +252,6 @@ func (s *State) UnmarshalJSON(data []byte) error { return err } s.CallbackState = state - return nil case string(StateTypeSleep): state := &SleepState{} @@ -268,9 +259,10 @@ func (s *State) UnmarshalJSON(data []byte) error { return err } s.SleepState = state - return nil - + case nil: + return fmt.Errorf("state parameter 'type' not defined") default: return fmt.Errorf("state type %v not supported", mapState["type"]) } + return nil } 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_validator_test.go b/model/states_validator_test.go new file mode 100644 index 0000000..296f726 --- /dev/null +++ b/model/states_validator_test.go @@ -0,0 +1,135 @@ +// 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" +) + +var stateTransitionDefault = State{ + BaseState: BaseState{ + Name: "name state", + Type: StateTypeOperation, + Transition: &Transition{ + NextState: "next name state", + }, + }, + OperationState: &OperationState{ + ActionMode: "sequential", + Actions: []Action{ + {}, + }, + }, +} + +var stateEndDefault = State{ + BaseState: BaseState{ + Name: "name state", + Type: StateTypeOperation, + End: &End{ + Terminate: true, + }, + }, + OperationState: &OperationState{ + ActionMode: "sequential", + Actions: []Action{ + {}, + }, + }, +} + +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 + instance State + err string + } + + testCases := []testCase{ + { + name: "state transition success", + instance: stateTransitionDefault, + err: ``, + }, + { + name: "state end success", + instance: stateEndDefault, + err: ``, + }, + { + name: "switch state success", + instance: switchStateTransitionDefault, + err: ``, + }, + { + name: "state end and transition", + instance: func() State { + s := stateTransitionDefault + s.End = stateEndDefault.End + return s + }(), + err: `Key: 'State.BaseState.Transition' Error:Field validation for 'Transition' failed on the 'exclusive' tag`, + }, + { + name: "basestate without end and transition", + instance: func() State { + s := stateTransitionDefault + s.Transition = nil + return s + }(), + err: `Key: 'State.BaseState.Transition' Error:Field validation for 'Transition' failed on the 'required' 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/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/workflow.go b/model/workflow.go index c182929..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,28 +50,13 @@ const ( UnlimitedTimeout = "unlimited" ) -func init() { - val.GetValidator().RegisterStructValidation(continueAsStructLevelValidation, ContinueAs{}) - val.GetValidator().RegisterStructValidation(BaseWorkflowStructLevelValidation, BaseWorkflow{}) -} - -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", "") - } - } -} - // 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 { // Workflow unique identifier - ID string `json:"id" validate:"omitempty,min=1"` + ID string `json:"id,omitempty" validate:"required_without=Key"` // Key Domain-specific workflow identifier - Key string `json:"key,omitempty" validate:"omitempty,min=1"` + Key string `json:"key,omitempty" validate:"required_without=ID"` // Workflow name Name string `json:"name,omitempty"` // Workflow description @@ -112,22 +92,6 @@ type BaseWorkflow struct { Auth AuthArray `json:"auth,omitempty" validate:"omitempty"` } -// BaseWorkflowStructLevelValidation custom validator for unique name of the auth methods -func BaseWorkflowStructLevelValidation(structLevel validator.StructLevel) { - // NOTE: we cannot add the custom validation of auth to AuthArray - // because `RegisterStructValidation` only works with struct type - wf := structLevel.Current().Interface().(BaseWorkflow) - dict := map[string]bool{} - - for _, a := range wf.Auth { - if !dict[a.Name] { - dict[a.Name] = true - } else { - structLevel.ReportError(reflect.ValueOf(a.Name), "Name", "name", "reqnameunique", "") - } - } -} - type AuthArray []Auth func (r *AuthArray) UnmarshalJSON(data []byte) error { @@ -186,8 +150,10 @@ func (w *Workflow) UnmarshalJSON(data []byte) error { } var rawStates []json.RawMessage - if err := json.Unmarshal(workflowMap["states"], &rawStates); err != nil { - return err + if _, ok := workflowMap["states"]; ok { + if err := json.Unmarshal(workflowMap["states"], &rawStates); err != nil { + return err + } } w.States = make([]State, len(rawStates)) @@ -197,6 +163,13 @@ func (w *Workflow) UnmarshalJSON(data []byte) error { } } + // if the start is not defined, use the first state + if w.BaseWorkflow.Start == nil && len(w.States) > 0 { + w.BaseWorkflow.Start = &Start{ + StateName: w.States[0].Name, + } + } + if _, ok := workflowMap["events"]; ok { if err := json.Unmarshal(workflowMap["events"], &w.Events); err != nil { var s string diff --git a/model/workflow_test.go b/model/workflow_test.go index 32ba486..c9ad3e9 100644 --- a/model/workflow_test.go +++ b/model/workflow_test.go @@ -19,56 +19,80 @@ import ( "testing" "github.com/stretchr/testify/assert" - - val "github.com/serverlessworkflow/sdk-go/v2/validator" ) -func TestContinueAsStructLevelValidation(t *testing.T) { +func TestWorkflowStartUnmarshalJSON(t *testing.T) { type testCase struct { - name string - continueAs ContinueAs - err string + desp string + data string + expect Workflow + err string } - testCases := []testCase{ { - name: "valid ContinueAs", - continueAs: ContinueAs{ - WorkflowID: "another-test", - Version: "2", - Data: FromString("${ del(.customerCount) }"), - WorkflowExecTimeout: WorkflowExecTimeout{ - Duration: "PT1H", - Interrupt: false, - RunBefore: "test", + desp: "start string", + data: `{"start": "start state name"}`, + expect: Workflow{ + BaseWorkflow: BaseWorkflow{ + ExpressionLang: "jq", + Start: &Start{ + StateName: "start state name", + }, }, + States: []State{}, }, err: ``, }, { - name: "invalid WorkflowExecTimeout", - continueAs: ContinueAs{ - WorkflowID: "test", - Version: "1", - Data: FromString("${ del(.customerCount) }"), - WorkflowExecTimeout: WorkflowExecTimeout{ - Duration: "invalid", + desp: "start empty and use the first state", + data: `{"states": [{"name": "start state name", "type": "operation"}]}`, + expect: Workflow{ + BaseWorkflow: BaseWorkflow{ + ExpressionLang: "jq", + Start: &Start{ + StateName: "start state name", + }, + }, + States: []State{ + { + BaseState: BaseState{ + Name: "start state name", + Type: StateTypeOperation, + }, + OperationState: &OperationState{ + ActionMode: "sequential", + }, + }, }, }, - err: `Key: 'ContinueAs.workflowExecTimeout' Error:Field validation for 'workflowExecTimeout' failed on the 'iso8601duration' tag`, + err: ``, + }, + { + desp: "start empty, and states empty", + data: `{"states": []}`, + expect: Workflow{ + BaseWorkflow: BaseWorkflow{ + ExpressionLang: "jq", + }, + States: []State{}, + }, + err: ``, }, } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := val.GetValidator().Struct(tc.continueAs) + for _, tc := range testCases[1:] { + t.Run(tc.desp, func(t *testing.T) { + var v Workflow + err := json.Unmarshal([]byte(tc.data), &v) if tc.err != "" { assert.Error(t, err) assert.Regexp(t, tc.err, err) return } + assert.NoError(t, err) + assert.Equal(t, tc.expect, v) }) } } diff --git a/model/workflow_validator.go b/model/workflow_validator.go new file mode 100644 index 0000000..68f8096 --- /dev/null +++ b/model/workflow_validator.go @@ -0,0 +1,97 @@ +// 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) +} + +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 + } + + // Naive check if transitions exist + for _, state := range statesMap { + if state.Transition != nil { + if _, ok := statesMap[state.Transition.NextState]; !ok { + structLevel.ReportError(reflect.ValueOf(state), "Transition", "transition", "transitionnotexists", state.Transition.NextState) + } + } + } + + // TODO: create states graph to complex check +} + +func validTransitionAndEnd(structLevel validator.StructLevel, field interface{}, transition *Transition, end *End) { + hasTransition := transition != nil + 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_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 d9c6f15..a11106f 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -16,7 +16,6 @@ package parser import ( "encoding/json" - "fmt" "os" "path/filepath" "strings" @@ -39,9 +38,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()) } @@ -379,8 +379,6 @@ func TestFromFile(t *testing.T) { // Workflow "name" no longer a required property assert.Empty(t, w.Name) - // Workflow "start" no longer a required property - assert.Empty(t, w.Start) // Functions: assert.NotEmpty(t, w.Functions[0]) @@ -557,7 +555,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) }, }, @@ -610,7 +607,7 @@ states: "version": "1.0", "name": "Applicant Request Decision Workflow", "description": "Determine if applicant request is valid", - "start": "CheckApplication", + "start": "Hello State", "specVersion": "0.8", "auth": [ { @@ -653,7 +650,7 @@ states: assert.NotNil(t, workflow.Auth) b, _ := json.Marshal(workflow) - assert.Equal(t, "{\"id\":\"applicantrequest\",\"name\":\"Applicant Request Decision Workflow\",\"description\":\"Determine if applicant request is valid\",\"version\":\"1.0\",\"start\":{\"stateName\":\"CheckApplication\"},\"specVersion\":\"0.8\",\"expressionLang\":\"jq\",\"auth\":[{\"name\":\"testAuth\",\"scheme\":\"bearer\",\"properties\":{\"token\":\"test_token\"}},{\"name\":\"testAuth2\",\"scheme\":\"basic\",\"properties\":{\"username\":\"test_user\",\"password\":\"test_pwd\"}}],\"states\":[{\"name\":\"Hello State\",\"type\":\"inject\",\"transition\":{\"nextState\":\"Next Hello State\"},\"data\":{\"result\":\"Hello World!\"}},{\"name\":\"Next Hello State\",\"type\":\"inject\",\"end\":{\"terminate\":true},\"data\":{\"result\":\"Next Hello World!\"}}]}", + assert.Equal(t, "{\"id\":\"applicantrequest\",\"name\":\"Applicant Request Decision Workflow\",\"description\":\"Determine if applicant request is valid\",\"version\":\"1.0\",\"start\":{\"stateName\":\"Hello State\"},\"specVersion\":\"0.8\",\"expressionLang\":\"jq\",\"auth\":[{\"name\":\"testAuth\",\"scheme\":\"bearer\",\"properties\":{\"token\":\"test_token\"}},{\"name\":\"testAuth2\",\"scheme\":\"basic\",\"properties\":{\"username\":\"test_user\",\"password\":\"test_pwd\"}}],\"states\":[{\"name\":\"Hello State\",\"type\":\"inject\",\"transition\":{\"nextState\":\"Next Hello State\"},\"data\":{\"result\":\"Hello World!\"}},{\"name\":\"Next Hello State\",\"type\":\"inject\",\"end\":{\"terminate\":true},\"data\":{\"result\":\"Next Hello World!\"}}]}", string(b)) }) @@ -665,7 +662,7 @@ states: "version": "1.0", "name": "Applicant Request Decision Workflow", "description": "Determine if applicant request is valid", - "start": "CheckApplication", + "start": "Hello State", "specVersion": "0.8", "auth": "./testdata/workflows/urifiles/auth.json", "states": [ @@ -684,7 +681,7 @@ states: assert.NotNil(t, workflow.Auth) b, _ := json.Marshal(workflow) - assert.Equal(t, "{\"id\":\"applicantrequest\",\"name\":\"Applicant Request Decision Workflow\",\"description\":\"Determine if applicant request is valid\",\"version\":\"1.0\",\"start\":{\"stateName\":\"CheckApplication\"},\"specVersion\":\"0.8\",\"expressionLang\":\"jq\",\"auth\":[{\"name\":\"testAuth\",\"scheme\":\"bearer\",\"properties\":{\"token\":\"test_token\"}},{\"name\":\"testAuth2\",\"scheme\":\"basic\",\"properties\":{\"username\":\"test_user\",\"password\":\"test_pwd\"}}],\"states\":[{\"name\":\"Hello State\",\"type\":\"inject\",\"end\":{\"terminate\":true},\"data\":{\"result\":\"Hello World!\"}}]}", + assert.Equal(t, "{\"id\":\"applicantrequest\",\"name\":\"Applicant Request Decision Workflow\",\"description\":\"Determine if applicant request is valid\",\"version\":\"1.0\",\"start\":{\"stateName\":\"Hello State\"},\"specVersion\":\"0.8\",\"expressionLang\":\"jq\",\"auth\":[{\"name\":\"testAuth\",\"scheme\":\"bearer\",\"properties\":{\"token\":\"test_token\"}},{\"name\":\"testAuth2\",\"scheme\":\"basic\",\"properties\":{\"username\":\"test_user\",\"password\":\"test_pwd\"}}],\"states\":[{\"name\":\"Hello State\",\"type\":\"inject\",\"end\":{\"terminate\":true},\"data\":{\"result\":\"Hello World!\"}}]}", string(b)) }) @@ -696,7 +693,7 @@ states: "version": "1.0", "name": "Applicant Request Decision Workflow", "description": "Determine if applicant request is valid", - "start": "CheckApplication", + "start": "Hello State", "specVersion": "0.7", "auth": 123, "states": [ @@ -726,7 +723,7 @@ version: '1.0.0' specVersion: '0.8' name: WorkflowStatesTest description: Inject Hello World -start: Hello State +start: GreetDelay metadata: metadata1: metadata1 metadata2: metadata2 @@ -743,7 +740,7 @@ states: type: delay timeDelay: PT5S transition: - nextState: Hello State + nextState: StoreCarAuctionBid - name: StoreCarAuctionBid type: event exclusive: true @@ -775,6 +772,7 @@ states: stateExecTimeout: total: PT1S single: PT2S + transition: ParallelExec - name: ParallelExec type: parallel completionType: atLeast @@ -794,6 +792,7 @@ states: total: PT1S single: PT2S numCompleted: 13 + transition: CheckVisaStatusSwitchEventBased - name: CheckVisaStatusSwitchEventBased type: switch eventConditions: @@ -840,6 +839,7 @@ states: stateExecTimeout: total: PT11S single: PT22S + transition: HelloInject - name: HelloInject type: inject data: @@ -848,10 +848,10 @@ states: stateExecTimeout: total: PT11M single: PT22M + transition: WaitForCompletionSleep - name: WaitForCompletionSleep type: sleep duration: PT5S - transition: GetJobStatus timeouts: stateExecTimeout: total: PT100S @@ -887,6 +887,7 @@ states: stateExecTimeout: total: PT115M single: PT22M + transition: HandleApprovedVisa - name: HandleApprovedVisa type: operation actions: @@ -910,7 +911,6 @@ states: terminate: true `)) assert.Nil(t, err) - fmt.Println(err) assert.NotNil(t, workflow) b, err := json.Marshal(workflow) @@ -921,31 +921,31 @@ 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\"}}")) // Delay State - assert.True(t, strings.Contains(string(b), "{\"name\":\"GreetDelay\",\"type\":\"delay\",\"transition\":{\"nextState\":\"Hello State\"},\"timeDelay\":\"PT5S\"}")) + assert.True(t, strings.Contains(string(b), "{\"name\":\"GreetDelay\",\"type\":\"delay\",\"transition\":{\"nextState\":\"StoreCarAuctionBid\"},\"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",