diff --git a/README.md b/README.md index 64ead0c..0bcad4f 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Current status of features implemented in the SDK is listed in the table below: | [v1.0.0](https://github.com/serverlessworkflow/sdk-go/releases/tag/v1.0.0) | [v0.5](https://github.com/serverlessworkflow/specification/tree/0.5.x) | | [v2.0.1](https://github.com/serverlessworkflow/sdk-go/releases/tag/v2.0.1) | [v0.6](https://github.com/serverlessworkflow/specification/tree/0.6.x) | | [v2.1.2](https://github.com/serverlessworkflow/sdk-go/releases/tag/v2.1.2) | [v0.7](https://github.com/serverlessworkflow/specification/tree/0.7.x) | -| [v2.2.3](https://github.com/serverlessworkflow/sdk-go/releases/tag/v2.2.2) | [v0.8](https://github.com/serverlessworkflow/specification/tree/0.8.x) | +| [v2.2.4](https://github.com/serverlessworkflow/sdk-go/releases/tag/v2.2.4) | [v0.8](https://github.com/serverlessworkflow/specification/tree/0.8.x) | ## How to use diff --git a/code-of-conduct.md b/code-of-conduct.md index ddd14b6..97a8526 100644 --- a/code-of-conduct.md +++ b/code-of-conduct.md @@ -1,58 +1,11 @@ -## CNCF Community Code of Conduct v1.0 +# Code of Conduct -Other languages available: -- [Chinese/中文](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/zh.md) -- [German/Deutsch](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/de.md) -- [Spanish/Español](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/es.md) -- [French/Français](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/fr.md) -- [Italian/Italiano](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/it.md) -- [Japanese/日本語](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/jp.md) -- [Korean/한국어](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/ko.md) -- [Ukrainian/Українська](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/uk.md) -- [Russian/Русский](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/ru.md) -- [Portuguese/Português](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/pt.md) -- [Arabic/العربية](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/ar.md) -- [Polish/Polski](https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/pl.md) +We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). -### Contributor Code of Conduct - -As contributors and maintainers of this project, and in the interest of fostering -an open and welcoming community, we pledge to respect all people who contribute -through reporting issues, posting feature requests, updating documentation, -submitting pull requests or patches, and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, -sexual orientation, disability, personal appearance, body size, race, ethnicity, age, -religion, or nationality. - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing others' private information, such as physical or electronic addresses, - without explicit permission -* Other unethical or unprofessional conduct. - -Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are not -aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers -commit themselves to fairly and consistently applying these principles to every aspect -of managing this project. Project maintainers who do not follow or enforce the Code of -Conduct may be permanently removed from the project team. - -This code of conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior in Kubernetes may be reported by contacting the [Kubernetes Code of Conduct Committee](https://git.k8s.io/community/committee-code-of-conduct) via conduct@kubernetes.io. For other projects, please contact a CNCF project maintainer or our mediator, Mishi Choudhary via mishi@linux.com. - -This Code of Conduct is adapted from the Contributor Covenant -(), version 1.2.0, available at - - -### CNCF Events Code of Conduct - -CNCF events are governed by the Linux Foundation [Code of Conduct](https://events.linuxfoundation.org/code-of-conduct/) available on the event page. -This is designed to be compatible with the above policy and also includes more details on responding to incidents. \ No newline at end of file + +Please contact the [CNCF Code of Conduct Committee](mailto:conduct@cncf.io) +in order to report violations of the Code of Conduct. diff --git a/go.mod b/go.mod index ea25056..fcbcf95 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/go-playground/validator/v10 v10.11.1 github.com/pkg/errors v0.9.1 + github.com/relvacode/iso8601 v1.3.0 github.com/senseyeio/duration v0.0.0-20180430131211-7c2a214ada46 github.com/stretchr/testify v1.8.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index e2bb434..84f4c23 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko= +github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= diff --git a/model/object.go b/model/object.go index 10f4395..b8360a7 100644 --- a/model/object.go +++ b/model/object.go @@ -15,9 +15,23 @@ package model import ( + "bytes" "encoding/json" "fmt" "math" + "strconv" +) + +type Type int8 + +const ( + Null Type = iota + String + Int + Float + Map + Slice + Bool ) // Object is used to allow integration with DeepCopy tool by replacing 'interface' generic type. @@ -29,80 +43,167 @@ import ( // - String - holds string values // - Integer - holds int32 values, JSON marshal any number to float64 by default, during the marshaling process it is // parsed to int32 -// - raw - holds any not typed value, replaces the interface{} behavior. // // +kubebuilder:validation:Type=object type Object struct { - Type Type `json:"type,inline"` - IntVal int32 `json:"intVal,inline"` - StrVal string `json:"strVal,inline"` - RawValue json.RawMessage `json:"rawValue,inline"` - BoolValue bool `json:"boolValue,inline"` + Type Type `json:"type,inline"` + StringValue string `json:"strVal,inline"` + IntValue int32 `json:"intVal,inline"` + FloatValue float64 + MapValue map[string]Object + SliceValue []Object + BoolValue bool `json:"boolValue,inline"` } -type Type int64 +// UnmarshalJSON implements json.Unmarshaler +func (obj *Object) UnmarshalJSON(data []byte) error { + data = bytes.TrimSpace(data) -const ( - Integer Type = iota - String - Raw - Boolean -) + if data[0] == '"' { + obj.Type = String + return json.Unmarshal(data, &obj.StringValue) + } else if data[0] == 't' || data[0] == 'f' { + obj.Type = Bool + return json.Unmarshal(data, &obj.BoolValue) + } else if data[0] == 'n' { + obj.Type = Null + return nil + } else if data[0] == '{' { + obj.Type = Map + return json.Unmarshal(data, &obj.MapValue) + } else if data[0] == '[' { + obj.Type = Slice + return json.Unmarshal(data, &obj.SliceValue) + } + + number := string(data) + intValue, err := strconv.ParseInt(number, 10, 32) + if err == nil { + obj.Type = Int + obj.IntValue = int32(intValue) + return nil + } + + floatValue, err := strconv.ParseFloat(number, 64) + if err == nil { + obj.Type = Float + obj.FloatValue = floatValue + return nil + } + + return fmt.Errorf("json invalid number %q", number) +} + +// MarshalJSON marshal the given json object into the respective Object subtype. +func (obj Object) MarshalJSON() ([]byte, error) { + switch obj.Type { + case String: + return []byte(fmt.Sprintf(`%q`, obj.StringValue)), nil + case Int: + return []byte(fmt.Sprintf(`%d`, obj.IntValue)), nil + case Float: + return []byte(fmt.Sprintf(`%f`, obj.FloatValue)), nil + case Map: + return json.Marshal(obj.MapValue) + case Slice: + return json.Marshal(obj.SliceValue) + case Bool: + return []byte(fmt.Sprintf(`%t`, obj.BoolValue)), nil + case Null: + return []byte("null"), nil + default: + panic("object invalid type") + } +} + +func FromString(val string) Object { + return Object{Type: String, StringValue: val} +} func FromInt(val int) Object { if val > math.MaxInt32 || val < math.MinInt32 { fmt.Println(fmt.Errorf("value: %d overflows int32", val)) } - return Object{Type: Integer, IntVal: int32(val)} + return Object{Type: Int, IntValue: int32(val)} } -func FromString(val string) Object { - return Object{Type: String, StrVal: val} +func FromFloat(val float64) Object { + if val > math.MaxFloat64 || val < -math.MaxFloat64 { + fmt.Println(fmt.Errorf("value: %f overflows float64", val)) + } + return Object{Type: Float, FloatValue: float64(val)} } -func FromBool(val bool) Object { - return Object{Type: Boolean, BoolValue: val} +func FromMap(mapValue map[string]any) Object { + mapValueObject := make(map[string]Object, len(mapValue)) + for key, value := range mapValue { + mapValueObject[key] = FromInterface(value) + } + return Object{Type: Map, MapValue: mapValueObject} } -func FromRaw(val interface{}) Object { - custom, err := json.Marshal(val) - if err != nil { - er := fmt.Errorf("failed to parse value to Raw: %w", err) - fmt.Println(er.Error()) - return Object{} +func FromSlice(sliceValue []any) Object { + sliceValueObject := make([]Object, len(sliceValue)) + for key, value := range sliceValue { + sliceValueObject[key] = FromInterface(value) } - return Object{Type: Raw, RawValue: custom} + return Object{Type: Slice, SliceValue: sliceValueObject} } -// UnmarshalJSON implements json.Unmarshaler -func (obj *Object) UnmarshalJSON(data []byte) error { - if data[0] == '"' { - obj.Type = String - return json.Unmarshal(data, &obj.StrVal) - } else if data[0] == 't' || data[0] == 'f' { - obj.Type = Boolean - return json.Unmarshal(data, &obj.BoolValue) - } else if data[0] == '{' { - obj.Type = Raw - return json.Unmarshal(data, &obj.RawValue) +func FromBool(val bool) Object { + return Object{Type: Bool, BoolValue: val} +} + +func FromNull() Object { + return Object{Type: Null} +} + +func FromInterface(value any) Object { + switch v := value.(type) { + case string: + return FromString(v) + case int: + return FromInt(v) + case int32: + return FromInt(int(v)) + case float64: + return FromFloat(v) + case map[string]any: + return FromMap(v) + case []any: + return FromSlice(v) + case bool: + return FromBool(v) + case nil: + return FromNull() } - obj.Type = Integer - return json.Unmarshal(data, &obj.IntVal) + panic("invalid type") } -// MarshalJSON marshal the given json object into the respective Object subtype. -func (obj Object) MarshalJSON() ([]byte, error) { - switch obj.Type { +func ToInterface(object Object) any { + switch object.Type { case String: - return []byte(fmt.Sprintf(`%q`, obj.StrVal)), nil - case Boolean: - return []byte(fmt.Sprintf(`%t`, obj.BoolValue)), nil - case Integer: - return []byte(fmt.Sprintf(`%d`, obj.IntVal)), nil - case Raw: - val, _ := json.Marshal(obj.RawValue) - return val, nil - default: - return []byte(fmt.Sprintf("%+v", obj)), nil + return object.StringValue + case Int: + return object.IntValue + case Float: + return object.FloatValue + case Map: + mapInterface := make(map[string]any, len(object.MapValue)) + for key, value := range object.MapValue { + mapInterface[key] = ToInterface(value) + } + return mapInterface + case Slice: + sliceInterface := make([]any, len(object.SliceValue)) + for key, value := range object.SliceValue { + sliceInterface[key] = ToInterface(value) + } + return sliceInterface + case Bool: + return object.BoolValue + case Null: + return nil } + panic("invalid type") } diff --git a/model/object_test.go b/model/object_test.go new file mode 100644 index 0000000..0cf928f --- /dev/null +++ b/model/object_test.go @@ -0,0 +1,181 @@ +// 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 ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_unmarshal(t *testing.T) { + testCases := []struct { + name string + json string + object Object + any any + err string + }{ + { + name: "string", + json: "\"value\"", + object: FromString("value"), + any: any("value"), + }, + { + name: "int", + json: "123", + object: FromInt(123), + any: any(int32(123)), + }, + { + name: "float", + json: "123.123", + object: FromFloat(123.123), + any: any(123.123), + }, + { + name: "map", + json: "{\"key\": \"value\", \"key2\": 123}", + object: FromMap(map[string]any{"key": "value", "key2": 123}), + any: any(map[string]any{"key": "value", "key2": int32(123)}), + }, + { + name: "slice", + json: "[\"key\", 123]", + object: FromSlice([]any{"key", 123}), + any: any([]any{"key", int32(123)}), + }, + { + name: "bool true", + json: "true", + object: FromBool(true), + any: any(true), + }, + { + name: "bool false", + json: "false", + object: FromBool(false), + any: any(false), + }, + { + name: "null", + json: "null", + object: FromNull(), + any: nil, + }, + { + name: "string invalid", + json: "\"invalid", + err: "unexpected end of JSON input", + }, + { + name: "number invalid", + json: "123a", + err: "invalid character 'a' after top-level value", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + o := Object{} + err := json.Unmarshal([]byte(tc.json), &o) + if tc.err == "" { + assert.NoError(t, err) + assert.Equal(t, tc.object, o) + assert.Equal(t, ToInterface(tc.object), tc.any) + } else { + assert.Equal(t, tc.err, err.Error()) + } + }) + } +} + +func Test_marshal(t *testing.T) { + testCases := []struct { + name string + json string + object Object + err string + }{ + { + name: "string", + json: "\"value\"", + object: FromString("value"), + }, + { + name: "int", + json: "123", + object: FromInt(123), + }, + { + name: "float", + json: "123.123000", + object: FromFloat(123.123), + }, + { + name: "map", + json: "{\"key\":\"value\",\"key2\":123}", + object: FromMap(map[string]any{"key": "value", "key2": 123}), + }, + { + name: "slice", + json: "[\"key\",123]", + object: FromSlice([]any{"key", 123}), + }, + { + name: "bool true", + json: "true", + object: FromBool(true), + }, + { + name: "bool false", + json: "false", + object: FromBool(false), + }, + { + name: "null", + json: "null", + object: FromNull(), + }, + { + name: "interface", + json: "[\"value\",123,123.123000,[1],{\"key\":1.100000},true,false,null]", + object: FromInterface([]any{ + "value", + 123, + 123.123, + []any{1}, + map[string]any{"key": 1.1}, + true, + false, + nil, + }), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + json, err := json.Marshal(tc.object) + if tc.err == "" { + assert.NoError(t, err) + assert.Equal(t, tc.json, string(json)) + } else { + assert.Equal(t, tc.err, err.Error()) + } + }) + } +} diff --git a/model/workflow.go b/model/workflow.go index 58b382a..3fddfb8 100644 --- a/model/workflow.go +++ b/model/workflow.go @@ -75,6 +75,7 @@ func (i ExpressionLangType) KindValues() []string { return []string{ string(JqExpressionLang), string(JsonPathExpressionLang), + string(CELExpressionLang), } } @@ -88,6 +89,9 @@ const ( // JsonPathExpressionLang ... JsonPathExpressionLang ExpressionLangType = "jsonpath" + + // CELExpressionLang + CELExpressionLang ExpressionLangType = "cel" ) // BaseWorkflow describes the partial Workflow definition that does not rely on generic interfaces @@ -132,7 +136,7 @@ type BaseWorkflow struct { // +optional Constants *Constants `json:"constants,omitempty"` // Identifies the expression language used for workflow expressions. Default is 'jq'. - // +kubebuilder:validation:Enum=jq;jsonpath + // +kubebuilder:validation:Enum=jq;jsonpath;cel // +kubebuilder:default=jq // +optional ExpressionLang ExpressionLangType `json:"expressionLang,omitempty" validate:"required,oneofkind"` @@ -372,7 +376,7 @@ type Cron struct { Expression string `json:"expression" validate:"required"` // Specific date and time (ISO 8601 format) when the cron expression is no longer valid. // +optional - ValidUntil string `json:"validUntil,omitempty" validate:"omitempty,iso8601duration"` + ValidUntil string `json:"validUntil,omitempty" validate:"omitempty,iso8601datetime"` } type cronUnmarshal Cron @@ -503,7 +507,7 @@ type DataInputSchema struct { // +kubebuilder:validation:Required Schema string `json:"schema" validate:"required"` // +kubebuilder:validation:Required - FailOnValidationErrors bool `json:"failOnValidationErrors" validate:"required"` + FailOnValidationErrors bool `json:"failOnValidationErrors"` } type dataInputSchemaUnmarshal DataInputSchema diff --git a/model/workflow_validator_test.go b/model/workflow_validator_test.go index 10e935a..9cdb77e 100644 --- a/model/workflow_validator_test.go +++ b/model/workflow_validator_test.go @@ -162,7 +162,7 @@ workflow.key required when "workflow.id" is not defined`, model.BaseWorkflow.ExpressionLang = JqExpressionLang + "invalid" return *model }, - Err: `workflow.expressionLang need by one of [jq jsonpath]`, + Err: `workflow.expressionLang need by one of [jq jsonpath cel]`, }, } @@ -417,6 +417,39 @@ Key: 'Workflow.States[3].BaseState.Transition.NextState' Error:Field validation StructLevelValidationCtx(t, testCases) } +func TestDataInputSchemaStructLevelValidation(t *testing.T) { + baseWorkflow := buildWorkflow() + + operationState := buildOperationState(baseWorkflow, "start state") + buildEndByState(operationState, true, false) + action1 := buildActionByOperationState(operationState, "action 1") + buildFunctionRef(baseWorkflow, action1, "function 1") + + testCases := []ValidationCase{ + { + Desp: "empty DataInputSchema", + Model: func() Workflow { + model := baseWorkflow.DeepCopy() + model.DataInputSchema = &DataInputSchema{} + return *model + }, + Err: `workflow.dataInputSchema.schema is required`, + }, + { + Desp: "filled Schema, default failOnValidationErrors", + Model: func() Workflow { + model := baseWorkflow.DeepCopy() + model.DataInputSchema = &DataInputSchema{ + Schema: "sample schema", + } + return *model + }, + }, + } + + StructLevelValidationCtx(t, testCases) +} + func TestSecretsStructLevelValidation(t *testing.T) { baseWorkflow := buildWorkflow() diff --git a/model/zz_generated.deepcopy.go b/model/zz_generated.deepcopy.go index 804706f..3e76ab1 100644 --- a/model/zz_generated.deepcopy.go +++ b/model/zz_generated.deepcopy.go @@ -1101,10 +1101,19 @@ func (in *OAuth2AuthProperties) DeepCopy() *OAuth2AuthProperties { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Object) DeepCopyInto(out *Object) { *out = *in - if in.RawValue != nil { - in, out := &in.RawValue, &out.RawValue - *out = make(json.RawMessage, len(*in)) - copy(*out, *in) + if in.MapValue != nil { + in, out := &in.MapValue, &out.MapValue + *out = make(map[string]Object, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.SliceValue != nil { + in, out := &in.SliceValue, &out.SliceValue + *out = make([]Object, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } diff --git a/parser/parser_test.go b/parser/parser_test.go index c5cf0f0..91dc273 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -455,13 +455,21 @@ func TestFromFile(t *testing.T) { assert.Equal(t, "visaApprovedEvent", w.States[3].EventConditions[0].Name) assert.Equal(t, "visaApprovedEventRef", w.States[3].EventConditions[0].EventRef) assert.Equal(t, "HandleApprovedVisa", w.States[3].EventConditions[0].Transition.NextState) - assert.Equal(t, model.Metadata{"mastercard": model.Object{Type: 1, IntVal: 0, StrVal: "disallowed", RawValue: json.RawMessage(nil)}, - "visa": model.Object{Type: 1, IntVal: 0, StrVal: "allowed", RawValue: json.RawMessage(nil)}}, - w.States[3].EventConditions[0].Metadata) + assert.Equal(t, + model.Metadata{ + "mastercard": model.FromString("disallowed"), + "visa": model.FromString("allowed"), + }, + w.States[3].EventConditions[0].Metadata, + ) assert.Equal(t, "visaRejectedEvent", w.States[3].EventConditions[1].EventRef) assert.Equal(t, "HandleRejectedVisa", w.States[3].EventConditions[1].Transition.NextState) - assert.Equal(t, model.Metadata{"test": model.Object{Type: 1, IntVal: 0, StrVal: "tested", RawValue: json.RawMessage(nil)}}, - w.States[3].EventConditions[1].Metadata) + assert.Equal(t, + model.Metadata{ + "test": model.FromString("tested"), + }, + w.States[3].EventConditions[1].Metadata, + ) assert.Equal(t, "PT1H", w.States[3].SwitchState.Timeouts.EventTimeout) assert.Equal(t, "PT1S", w.States[3].SwitchState.Timeouts.StateExecTimeout.Total) assert.Equal(t, "PT2S", w.States[3].SwitchState.Timeouts.StateExecTimeout.Single) @@ -534,8 +542,14 @@ func TestFromFile(t *testing.T) { assert.Equal(t, "CheckCreditCallback", w.States[8].Name) assert.Equal(t, model.StateTypeCallback, w.States[8].Type) assert.Equal(t, "callCreditCheckMicroservice", w.States[8].CallbackState.Action.FunctionRef.RefName) - assert.Equal(t, map[string]model.Object{"argsObj": model.FromRaw(map[string]interface{}{"age": 10, "name": "hi"}), "customer": model.FromString("${ .customer }"), "time": model.FromInt(48)}, - w.States[8].CallbackState.Action.FunctionRef.Arguments) + assert.Equal(t, + map[string]model.Object{ + "argsObj": model.FromMap(map[string]interface{}{"age": 10, "name": "hi"}), + "customer": model.FromString("${ .customer }"), + "time": model.FromInt(48), + }, + w.States[8].CallbackState.Action.FunctionRef.Arguments, + ) assert.Equal(t, "PT10S", w.States[8].CallbackState.Action.Sleep.Before) assert.Equal(t, "PT20S", w.States[8].CallbackState.Action.Sleep.After) assert.Equal(t, "PT150M", w.States[8].CallbackState.Timeouts.ActionExecTimeout) @@ -565,6 +579,13 @@ func TestFromFile(t *testing.T) { assert.Equal(t, "SendTextForHighPriority", w.States[10].SwitchState.DefaultCondition.Transition.NextState) assert.Equal(t, true, w.States[10].End.Terminate) }, + }, { + "./testdata/workflows/dataInputSchemaValidation.yaml", func(t *testing.T, w *model.Workflow) { + assert.NotNil(t, w.DataInputSchema) + + assert.Equal(t, "sample schema", w.DataInputSchema.Schema) + assert.Equal(t, false, w.DataInputSchema.FailOnValidationErrors) + }, }, } for _, file := range files { diff --git a/parser/testdata/workflows/dataInputSchemaValidation.yaml b/parser/testdata/workflows/dataInputSchemaValidation.yaml new file mode 100644 index 0000000..ed685a6 --- /dev/null +++ b/parser/testdata/workflows/dataInputSchemaValidation.yaml @@ -0,0 +1,28 @@ +# 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. + +id: Valid DataInputSchema +version: '1.0' +specVersion: '0.8' +start: Start +dataInputSchema: + failOnValidationErrors: false + schema: "sample schema" +states: +- name: Start + type: inject + data: + done: true + end: + terminate: true \ No newline at end of file diff --git a/validator/validator.go b/validator/validator.go index 1e77b36..c2ae024 100644 --- a/validator/validator.go +++ b/validator/validator.go @@ -18,6 +18,7 @@ import ( "context" "strconv" + "github.com/relvacode/iso8601" "github.com/senseyeio/duration" "k8s.io/apimachinery/pkg/util/intstr" @@ -41,6 +42,11 @@ func init() { panic(err) } + err = validate.RegisterValidationCtx("iso8601datetime", validateISO8601DatetimeFunc) + if err != nil { + panic(err) + } + err = validate.RegisterValidation("oneofkind", oneOfKind) if err != nil { panic(err) @@ -63,6 +69,17 @@ func validateISO8601TimeDurationFunc(_ context.Context, fl validator.FieldLevel) return err == nil } +// ValidateISO8601Datetime validate the string is iso8601 Datetime format +func ValidateISO8601Datetime(s string) error { + _, err := iso8601.ParseString(s) + return err +} + +func validateISO8601DatetimeFunc(_ context.Context, fl validator.FieldLevel) bool { + err := ValidateISO8601Datetime(fl.Field().String()) + return err == nil +} + func oneOfKind(fl validator.FieldLevel) bool { if val, ok := fl.Field().Interface().(Kind); ok { for _, value := range val.KindValues() { diff --git a/validator/validator_test.go b/validator/validator_test.go index 73ef555..8dd6c9c 100644 --- a/validator/validator_test.go +++ b/validator/validator_test.go @@ -59,6 +59,54 @@ func TestValidateISO8601TimeDuration(t *testing.T) { } } +func TestValidateISO8601Timestamp(t *testing.T) { + type testCase struct { + desp string + s string + err string + } + testCases := []testCase{ + { + desp: "workflow_spec_example", + s: "2021-11-05T08:15:30-05:00", + err: ``, + }, + { + desp: "datetime", + s: "2023-09-08T20:15:46+00:00", + err: ``, + }, + { + desp: "date", + s: "2023-09-08", + err: ``, + }, + { + desp: "time", + s: "13:15:33.074-07:00", + err: "iso8601: Unexpected character `:`", + }, + { + desp: "empty value", + s: "", + err: `iso8601: Cannot parse "": month 0 is not in range 1-12`, + }, + } + for _, tc := range testCases { + t.Run(tc.desp, func(t *testing.T) { + err := ValidateISO8601Datetime(tc.s) + + if tc.err != "" { + assert.Error(t, err) + assert.Regexp(t, tc.err, err) + return + } + + assert.NoError(t, err) + }) + } +} + type testKind string func (k testKind) KindValues() []string { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy