diff --git a/kubernetes/k8s_workflow_integration.go b/kubernetes/k8s_workflow_integration.go index af8b484..1733611 100644 --- a/kubernetes/k8s_workflow_integration.go +++ b/kubernetes/k8s_workflow_integration.go @@ -26,13 +26,24 @@ import ( // github.com/serverlessworkflow/sdk-go/model/event.go:51:2: encountered struct field "" without JSON tag in type "Event" // github.com/serverlessworkflow/sdk-go/model/states.go:66:12: unsupported AST kind *ast.InterfaceType +// States should be objects that will be in the same array even if it belongs to +// different types. An issue similar to the below will happen when trying to deploy your custom CR: +// strict decoding error: unknown field "spec.states[0].dataConditions" +// To make the CRD is compliant to the specs there are two options, +// a flat struct with all states fields at the same level, +// or use the // +kubebuilder:pruning:PreserveUnknownFields +// kubebuilder validator and delegate the validation to the sdk-go validator using the admission webhook. +// TODO add a webhook example + // ServerlessWorkflowSpec defines a base API for integration test with operator-sdk type ServerlessWorkflowSpec struct { - BaseWorkflow model.BaseWorkflow `json:"inline"` + BaseWorkflow model.BaseWorkflow `json:",inline"` Events []model.Event `json:"events,omitempty"` Functions []model.Function `json:"functions,omitempty"` Retries []model.Retry `json:"retries,omitempty"` - States []model.State `json:"states"` + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:pruning:PreserveUnknownFields + States []model.State `json:"states"` } // ServerlessWorkflow ... diff --git a/model/function.go b/model/function.go index 385034b..c48dfeb 100644 --- a/model/function.go +++ b/model/function.go @@ -52,9 +52,9 @@ type Function struct { // #. // +kubebuilder:validation:Required Operation string `json:"operation" validate:"required,oneof=rest rpc expression"` - // Defines the function type. Is either `rest`, `rpc`, `expression`, `graphql`, `asyncapi`, `asyncapi` or `asyncapi`. + // Defines the function type. Is either `custom`, `rest`, `rpc`, `expression`, `graphql`, `asyncapi`, `asyncapi` or `asyncapi`. // Default is `rest`. - // +kubebuilder:validation:Enum=rest;rpc;expression;graphql;asyncapi;asyncapi;asyncapi + // +kubebuilder:validation:Enum=rest;rpc;expression;graphql;asyncapi;asyncapi;asyncapi;custom // +kubebuilder:default=rest Type FunctionType `json:"type,omitempty"` // References an auth definition name to be used to access to resource defined in the operation parameter. diff --git a/model/object.go b/model/object.go index 074b3dd..614b396 100644 --- a/model/object.go +++ b/model/object.go @@ -33,10 +33,10 @@ import ( // // +kubebuilder:validation:Type=object type Object struct { - Type Type `json:",inline"` - IntVal int32 `json:",inline"` - StrVal string `json:",inline"` - RawValue json.RawMessage `json:",inline"` + Type Type `json:"type,inline"` + IntVal int32 `json:"intVal,inline"` + StrVal string `json:"strVal,inline"` + RawValue json.RawMessage `json:"rawValue,inline"` } type Type int64 diff --git a/model/retry.go b/model/retry.go index cda444c..6ce8277 100644 --- a/model/retry.go +++ b/model/retry.go @@ -32,6 +32,7 @@ type Retry struct { // Static value by which the delay increases during each attempt (ISO 8601 time format) Increment string `json:"increment,omitempty" validate:"omitempty,iso8601duration"` // Numeric value, if specified the delay between retries is multiplied by this value. + // +optional Multiplier *floatstr.Float32OrString `json:"multiplier,omitempty" validate:"omitempty,min=1"` // Maximum number of retry attempts. // +kubebuilder:validation:Required diff --git a/model/switch_state.go b/model/switch_state.go index 1e87110..dc6a971 100644 --- a/model/switch_state.go +++ b/model/switch_state.go @@ -36,6 +36,36 @@ type SwitchState struct { Timeouts *SwitchStateTimeout `json:"timeouts,omitempty"` } +// DefaultCondition Can be either a transition or end definition +type DefaultCondition struct { + // Serverless workflow states can have one or more incoming and outgoing transitions (from/to other states). + // Each state can define a transition definition that is used to determine which state to transition to next. + // +optional + Transition *Transition `json:"transition,omitempty"` + // If this state an end state + // +optional + End *End `json:"end,omitempty"` +} + +// UnmarshalJSON ... +func (e *DefaultCondition) UnmarshalJSON(data []byte) error { + type defCondUnmarshal DefaultCondition + + obj, str, err := primitiveOrStruct[string, defCondUnmarshal](data) + if err != nil { + return err + } + + if obj == nil { + transition := &Transition{NextState: str} + e.Transition = transition + } else { + *e = DefaultCondition(*obj) + } + + return nil +} + func (s *SwitchState) MarshalJSON() ([]byte, error) { type Alias SwitchState custom, err := json.Marshal(&struct { @@ -48,17 +78,6 @@ func (s *SwitchState) MarshalJSON() ([]byte, error) { return custom, err } -// DefaultCondition Can be either a transition or end definition -type DefaultCondition struct { - // Serverless workflow states can have one or more incoming and outgoing transitions (from/to other states). - // Each state can define a transition definition that is used to determine which state to transition to next. - // +optional - Transition *Transition `json:"transition,omitempty"` - // If this state an end state - // +optional - End *End `json:"end,omitempty"` -} - // SwitchStateTimeout defines the specific timeout settings for switch state type SwitchStateTimeout struct { // Default workflow state execution timeout (ISO 8601 duration format) @@ -107,5 +126,5 @@ type DataCondition struct { // Explicit transition to end End *End `json:"end" validate:"omitempty"` // Workflow transition if condition is evaluated to true - Transition *Transition `json:"transition" validate:"omitempty"` + Transition *Transition `json:"transition,omitempty" validate:"omitempty"` } diff --git a/model/workflow.go b/model/workflow.go index 0c0fa34..354d357 100644 --- a/model/workflow.go +++ b/model/workflow.go @@ -86,6 +86,7 @@ type BaseWorkflow struct { DataInputSchema *DataInputSchema `json:"dataInputSchema,omitempty"` // Serverless Workflow schema version // +kubebuilder:validation:Required + // +kubebuilder:default="0.8" SpecVersion string `json:"specVersion" validate:"required"` // Secrets allow you to access sensitive information, such as passwords, OAuth tokens, ssh keys, etc, // inside your Workflow Expressions. @@ -501,26 +502,19 @@ type Transition struct { } // UnmarshalJSON ... -func (t *Transition) UnmarshalJSON(data []byte) error { - transitionMap := make(map[string]json.RawMessage) - if err := json.Unmarshal(data, &transitionMap); err != nil { - t.NextState, err = unmarshalString(data) - if err != nil { - return err - } - return nil - } +func (e *Transition) UnmarshalJSON(data []byte) error { + type defTransitionUnmarshal Transition - if err := unmarshalKey("compensate", transitionMap, &t.Compensate); err != nil { - return err - } - if err := unmarshalKey("produceEvents", transitionMap, &t.ProduceEvents); err != nil { - return err - } - if err := unmarshalKey("nextState", transitionMap, &t.NextState); err != nil { + obj, str, err := primitiveOrStruct[string, defTransitionUnmarshal](data) + if err != nil { return err } + if obj == nil { + e.NextState = str + } else { + *e = Transition(*obj) + } return nil } diff --git a/parser/parser_test.go b/parser/parser_test.go index a11106f..27d430d 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -16,6 +16,7 @@ package parser import ( "encoding/json" + "fmt" "os" "path/filepath" "strings" @@ -556,6 +557,14 @@ func TestFromFile(t *testing.T) { 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, true, w.States[9].End.Terminate) + + // switch state with DefaultCondition as string + assert.NotEmpty(t, w.States[10].SwitchState) + assert.Equal(t, "HelloStateWithDefaultConditionString", w.States[10].Name) + assert.Equal(t, "${ true }", w.States[10].SwitchState.DataConditions[0].Condition) + assert.Equal(t, "HandleApprovedVisa", w.States[10].SwitchState.DataConditions[0].Transition.NextState) + assert.Equal(t, "SendTextForHighPriority", w.States[10].SwitchState.DefaultCondition.Transition.NextState) + assert.Equal(t, true, w.States[10].End.Terminate) }, }, } @@ -815,7 +824,17 @@ states: single: PT20S defaultCondition: transition: - nextState: CheckCreditCallback + nextState: HelloStateWithDefaultConditionString +- name: HelloStateWithDefaultConditionString + type: switch + dataConditions: + - condition: ${ true } + transition: + nextState: HandleApprovedVisa + - condition: ${ false } + transition: + nextState: HandleRejectedVisa + defaultCondition: SendTextForHighPriority - name: SendTextForHighPriority type: foreach inputCollection: "${ .messages }" @@ -911,6 +930,7 @@ states: terminate: true `)) assert.Nil(t, err) + fmt.Println(err) assert.NotNil(t, workflow) b, err := json.Marshal(workflow) @@ -936,7 +956,10 @@ states: 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\"}}")) + assert.True(t, strings.Contains(string(b), "{\"name\":\"CheckVisaStatusSwitchEventBased\",\"type\":\"switch\",\"defaultCondition\":{\"transition\":{\"nextState\":\"HelloStateWithDefaultConditionString\"}},\"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\"}}")) + + // Switch State with string DefaultCondition + assert.True(t, strings.Contains(string(b), "{\"name\":\"HelloStateWithDefaultConditionString\",\"type\":\"switch\",\"defaultCondition\":{\"transition\":{\"nextState\":\"SendTextForHighPriority\"}},\"eventConditions\":null,\"dataConditions\":[{\"condition\":\"${ true }\",\"end\":null,\"transition\":{\"nextState\":\"HandleApprovedVisa\"}},{\"condition\":\"${ false }\",\"end\":null,\"transition\":{\"nextState\":\"HandleRejectedVisa\"}}]}")) // Foreach State 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\"}}")) @@ -973,7 +996,7 @@ states: nextState: HandleRejectedVisa defaultCondition: transition: - nextState: HandleNoVisaDecision + nextState: HandleApprovedVisa - name: HandleApprovedVisa type: operation actions: diff --git a/parser/testdata/workflows/greetings-v08-spec.sw.yaml b/parser/testdata/workflows/greetings-v08-spec.sw.yaml index 71800b0..13b0d75 100644 --- a/parser/testdata/workflows/greetings-v08-spec.sw.yaml +++ b/parser/testdata/workflows/greetings-v08-spec.sw.yaml @@ -211,3 +211,13 @@ states: single: PT200S end: terminate: true + - name: HelloStateWithDefaultConditionString + type: switch + dataConditions: + - condition: ${ true } + transition: HandleApprovedVisa + - condition: ${ false } + transition: + nextState: HandleRejectedVisa + defaultCondition: SendTextForHighPriority + end: true \ No newline at end of file