diff --git a/model/states.go b/model/states.go index b7b11ab..3b69238 100644 --- a/model/states.go +++ b/model/states.go @@ -18,8 +18,29 @@ import ( "encoding/json" "fmt" "strings" + + validator "github.com/go-playground/validator/v10" + val "github.com/serverlessworkflow/sdk-go/v2/validator" ) +func init() { + val.GetValidator().RegisterStructValidation(stateStructLevelValidation, State{}) +} + +func stateStructLevelValidation(structLevel validator.StructLevel) { + state := structLevel.Current().Interface().(State) + + hasTransition := state.Transition != nil + isEnd := state.End != nil && state.End.Terminate + + // TODO: Improve message errors + if !hasTransition && !isEnd { + structLevel.ReportError(nil, "State.Transition,State.End", "transition,end", "required", "") + } else if hasTransition && isEnd { + structLevel.ReportError(nil, "State.Transition", "transition", "required_without", "end") + } +} + // StateType ... type StateType string @@ -53,7 +74,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 diff --git a/model/states_test.go b/model/states_test.go new file mode 100644 index 0000000..498ec8e --- /dev/null +++ b/model/states_test.go @@ -0,0 +1,108 @@ +// 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{ + {}, + }, + }, +} + +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: "state end and transition", + instance: func() State { + s := stateTransitionDefault + s.End = stateEndDefault.End + return s + }(), + err: `Key: 'State.State.Transition' Error:Field validation for 'State.Transition' failed on the 'required_without' tag`, + }, + { + name: "state without end and transition", + instance: func() State { + s := stateTransitionDefault + s.Transition = nil + return s + }(), + err: `Key: 'State.State.Transition,State.End' Error:Field validation for 'State.Transition,State.End' failed on the 'required' tag`, + }, + } + + 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/workflow.go b/model/workflow.go index b76b48a..2fcd630 100644 --- a/model/workflow.go +++ b/model/workflow.go @@ -86,17 +86,17 @@ func workflowStructLevelValidation(structLevel validator.StructLevel) { } } - // start state name exists in states list + // start state name exist in workflow states list if wf.BaseWorkflow.Start.StateName != "" { - startExists := false + startExist := false for _, state := range wf.States { if state.Name == wf.BaseWorkflow.Start.StateName { - startExists = true + startExist = true break } } - if !startExists { - structLevel.ReportError(reflect.ValueOf(wf.BaseWorkflow.Start.StateName), "Start", "start", "startnotexists", "") + if !startExist { + structLevel.ReportError(reflect.ValueOf(wf.BaseWorkflow.Start.StateName), "Start", "start", "startnotexist", "") } } } diff --git a/model/workflow_test.go b/model/workflow_test.go index c02024a..90b6813 100644 --- a/model/workflow_test.go +++ b/model/workflow_test.go @@ -36,21 +36,41 @@ var workflowStructDefault = Workflow{ StateName: "name state", }, }, - States: []State{{ - BaseState: BaseState{ - Name: "name state", - Type: StateTypeOperation, + States: []State{ + { + BaseState: BaseState{ + Name: "name state", + Type: StateTypeOperation, + Transition: &Transition{ + NextState: "next name state", + }, + }, + OperationState: &OperationState{ + ActionMode: "sequential", + Actions: []Action{ + {}, + }, + }, }, - OperationState: &OperationState{ - ActionMode: "sequential", - Actions: []Action{ - {}, + { + BaseState: BaseState{ + Name: "next name state", + Type: StateTypeOperation, + End: &End{ + Terminate: true, + }, + }, + OperationState: &OperationState{ + ActionMode: "sequential", + Actions: []Action{ + {}, + }, }, }, - }}, + }, } -func TestValidationAsStructLevelValidation(t *testing.T) { +func TestWorkflowStructLevelValidation(t *testing.T) { type testCase[T any] struct { name string instance T @@ -120,7 +140,7 @@ Key: 'Workflow.BaseWorkflow.Key' Error:Field validation for 'Key' failed on the } return w }(), - err: `Key: 'Workflow.Start' Error:Field validation for 'Start' failed on the 'startnotexists' tag`, + err: `Key: 'Workflow.Start' Error:Field validation for 'Start' failed on the 'startnotexist' tag`, }, { name: "valid ContinueAs",