diff --git a/Makefile b/Makefile index 0b0833f..06fde64 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ lint: .PHONY: test coverage="false" + test: deepcopy buildergen make lint @go test ./... diff --git a/builder/builder_test.go b/builder/builder_test.go index 47a13d2..97b8c84 100644 --- a/builder/builder_test.go +++ b/builder/builder_test.go @@ -17,6 +17,7 @@ package builder import ( "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/serverlessworkflow/sdk-go/v2/model" @@ -58,7 +59,13 @@ func TestValidate(t *testing.T) { state2.End().Terminate(true) err = Validate(state2.Build()) if assert.Error(t, err) { - assert.Equal(t, "state.name is required", err.(val.WorkflowErrors)[0].Error()) + var workflowErrors val.WorkflowErrors + if errors.As(err, &workflowErrors) { + assert.Equal(t, "state.name is required", workflowErrors[0].Error()) + } else { + // Handle other error types if necessary + t.Errorf("Unexpected error: %v", err) + } } } diff --git a/model/workflow.go b/model/workflow.go index 8f7f032..aa72d1f 100644 --- a/model/workflow.go +++ b/model/workflow.go @@ -15,7 +15,9 @@ package model import ( + "bytes" "encoding/json" + "errors" "github.com/serverlessworkflow/sdk-go/v2/util" ) @@ -121,7 +123,7 @@ type BaseWorkflow struct { // qualities. // +optional Annotations []string `json:"annotations,omitempty"` - // DataInputSchema URI of the JSON Schema used to validate the workflow data input + // DataInputSchema URI or Object of the JSON Schema used to validate the workflow data input // +optional DataInputSchema *DataInputSchema `json:"dataInputSchema,omitempty"` // Serverless Workflow schema version @@ -225,6 +227,7 @@ func (w *Workflow) UnmarshalJSON(data []byte) error { return nil } +// States ... // +kubebuilder:validation:MinItems=1 type States []State @@ -510,7 +513,7 @@ type StateDataFilter struct { // +builder-gen:new-call=ApplyDefault type DataInputSchema struct { // +kubebuilder:validation:Required - Schema string `json:"schema" validate:"required"` + Schema *Object `json:"schema" validate:"required"` // +kubebuilder:validation:Required FailOnValidationErrors bool `json:"failOnValidationErrors"` } @@ -520,7 +523,41 @@ type dataInputSchemaUnmarshal DataInputSchema // UnmarshalJSON implements json.Unmarshaler func (d *DataInputSchema) UnmarshalJSON(data []byte) error { d.ApplyDefault() - return util.UnmarshalPrimitiveOrObject("dataInputSchema", data, &d.Schema, (*dataInputSchemaUnmarshal)(d)) + + // expected: data = "{\"key\": \"value\"}" + // data = {"key": "value"} + // data = "file://..." + // data = { "schema": "{\"key\": \"value\"}", "failOnValidationErrors": true } + // data = { "schema": {"key": "value"}, "failOnValidationErrors": true } + // data = { "schema": "file://...", "failOnValidationErrors": true } + + schemaString := "" + err := util.UnmarshalPrimitiveOrObject("dataInputSchema", data, &schemaString, (*dataInputSchemaUnmarshal)(d)) + if err != nil { + return err + } + + if d.Schema != nil { + if d.Schema.Type == Map { + return nil + + } else if d.Schema.Type == String { + schemaString = d.Schema.StringValue + + } else { + return errors.New("invalid dataInputSchema must be a string or object") + } + } + + if schemaString != "" { + data = []byte(schemaString) + if bytes.TrimSpace(data)[0] != '{' { + data = []byte("\"" + schemaString + "\"") + } + } + + d.Schema = new(Object) + return util.UnmarshalObjectOrFile("schema", data, &d.Schema) } // ApplyDefault set the default values for Data Input Schema diff --git a/model/workflow_test.go b/model/workflow_test.go index 352a751..a5aa42a 100644 --- a/model/workflow_test.go +++ b/model/workflow_test.go @@ -498,6 +498,13 @@ func TestTransitionUnmarshalJSON(t *testing.T) { } func TestDataInputSchemaUnmarshalJSON(t *testing.T) { + + var schemaName Object + err := json.Unmarshal([]byte("{\"key\": \"value\"}"), &schemaName) + if !assert.NoError(t, err) { + return + } + type testCase struct { desp string data string @@ -508,39 +515,58 @@ func TestDataInputSchemaUnmarshalJSON(t *testing.T) { testCases := []testCase{ { desp: "string success", - data: `"schema name"`, + data: "{\"key\": \"value\"}", expect: DataInputSchema{ - Schema: "schema name", + Schema: &schemaName, FailOnValidationErrors: true, }, err: ``, }, { - desp: `object success`, - data: `{"schema": "schema name"}`, + desp: "string fail", + data: "{\"key\": }", expect: DataInputSchema{ - Schema: "schema name", + Schema: &schemaName, + FailOnValidationErrors: true, + }, + err: `invalid character '}' looking for beginning of value`, + }, + { + desp: `object success (without quotes)`, + data: `{"key": "value"}`, + expect: DataInputSchema{ + Schema: &schemaName, FailOnValidationErrors: true, }, err: ``, }, { - desp: `object fail`, - data: `{"schema": "schema name}`, + desp: `schema object success`, + data: `{"schema": "{\"key\": \"value\"}"}`, expect: DataInputSchema{ - Schema: "schema name", + Schema: &schemaName, FailOnValidationErrors: true, }, - err: `unexpected end of JSON input`, + err: ``, }, { - desp: `object key invalid`, - data: `{"schema_invalid": "schema name"}`, + desp: `schema object success (without quotes)`, + data: `{"schema": {"key": "value"}}`, expect: DataInputSchema{ + Schema: &schemaName, FailOnValidationErrors: true, }, err: ``, }, + { + desp: `schema object fail`, + data: `{"schema": "schema name}`, + expect: DataInputSchema{ + Schema: &schemaName, + FailOnValidationErrors: true, + }, + err: `unexpected end of JSON input`, + }, } for _, tc := range testCases { t.Run(tc.desp, func(t *testing.T) { @@ -548,13 +574,14 @@ func TestDataInputSchemaUnmarshalJSON(t *testing.T) { err := json.Unmarshal([]byte(tc.data), &v) if tc.err != "" { - assert.Error(t, err) - assert.Regexp(t, tc.err, err) + assert.Error(t, err, tc.desp) + assert.Regexp(t, tc.err, err, tc.desp) return } - assert.NoError(t, err) - assert.Equal(t, tc.expect, v) + assert.NoError(t, err, tc.desp) + assert.Equal(t, tc.expect.Schema, v.Schema, tc.desp) + assert.Equal(t, tc.expect.FailOnValidationErrors, v.FailOnValidationErrors, tc.desp) }) } } diff --git a/model/workflow_validator_test.go b/model/workflow_validator_test.go index 9cdb77e..2a6b5a0 100644 --- a/model/workflow_validator_test.go +++ b/model/workflow_validator_test.go @@ -425,6 +425,8 @@ func TestDataInputSchemaStructLevelValidation(t *testing.T) { action1 := buildActionByOperationState(operationState, "action 1") buildFunctionRef(baseWorkflow, action1, "function 1") + sampleSchema := FromString("sample schema") + testCases := []ValidationCase{ { Desp: "empty DataInputSchema", @@ -440,13 +442,14 @@ func TestDataInputSchemaStructLevelValidation(t *testing.T) { Model: func() Workflow { model := baseWorkflow.DeepCopy() model.DataInputSchema = &DataInputSchema{ - Schema: "sample schema", + Schema: &sampleSchema, } return *model }, }, } + //fmt.Printf("%+v", testCases[0].Model) StructLevelValidationCtx(t, testCases) } diff --git a/model/zz_generated.buildergen.go b/model/zz_generated.buildergen.go index 9ab7058..42564fe 100644 --- a/model/zz_generated.buildergen.go +++ b/model/zz_generated.buildergen.go @@ -1,11 +1,25 @@ //go:build !ignore_autogenerated // +build !ignore_autogenerated -// Code generated by main. DO NOT EDIT. +// Copyright 2023 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. +// Code generated by builder-gen. DO NOT EDIT. package model import ( + floatstr "github.com/serverlessworkflow/sdk-go/v2/util/floatstr" intstr "k8s.io/apimachinery/pkg/util/intstr" ) @@ -945,12 +959,15 @@ func NewDataInputSchemaBuilder() *DataInputSchemaBuilder { } type DataInputSchemaBuilder struct { - model DataInputSchema + model DataInputSchema + schema *ObjectBuilder } -func (b *DataInputSchemaBuilder) Schema(input string) *DataInputSchemaBuilder { - b.model.Schema = input - return b +func (b *DataInputSchemaBuilder) Schema() *ObjectBuilder { + if b.schema == nil { + b.schema = NewObjectBuilder() + } + return b.schema } func (b *DataInputSchemaBuilder) FailOnValidationErrors(input bool) *DataInputSchemaBuilder { @@ -959,6 +976,10 @@ func (b *DataInputSchemaBuilder) FailOnValidationErrors(input bool) *DataInputSc } func (b *DataInputSchemaBuilder) Build() DataInputSchema { + if b.schema != nil { + schema := b.schema.Build() + b.model.Schema = &schema + } return b.model } @@ -2237,11 +2258,21 @@ func (b *RetryBuilder) Increment(input string) *RetryBuilder { return b } +func (b *RetryBuilder) Multiplier(input *floatstr.Float32OrString) *RetryBuilder { + b.model.Multiplier = input + return b +} + func (b *RetryBuilder) MaxAttempts(input intstr.IntOrString) *RetryBuilder { b.model.MaxAttempts = input return b } +func (b *RetryBuilder) Jitter(input floatstr.Float32OrString) *RetryBuilder { + b.model.Jitter = input + return b +} + func (b *RetryBuilder) Build() Retry { return b.model } diff --git a/model/zz_generated.deepcopy.go b/model/zz_generated.deepcopy.go index 3e76ab1..0fb2566 100644 --- a/model/zz_generated.deepcopy.go +++ b/model/zz_generated.deepcopy.go @@ -223,7 +223,7 @@ func (in *BaseWorkflow) DeepCopyInto(out *BaseWorkflow) { if in.DataInputSchema != nil { in, out := &in.DataInputSchema, &out.DataInputSchema *out = new(DataInputSchema) - **out = **in + (*in).DeepCopyInto(*out) } if in.Secrets != nil { in, out := &in.Secrets, &out.Secrets @@ -568,6 +568,11 @@ func (in *DataCondition) DeepCopy() *DataCondition { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DataInputSchema) DeepCopyInto(out *DataInputSchema) { *out = *in + if in.Schema != nil { + in, out := &in.Schema, &out.Schema + *out = new(Object) + (*in).DeepCopyInto(*out) + } return } diff --git a/parser/parser_test.go b/parser/parser_test.go index faa28b8..8cc3de1 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" @@ -582,8 +583,25 @@ func TestFromFile(t *testing.T) { }, { "./testdata/workflows/dataInputSchemaValidation.yaml", func(t *testing.T, w *model.Workflow) { assert.NotNil(t, w.DataInputSchema) - - assert.Equal(t, "sample schema", w.DataInputSchema.Schema) + expected := model.DataInputSchema{} + data, err := util.LoadExternalResource("file://testdata/datainputschema.json") + err1 := util.UnmarshalObject("schema", data, &expected.Schema) + assert.Nil(t, err) + assert.Nil(t, err1) + assert.Equal(t, expected.Schema, w.DataInputSchema.Schema) + assert.Equal(t, false, w.DataInputSchema.FailOnValidationErrors) + }, + }, { + "./testdata/workflows/dataInputSchemaObject.json", func(t *testing.T, w *model.Workflow) { + assert.NotNil(t, w.DataInputSchema) + expected := model.Object{} + err := json.Unmarshal([]byte("{\"title\": \"Hello World Schema\", \"properties\": {\"person\": "+ + "{\"type\": \"object\",\"properties\": {\"name\": {\"type\": \"string\"}},\"required\": "+ + "[\"name\"]}}, \"required\": [\"person\"]}"), + &expected) + fmt.Printf("err: %s\n", err) + fmt.Printf("schema: %+v\n", expected) + assert.Equal(t, &expected, w.DataInputSchema.Schema) assert.Equal(t, false, w.DataInputSchema.FailOnValidationErrors) }, }, diff --git a/parser/testdata/datainputschema.json b/parser/testdata/datainputschema.json new file mode 100644 index 0000000..bace233 --- /dev/null +++ b/parser/testdata/datainputschema.json @@ -0,0 +1,16 @@ +{ + "title": "Hello World Schema", + "properties": { + "person": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } +} \ No newline at end of file diff --git a/parser/testdata/workflows/dataInputSchemaObject.json b/parser/testdata/workflows/dataInputSchemaObject.json new file mode 100644 index 0000000..7b50c0d --- /dev/null +++ b/parser/testdata/workflows/dataInputSchemaObject.json @@ -0,0 +1,56 @@ +{ + "id": "greeting", + "version": "1.0.0", + "specVersion": "0.8", + "name": "Greeting Workflow", + "description": "Greet Someone", + "start": "Greet", + "dataInputSchema": { + "failOnValidationErrors": false, + "schema": { + "title": "Hello World Schema", + "properties": { + "person": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + }, + "required": [ + "person" + ] + } + }, + "functions": [ + { + "name": "greetingFunction", + "operation": "file://myapis/greetingapis.json#greeting" + } + ], + "states": [ + { + "name": "Greet", + "type": "operation", + "actions": [ + { + "functionRef": { + "refName": "greetingFunction", + "arguments": { + "name": "${ .person.name }" + } + }, + "actionDataFilter": { + "results": "${ {greeting: .greeting} }" + } + } + ], + "end": true + } + ] +} \ No newline at end of file diff --git a/parser/testdata/workflows/dataInputSchemaValidation.yaml b/parser/testdata/workflows/dataInputSchemaValidation.yaml index ed685a6..4bc1e11 100644 --- a/parser/testdata/workflows/dataInputSchemaValidation.yaml +++ b/parser/testdata/workflows/dataInputSchemaValidation.yaml @@ -18,7 +18,7 @@ specVersion: '0.8' start: Start dataInputSchema: failOnValidationErrors: false - schema: "sample schema" + schema: "file://testdata/datainputschema.json" states: - name: Start type: inject diff --git a/util/unmarshal.go b/util/unmarshal.go index 6c70f4a..d00e9d2 100644 --- a/util/unmarshal.go +++ b/util/unmarshal.go @@ -72,6 +72,13 @@ func (e *UnmarshalError) Error() string { func (e *UnmarshalError) unmarshalMessageError(err *json.UnmarshalTypeError) string { if err.Struct == "" && err.Field == "" { primitiveTypeName := e.primitiveType.String() + + // in some cases the e.primitiveType might be invalid, one of the reasons is because it is nil + // default to string in that case + if e.primitiveType == reflect.Invalid { + primitiveTypeName = "string" + } + var objectTypeName string if e.objectType != reflect.Invalid { switch e.objectType { @@ -107,7 +114,7 @@ func (e *UnmarshalError) unmarshalMessageError(err *json.UnmarshalTypeError) str return err.Error() } -func loadExternalResource(url string) (b []byte, err error) { +func LoadExternalResource(url string) (b []byte, err error) { index := strings.Index(url, "://") if index == -1 { b, err = getBytesFromFile(url) @@ -199,7 +206,7 @@ func UnmarshalObjectOrFile[U any](parameterName string, data []byte, valObject * // Assumes that the value inside `data` is a path to a known location. // Returns the content of the file or a not nil error reference. - data, err = loadExternalResource(valString) + data, err = LoadExternalResource(valString) if err != nil { return err } @@ -214,7 +221,7 @@ func UnmarshalObjectOrFile[U any](parameterName string, data []byte, valObject * } data = bytes.TrimSpace(data) - if data[0] == '{' && parameterName != "constants" && parameterName != "timeouts" { + if data[0] == '{' && parameterName != "constants" && parameterName != "timeouts" && parameterName != "schema" { extractData := map[string]json.RawMessage{} err = json.Unmarshal(data, &extractData) if err != nil { diff --git a/util/unmarshal_test.go b/util/unmarshal_test.go index 0227123..f7051fb 100644 --- a/util/unmarshal_test.go +++ b/util/unmarshal_test.go @@ -58,23 +58,23 @@ func Test_loadExternalResource(t *testing.T) { defer server.Close() HttpClient = *server.Client() - data, err := loadExternalResource(server.URL + "/test.json") + data, err := LoadExternalResource(server.URL + "/test.json") assert.NoError(t, err) assert.Equal(t, "{}", string(data)) - data, err = loadExternalResource("parser/testdata/eventdefs.yml") + data, err = LoadExternalResource("parser/testdata/eventdefs.yml") assert.NoError(t, err) assert.Equal(t, "{\"events\":[{\"correlation\":[{\"contextAttributeName\":\"accountId\"}],\"name\":\"PaymentReceivedEvent\",\"source\":\"paymentEventSource\",\"type\":\"payment.receive\"},{\"kind\":\"produced\",\"name\":\"ConfirmationCompletedEvent\",\"type\":\"payment.confirmation\"}]}", string(data)) - data, err = loadExternalResource("file://../parser/testdata/eventdefs.yml") + data, err = LoadExternalResource("file://../parser/testdata/eventdefs.yml") assert.NoError(t, err) assert.Equal(t, "{\"events\":[{\"correlation\":[{\"contextAttributeName\":\"accountId\"}],\"name\":\"PaymentReceivedEvent\",\"source\":\"paymentEventSource\",\"type\":\"payment.receive\"},{\"kind\":\"produced\",\"name\":\"ConfirmationCompletedEvent\",\"type\":\"payment.confirmation\"}]}", string(data)) - data, err = loadExternalResource("./parser/testdata/eventdefs.yml") + data, err = LoadExternalResource("./parser/testdata/eventdefs.yml") assert.NoError(t, err) assert.Equal(t, "{\"events\":[{\"correlation\":[{\"contextAttributeName\":\"accountId\"}],\"name\":\"PaymentReceivedEvent\",\"source\":\"paymentEventSource\",\"type\":\"payment.receive\"},{\"kind\":\"produced\",\"name\":\"ConfirmationCompletedEvent\",\"type\":\"payment.confirmation\"}]}", string(data)) - _, err = loadExternalResource("ftp://test.yml") + _, err = LoadExternalResource("ftp://test.yml") assert.ErrorContains(t, err, "unsupported scheme: \"ftp\"") }