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/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..fdf70d8 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)