From ff3af1ead185ef7fa8f8492785b7df5b41225063 Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Fri, 9 Aug 2024 08:43:17 -0400 Subject: [PATCH 01/11] adding utils for converting data types --- util.go | 123 ++++++++++++++++++++++++++ util_test.go | 243 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 util.go create mode 100644 util_test.go diff --git a/util.go b/util.go new file mode 100644 index 0000000..2af1b67 --- /dev/null +++ b/util.go @@ -0,0 +1,123 @@ +package jsonserialization + +import ( + "fmt" + "math" + "reflect" +) + +type numericRange struct { + min float64 + max float64 + allowDecimal bool +} + +var ( + numericTypeRanges = map[reflect.Kind]numericRange{ + reflect.Int8: {math.MinInt8, math.MaxInt8, false}, + reflect.Uint8: {0, math.MaxUint8, false}, + reflect.Int16: {math.MinInt16, math.MaxInt16, false}, + reflect.Uint16: {0, math.MaxUint16, false}, + reflect.Int32: {math.MinInt32, math.MaxInt32, false}, + reflect.Uint32: {0, math.MaxUint32, false}, + reflect.Int64: {math.MinInt64, math.MaxInt64, false}, + reflect.Uint64: {0, math.MaxUint64, false}, + reflect.Float32: {-math.MaxFloat32, math.MaxFloat32, true}, + reflect.Float64: {-math.MaxFloat64, math.MaxFloat64, true}, + } +) + +type number interface { + int | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64 | float32 | float64 +} + +// isCompatible checks if the value is compatible with the type tp. +// It intentionally excludes checking if types are pointers to allow for possibility. +func isCompatible(value interface{}, tp reflect.Type) bool { + // Can't join with lower, number types are always "convertible" just not losslessly. + if isNumericType(value) && isNumericType(tp) { + //NOTE: no need to check if number is compatible with another, always yes, just overflows + //Check if number value is TRULY compatible + return isCompatibleInt(value, tp) + } + + return reflect.TypeOf(value).ConvertibleTo(tp) +} + +// as converts the value to the type T. +func as[T any](in interface{}, out T) error { + // No point in trying anything if already nil + if in == nil { + return nil + } + + // Make sure nothing is a pointer + valValue := reflect.ValueOf(in) + for valValue.Kind() == reflect.Ptr { + valValue = valValue.Elem() + in = valValue.Interface() + } + + outVal := reflect.ValueOf(out) + if outVal.Kind() != reflect.Pointer || outVal.IsNil() { + return fmt.Errorf("out is not pointer or is nil") + } + + nestedOutVal := outVal.Elem() + // Handle the case where out is a pointer to an interface + if nestedOutVal.Kind() == reflect.Interface && !nestedOutVal.IsNil() { + nestedOutVal = nestedOutVal.Elem() + } + + outType := nestedOutVal.Type() + + if !isCompatible(in, outType) { + return fmt.Errorf("value '%v' is not compatible with type %T", in, nestedOutVal.Interface()) + } + + outVal.Elem().Set(valValue.Convert(outType)) + return nil +} + +// isNumericType checks if the given type is a numeric type. +func isNumericType(in interface{}) bool { + tp, ok := in.(reflect.Type) + if !ok { + tp = reflect.TypeOf(in) + } + + switch tp.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return true + default: + return false + } +} + +// isCompatibleInt checks if the given value is compatible with the specified integer type. +// It returns true if the value falls within the valid range for the type and has no decimal places. +// Otherwise, it returns false. +func isCompatibleInt(in interface{}, tp reflect.Type) bool { + if !isNumericType(in) || !isNumericType(tp) { + return false + } + + inFloat := reflect.ValueOf(in).Convert(reflect.TypeOf(float64(0))).Float() + hasDecimal := hasDecimalPlace(inFloat) + + if rangeInfo, ok := numericTypeRanges[tp.Kind()]; ok { + if inFloat >= rangeInfo.min && inFloat <= rangeInfo.max { + return rangeInfo.allowDecimal || !hasDecimal + } + } + return false +} + +// hasDecimalPlace checks if the given float64 value has a decimal place. +// It returns true if the fractional part of the value is greater than 0.0 (indicating a decimal). +// Otherwise, it returns false. +func hasDecimalPlace(value float64) bool { + return value != float64(int64(value)) +} diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..dc865e7 --- /dev/null +++ b/util_test.go @@ -0,0 +1,243 @@ +package jsonserialization + +import ( + "errors" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsCompatible(t *testing.T) { + cases := []struct { + Title string + InputVal interface{} + InputType reflect.Type + Expected interface{} + }{ + { + Title: "Valid", + InputVal: int(5), + InputType: reflect.TypeOf(int8(0)), + Expected: true, + }, + { + Title: "Incompatible", + InputVal: "I am a string", + InputType: reflect.TypeOf(int8(0)), + Expected: false, + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + isComp := isCompatible(test.InputVal, test.InputType) + assert.Equal(t, test.Expected, isComp) + }) + } +} + +func TestAs(t *testing.T) { + cases := []struct { + Title string + InputVal []interface{} + Expected interface{} + Error error + }{ + { + Title: "Number", + InputVal: []interface{}{ + int8(1), + int16(0), + }, + Expected: int16(1), + Error: nil, + }, + { + Title: "Interface", + InputVal: []interface{}{ + int8(1), + nil, + }, + Expected: interface{}(int8(1)), + Error: nil, + }, + { + Title: "Incompatible", + InputVal: []interface{}{ + "I am a string", + int8(0), + }, + Expected: int8(0), + Error: errors.New("value 'I am a string' is not compatible with type int8"), + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + in := test.InputVal[0] + out := test.InputVal[1] + err := as(in, &out) + + assert.Equal(t, test.Error, err) + assert.Equal(t, test.Expected, out) + }) + } +} + +func TestIsNumericType(t *testing.T) { + cases := []struct { + Title string + InputVal interface{} + Expected bool + }{ + { + Title: "Int", + InputVal: int(1), + Expected: true, + }, + { + Title: "int8", + InputVal: int8(1), + Expected: true, + }, + { + Title: "uint8", + InputVal: uint8(1), + Expected: true, + }, + { + Title: "int16", + InputVal: int16(1), + Expected: true, + }, + { + Title: "uint16", + InputVal: uint16(1), + Expected: true, + }, + { + Title: "int32", + InputVal: int32(1), + Expected: true, + }, + { + Title: "uint32", + InputVal: uint32(1), + Expected: true, + }, + { + Title: "int64", + InputVal: int64(1), + Expected: true, + }, + { + Title: "uint64", + InputVal: uint64(1), + Expected: true, + }, + { + Title: "float32", + InputVal: float32(1.1), + Expected: true, + }, + { + Title: "float64", + InputVal: float64(1.1), + Expected: true, + }, + { + Title: "string", + InputVal: "1.1", + Expected: false, + }, + { + Title: "bool", + InputVal: true, + Expected: false, + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + isNumber := isNumericType(test.InputVal) + + assert.Equal(t, test.Expected, isNumber) + }) + } +} + +func TestIsCompatibleInt(t *testing.T) { + cases := []struct { + Title string + InputVal interface{} + InputType reflect.Type + Expected bool + }{ + { + Title: "Valid", + InputVal: 1, + InputType: reflect.TypeOf(float64(0)), + Expected: true, + }, + { + Title: "Too Big", + InputVal: 300, + InputType: reflect.TypeOf(int8(0)), + Expected: false, + }, + { + Title: "String", + InputVal: "1", + InputType: reflect.TypeOf(int8(0)), + Expected: false, + }, + { + Title: "Nested Int", + InputVal: interface{}(int64(1)), + InputType: reflect.TypeOf(int8(0)), + Expected: true, + }, + { + Title: "Bool", + InputVal: true, + InputType: reflect.TypeOf(int8(0)), + Expected: false, + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + isNumber := isCompatibleInt(test.InputVal, test.InputType) + + assert.Equal(t, test.Expected, isNumber) + }) + } +} + +func TestHasDecimal(t *testing.T) { + cases := []struct { + Title string + InputVal float64 + Expected bool + }{ + { + Title: "Yes", + InputVal: 1.000005, + Expected: true, + }, + { + Title: "No", + InputVal: 1.0, + Expected: false, + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + hasDecimal := hasDecimalPlace(test.InputVal) + + assert.Equal(t, test.Expected, hasDecimal) + }) + } +} From 50a835952eb958dccbf1812091fe5947bb54a9f4 Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Fri, 9 Aug 2024 08:44:21 -0400 Subject: [PATCH 02/11] will panic if value is not exact type --- json_parse_node.go | 113 +++++------ json_parse_node_test.go | 414 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 473 insertions(+), 54 deletions(-) diff --git a/json_parse_node.go b/json_parse_node.go index 18396de..b589c08 100644 --- a/json_parse_node.go +++ b/json_parse_node.go @@ -45,7 +45,7 @@ func loadJsonTree(decoder *json.Decoder) (*JsonParseNode, error) { if err != nil { return nil, err } - switch token.(type) { + switch val := token.(type) { case json.Delim: switch token.(json.Delim) { case '{': @@ -99,43 +99,43 @@ func loadJsonTree(decoder *json.Decoder) (*JsonParseNode, error) { } return c, nil case string: - v := token.(string) + v := val c := &JsonParseNode{} c.SetValue(&v) return c, nil case bool: c := &JsonParseNode{} - v := token.(bool) + v := val c.SetValue(&v) return c, nil case int8: c := &JsonParseNode{} - v := token.(int8) + v := val c.SetValue(&v) return c, nil - case byte: + case int32: c := &JsonParseNode{} - v := token.(byte) + v := val c.SetValue(&v) return c, nil - case float64: + case int64: c := &JsonParseNode{} - v := token.(float64) + v := val c.SetValue(&v) return c, nil case float32: c := &JsonParseNode{} - v := token.(float32) + v := val c.SetValue(&v) return c, nil - case int32: + case float64: c := &JsonParseNode{} - v := token.(int32) + v := val c.SetValue(&v) return c, nil - case int64: + case byte: c := &JsonParseNode{} - v := token.(int64) + v := val c.SetValue(&v) return c, nil case nil: @@ -343,6 +343,7 @@ func (n *JsonParseNode) GetCollectionOfPrimitiveValues(targetType string) ([]int } return result, nil } + func (n *JsonParseNode) getPrimitiveValue(targetType string) (interface{}, error) { switch targetType { case "string": @@ -407,86 +408,90 @@ func (n *JsonParseNode) GetCollectionOfEnumValues(parser absser.EnumFactory) ([] // GetStringValue returns a String value from the nodes. func (n *JsonParseNode) GetStringValue() (*string, error) { - if n == nil || n.value == nil { - return nil, nil - } - res, ok := n.value.(*string) - if ok { - return res, nil - } else { - return nil, nil + var val string + + if err := as(n.value, &val); err != nil { + return nil, err } + + return &val, nil } // GetBoolValue returns a Bool value from the nodes. func (n *JsonParseNode) GetBoolValue() (*bool, error) { - if n == nil || n.value == nil { - return nil, nil + var val bool + + if err := as(n.value, &val); err != nil { + return nil, err } - return n.value.(*bool), nil + + return &val, nil } // GetInt8Value returns a int8 value from the nodes. func (n *JsonParseNode) GetInt8Value() (*int8, error) { - if n == nil || n.value == nil { - return nil, nil + var val int8 + + if err := as(n.value, &val); err != nil { + return nil, err } - return n.value.(*int8), nil + + return &val, nil } // GetBoolValue returns a Bool value from the nodes. func (n *JsonParseNode) GetByteValue() (*byte, error) { - if n == nil || n.value == nil { - return nil, nil + var val byte + + if err := as(n.value, &val); err != nil { + return nil, err } - return n.value.(*byte), nil + + return &val, nil } // GetFloat32Value returns a Float32 value from the nodes. func (n *JsonParseNode) GetFloat32Value() (*float32, error) { - v, err := n.GetFloat64Value() - if err != nil { + var val float32 + + if err := as(n.value, &val); err != nil { return nil, err } - if v == nil { - return nil, nil - } - cast := float32(*v) - return &cast, nil + + return &val, nil } // GetFloat64Value returns a Float64 value from the nodes. func (n *JsonParseNode) GetFloat64Value() (*float64, error) { - if n == nil || n.value == nil { - return nil, nil + var val float64 + + if err := as(n.value, &val); err != nil { + return nil, err } - return n.value.(*float64), nil + + return &val, nil } // GetInt32Value returns a Int32 value from the nodes. func (n *JsonParseNode) GetInt32Value() (*int32, error) { - v, err := n.GetFloat64Value() - if err != nil { + var val int32 + + if err := as(n.value, &val); err != nil { return nil, err } - if v == nil { - return nil, nil - } - cast := int32(*v) - return &cast, nil + + return &val, nil } // GetInt64Value returns a Int64 value from the nodes. func (n *JsonParseNode) GetInt64Value() (*int64, error) { - v, err := n.GetFloat64Value() - if err != nil { + var val int64 + + if err := as(n.value, &val); err != nil { return nil, err } - if v == nil { - return nil, nil - } - cast := int64(*v) - return &cast, nil + + return &val, nil } // GetTimeValue returns a Time value from the nodes. diff --git a/json_parse_node_test.go b/json_parse_node_test.go index 5f79ce1..e41b653 100644 --- a/json_parse_node_test.go +++ b/json_parse_node_test.go @@ -1,6 +1,8 @@ package jsonserialization import ( + "errors" + "reflect" "testing" "github.com/microsoft/kiota-serialization-json-go/internal" @@ -321,6 +323,418 @@ func TestUntypedJsonObject(t *testing.T) { } } +func TestJsonGetStringValue(t *testing.T) { + cases := []struct { + Title string + Input []byte + Expected interface{} + Error error + }{ + { + Title: "Valid", + Input: []byte(`"I am a string"`), + Expected: "I am a string", + Error: nil, + }, + { + //Intentionally does not work, see https://github.com/microsoft/kiota-serialization-json-go/issues/142 + Title: "Integer", + Input: []byte(`1`), + Expected: (*string)(nil), + Error: errors.New("value '1' is not compatible with type string"), + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + var val any + + node, err := NewJsonParseNode(test.Input) + assert.Nil(t, err) + + val, err = node.GetStringValue() + + assert.Equal(t, test.Error, err) + v := reflect.ValueOf(val) + if !v.IsNil() && v.Kind() == reflect.Ptr { + val = v.Elem().Interface() + } + assert.Equal(t, test.Expected, val) + }) + } +} + +func TestJsonGetBoolValue(t *testing.T) { + cases := []struct { + Title string + Input []byte + Expected interface{} + Error error + }{ + { + Title: "Valid", + Input: []byte(`true`), + Expected: true, + Error: nil, + }, + { + Title: "Integer", + Input: []byte(`1`), + Expected: (*bool)(nil), + Error: errors.New("value '1' is not compatible with type bool"), + }, + { + Title: "String", + Input: []byte(`"true"`), + Expected: (*bool)(nil), + Error: errors.New("value 'true' is not compatible with type bool"), + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + var val any + + node, err := NewJsonParseNode(test.Input) + assert.Nil(t, err) + + val, err = node.GetBoolValue() + + assert.Equal(t, test.Error, err) + v := reflect.ValueOf(val) + if !v.IsNil() && v.Kind() == reflect.Ptr { + val = v.Elem().Interface() + } + assert.Equal(t, test.Expected, val) + }) + } +} + +func TestJsonGetInt8Value(t *testing.T) { + cases := []struct { + Title string + Input []byte + Expected interface{} + Error error + }{ + { + Title: "Valid", + Input: []byte(`1`), + Expected: int8(1), + Error: nil, + }, + { + Title: "Bool", + Input: []byte(`true`), + Expected: (*int8)(nil), + Error: errors.New("value 'true' is not compatible with type int8"), + }, + { + Title: "String", + Input: []byte(`"1"`), + Expected: (*int8)(nil), + Error: errors.New("value '1' is not compatible with type int8"), + }, + { + Title: "Float", + Input: []byte(`1.1`), + Expected: (*int8)(nil), + Error: errors.New("value '1.1' is not compatible with type int8"), + }, + { + Title: "Too Big", + Input: []byte(`129`), + Expected: (*int8)(nil), + Error: errors.New("value '129' is not compatible with type int8"), + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + var val any + + node, err := NewJsonParseNode(test.Input) + assert.Nil(t, err) + + val, err = node.GetInt8Value() + + assert.Equal(t, test.Error, err) + v := reflect.ValueOf(val) + if !v.IsNil() && v.Kind() == reflect.Ptr { + val = v.Elem().Interface() + } + assert.Equal(t, test.Expected, val) + }) + } +} + +func TestJsonGetByteValue(t *testing.T) { + cases := []struct { + Title string + Input []byte + Expected interface{} + Error error + }{ + { + Title: "Valid", + Input: []byte(`1`), + Expected: uint8(1), + Error: nil, + }, + { + Title: "Bool", + Input: []byte(`true`), + Expected: (*uint8)(nil), + Error: errors.New("value 'true' is not compatible with type uint8"), + }, + { + Title: "Float", + Input: []byte(`1.1`), + Expected: (*uint8)(nil), + Error: errors.New("value '1.1' is not compatible with type uint8"), + }, + { + Title: "String", + Input: []byte(`"1"`), + Expected: (*uint8)(nil), + Error: errors.New("value '1' is not compatible with type uint8"), + }, + { + Title: "Too Big", + Input: []byte(`3.40283e+38`), + Expected: (*uint8)(nil), + Error: errors.New("value '3.40283e+38' is not compatible with type uint8"), + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + var val any + + node, err := NewJsonParseNode(test.Input) + assert.Nil(t, err) + + val, err = node.GetByteValue() + + assert.Equal(t, test.Error, err) + v := reflect.ValueOf(val) + if !v.IsNil() && v.Kind() == reflect.Ptr { + val = v.Elem().Interface() + } + assert.Equal(t, test.Expected, val) + }) + } +} + +func TestJsonGetFloat32Value(t *testing.T) { + cases := []struct { + Title string + Input []byte + Expected interface{} + Error error + }{ + { + Title: "Valid", + Input: []byte(`1`), + Expected: float32(1), + Error: nil, + }, + { + Title: "Bool", + Input: []byte(`true`), + Expected: (*float32)(nil), + Error: errors.New("value 'true' is not compatible with type float32"), + }, + { + Title: "String", + Input: []byte(`"1"`), + Expected: (*float32)(nil), + Error: errors.New("value '1' is not compatible with type float32"), + }, + { + Title: "Too Big", + Input: []byte(`3.40283e+38`), + Expected: (*float32)(nil), + Error: errors.New("value '3.40283e+38' is not compatible with type float32"), + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + var val any + + node, err := NewJsonParseNode(test.Input) + assert.Nil(t, err) + + val, err = node.GetFloat32Value() + + assert.Equal(t, test.Error, err) + v := reflect.ValueOf(val) + if !v.IsNil() && v.Kind() == reflect.Ptr { + val = v.Elem().Interface() + } + assert.Equal(t, test.Expected, val) + }) + } +} + +func TestJsonGetFloat64Value(t *testing.T) { + cases := []struct { + Title string + Input []byte + Expected interface{} + Error error + }{ + { + Title: "Valid", + Input: []byte(`1`), + Expected: float64(1), + Error: nil, + }, + { + Title: "Bool", + Input: []byte(`true`), + Expected: (*float64)(nil), + Error: errors.New("value 'true' is not compatible with type float64"), + }, + { + Title: "String", + Input: []byte(`"1"`), + Expected: (*float64)(nil), + Error: errors.New("value '1' is not compatible with type float64"), + }, + //NOTE: no point in checking too big, the STD JSON encoder will error out first :) + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + var val any + + node, err := NewJsonParseNode(test.Input) + assert.Nil(t, err) + + val, err = node.GetFloat64Value() + + assert.Equal(t, test.Error, err) + v := reflect.ValueOf(val) + if !v.IsNil() && v.Kind() == reflect.Ptr { + val = v.Elem().Interface() + } + assert.Equal(t, test.Expected, val) + }) + } +} + +func TestJsonGetInt32Value(t *testing.T) { + cases := []struct { + Title string + Input []byte + Expected interface{} + Error error + }{ + { + Title: "Valid", + Input: []byte(`1`), + Expected: int32(1), + Error: nil, + }, + { + Title: "Bool", + Input: []byte(`true`), + Expected: (*int32)(nil), + Error: errors.New("value 'true' is not compatible with type int32"), + }, + { + Title: "Float", + Input: []byte(`1.1`), + Expected: (*int32)(nil), + Error: errors.New("value '1.1' is not compatible with type int32"), + }, + { + Title: "String", + Input: []byte(`"1"`), + Expected: (*int32)(nil), + Error: errors.New("value '1' is not compatible with type int32"), + }, + { + Title: "Too Big", + Input: []byte(`3.40283e+38`), + Expected: (*int32)(nil), + Error: errors.New("value '3.40283e+38' is not compatible with type int32"), + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + var val any + + node, err := NewJsonParseNode(test.Input) + assert.Nil(t, err) + + val, err = node.GetInt32Value() + + assert.Equal(t, test.Error, err) + v := reflect.ValueOf(val) + if !v.IsNil() && v.Kind() == reflect.Ptr { + val = v.Elem().Interface() + } + assert.Equal(t, test.Expected, val) + }) + } +} + +func TestJsonGetInt64Value(t *testing.T) { + cases := []struct { + Title string + Input []byte + Expected interface{} + Error error + }{ + { + Title: "Valid", + Input: []byte(`1`), + Expected: int64(1), + Error: nil, + }, + { + Title: "Bool", + Input: []byte(`true`), + Expected: (*int64)(nil), + Error: errors.New("value 'true' is not compatible with type int64"), + }, + { + Title: "Float", + Input: []byte(`1.1`), + Expected: (*int64)(nil), + Error: errors.New("value '1.1' is not compatible with type int64"), + }, + { + Title: "Too Big", + Input: []byte(`3.40283e+38`), + Expected: (*int64)(nil), + Error: errors.New("value '3.40283e+38' is not compatible with type int64"), + }, + } + + for _, test := range cases { + t.Run(test.Title, func(t *testing.T) { + var val any + + node, err := NewJsonParseNode(test.Input) + assert.Nil(t, err) + + val, err = node.GetInt64Value() + + assert.Equal(t, test.Error, err) + v := reflect.ValueOf(val) + if !v.IsNil() && v.Kind() == reflect.Ptr { + val = v.Elem().Interface() + } + assert.Equal(t, test.Expected, val) + }) + } +} + const TestUntypedJson = "{\r\n" + " \"@odata.context\": \"https://graph.microsoft.com/v1.0/$metadata#sites('contoso.sharepoint.com')/lists('fa631c4d-ac9f-4884-a7f5-13c659d177e3')/items('1')/fields/$entity\",\r\n" + " \"id\": \"5\",\r\n" + From 3c6825e685cb222a1aade027a369cf821919629f Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Fri, 9 Aug 2024 08:45:13 -0400 Subject: [PATCH 03/11] didn't account for varying timezones (+/-) --- json_parse_node_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json_parse_node_test.go b/json_parse_node_test.go index e41b653..fbea9cb 100644 --- a/json_parse_node_test.go +++ b/json_parse_node_test.go @@ -236,7 +236,7 @@ func TestParsingTime(t *testing.T) { assert.Nil(t, err) time1, err := someProp.GetTimeValue() assert.Nil(t, err) - assert.Contains(t, time1.String(), "2023-07-12 08:54:24 +") + assert.Regexp(t, "^2023-07-12 08:54:24 [-+]", time1.String()) someProp2, err := parseNode.GetChildNode("withZone") assert.Nil(t, err) From 4ff3bd0d1eaf41de8c75d2c0d4a8c06ab9627182 Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Fri, 9 Aug 2024 09:16:18 -0400 Subject: [PATCH 04/11] missing check for nil --- .vscode/settings.json | 11 +++++++++++ util.go | 5 +++++ util_test.go | 15 +++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0709ac5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "go.lintTool": "golangci-lint", + "go.formatFlags": ["-s", "-w"], + "go.lintOnSave": "package", + "go.lintFlags": ["-c", "~/.dotfiles/.golangci.yml", "--issues-exit-code=0"], + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "go.coverOnSave": true, + "go.coverageOptions": "showBothCoveredAndUncoveredCode", + "go.testOnSave": true +} diff --git a/util.go b/util.go index 2af1b67..8eb2900 100644 --- a/util.go +++ b/util.go @@ -81,6 +81,11 @@ func as[T any](in interface{}, out T) error { // isNumericType checks if the given type is a numeric type. func isNumericType(in interface{}) bool { + + if in == nil { + return false + } + tp, ok := in.(reflect.Type) if !ok { tp = reflect.TypeOf(in) diff --git a/util_test.go b/util_test.go index dc865e7..5cf542d 100644 --- a/util_test.go +++ b/util_test.go @@ -156,6 +156,21 @@ func TestIsNumericType(t *testing.T) { InputVal: true, Expected: false, }, + { + Title: "Untyped Nil", + InputVal: nil, + Expected: false, + }, + { + Title: "Typed Nil", + InputVal: (*int)(nil), + Expected: false, + }, + { + Title: "Interface", + InputVal: interface{}(int(1)), + Expected: true, + }, } for _, test := range cases { From fd1f34e5da9f75c20d76ae24a0ce732409c2188a Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Fri, 9 Aug 2024 10:24:30 -0400 Subject: [PATCH 05/11] id property being a int appears to be a mistype, otherwise test fails --- intersection_type_wrapper_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/intersection_type_wrapper_test.go b/intersection_type_wrapper_test.go index dfa789f..2bf82d5 100644 --- a/intersection_type_wrapper_test.go +++ b/intersection_type_wrapper_test.go @@ -31,7 +31,7 @@ func TestItParsesIntersectionTypeComplexProperty1(t *testing.T) { } func TestItParsesIntersectionTypeComplexProperty2(t *testing.T) { - source := "{\"displayName\":\"McGill\",\"officeLocation\":\"Montreal\", \"id\": 10}" + source := "{\"displayName\":\"McGill\",\"officeLocation\":\"Montreal\", \"id\": \"10\"}" sourceArray := []byte(source) parseNode, err := NewJsonParseNode(sourceArray) @@ -39,17 +39,16 @@ func TestItParsesIntersectionTypeComplexProperty2(t *testing.T) { t.Error(err) } result, err := parseNode.GetObjectValue(internal.CreateIntersectionTypeMockFromDiscriminator) - if err != nil { - t.Error(err) - } + assert.Nil(t, err) assert.NotNil(t, result) cast, ok := result.(internal.IntersectionTypeMockable) + assert.NotNil(t, cast) assert.True(t, ok) assert.NotNil(t, cast.GetComposedType1()) assert.NotNil(t, cast.GetComposedType2()) assert.Nil(t, cast.GetStringValue()) assert.Nil(t, cast.GetComposedType3()) - assert.Nil(t, cast.GetComposedType1().GetId()) + assert.NotNil(t, cast.GetComposedType1().GetId()) assert.Nil(t, cast.GetComposedType2().GetId()) // it's expected to be null since we have conflicting properties here and the parser will only try one to avoid having to brute its way through assert.Equal(t, "McGill", *cast.GetComposedType2().GetDisplayName()) } From 488f87a9ba98a9149b4b6800e833aed2b2671d1f Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Fri, 9 Aug 2024 11:37:02 -0400 Subject: [PATCH 06/11] wasn't cases to handle if out is nil or nested nil --- util.go | 28 +++++++++++++++++++-- util_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/util.go b/util.go index 8eb2900..a94107d 100644 --- a/util.go +++ b/util.go @@ -44,10 +44,29 @@ func isCompatible(value interface{}, tp reflect.Type) bool { return reflect.TypeOf(value).ConvertibleTo(tp) } +// isNil checks if a value is nil or a nil interface, including nested pointers. +func isNil(a interface{}) bool { + if a == nil { + return true + } + val := reflect.ValueOf(a) + for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { + if val.IsNil() { + return true + } + val = val.Elem() + } + switch val.Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice: + return val.IsNil() + } + return false +} + // as converts the value to the type T. func as[T any](in interface{}, out T) error { // No point in trying anything if already nil - if in == nil { + if isNil(in) { return nil } @@ -59,7 +78,7 @@ func as[T any](in interface{}, out T) error { } outVal := reflect.ValueOf(out) - if outVal.Kind() != reflect.Pointer || outVal.IsNil() { + if outVal.Kind() != reflect.Pointer || isNil(out) { return fmt.Errorf("out is not pointer or is nil") } @@ -126,3 +145,8 @@ func isCompatibleInt(in interface{}, tp reflect.Type) bool { func hasDecimalPlace(value float64) bool { return value != float64(int64(value)) } + +// isNilNode checks if the JsonParseNode is nil. +func isNilNode(n *JsonParseNode) bool { + return n == nil || n.value == nil +} diff --git a/util_test.go b/util_test.go index 5cf542d..07b1f36 100644 --- a/util_test.go +++ b/util_test.go @@ -37,6 +37,38 @@ func TestIsCompatible(t *testing.T) { } } +func TestIsNil(t *testing.T) { + tests := []struct { + Title string + Input interface{} + Expected bool + }{ + {"nil value", nil, true}, + {"non-nil int", 42, false}, + {"nil pointer", (*int)(nil), true}, + {"non-nil pointer", new(int), false}, + {"nil interface", interface{}(nil), true}, + {"non-nil interface", interface{}(42), false}, + {"nil slice", ([]int)(nil), true}, + {"non-nil slice", []int{1, 2, 3}, false}, + {"nil map", (map[string]int)(nil), true}, + {"non-nil map", map[string]int{"a": 1}, false}, + {"nil chan", (chan int)(nil), true}, + {"non-nil chan", make(chan int), false}, + {"nil func", (func())(nil), true}, + {"non-nil func", func() {}, false}, + {"nested nil pointer", (**int)(nil), true}, + {"nested non-nil pointer", func() **int { var i int; p := &i; return &p }(), false}, + } + + for _, tt := range tests { + t.Run(tt.Title, func(t *testing.T) { + got := isNil(tt.Input) + assert.Equal(t, tt.Expected, got) + }) + } +} + func TestAs(t *testing.T) { cases := []struct { Title string @@ -54,22 +86,49 @@ func TestAs(t *testing.T) { Error: nil, }, { - Title: "Interface", + Title: "Incompatible", + InputVal: []interface{}{ + "I am a string", + int8(0), + }, + Expected: int8(0), + Error: errors.New("value 'I am a string' is not compatible with type int8"), + }, + { + Title: "Untyped Nil - In", InputVal: []interface{}{ - int8(1), nil, + int8(0), }, - Expected: interface{}(int8(1)), + Expected: int8(0), Error: nil, }, { - Title: "Incompatible", + Title: "Typed Nil - In", InputVal: []interface{}{ - "I am a string", + (*string)(nil), int8(0), }, Expected: int8(0), - Error: errors.New("value 'I am a string' is not compatible with type int8"), + Error: nil, + }, + { + Title: "Untyped Nil - Out", + InputVal: []interface{}{ + int8(1), + nil, + }, + Expected: nil, + Error: errors.New("out is not pointer or is nil"), + }, + { + Title: "Typed Nil - Out", + InputVal: []interface{}{ + int8(0), + (*int8)(nil), + }, + Expected: (*int8)(nil), + Error: errors.New("out is not pointer or is nil"), }, } From 5aeb78c8b38a2b47e1aa95efeba9babcdf213b06 Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Fri, 9 Aug 2024 11:38:27 -0400 Subject: [PATCH 07/11] ensure value or node isn't nil --- json_parse_node.go | 65 +++++++++++++++++++++++++++++++++++++++++----- util.go | 2 +- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/json_parse_node.go b/json_parse_node.go index b589c08..2a0a57d 100644 --- a/json_parse_node.go +++ b/json_parse_node.go @@ -153,6 +153,9 @@ func (n *JsonParseNode) SetValue(value interface{}) { // GetChildNode returns a new parse node for the given identifier. func (n *JsonParseNode) GetChildNode(index string) (absser.ParseNode, error) { + if isNilNode(n) { + return nil, nil + } if index == "" { return nil, errors.New("index is empty") } @@ -178,12 +181,12 @@ func (n *JsonParseNode) GetChildNode(index string) (absser.ParseNode, error) { // GetObjectValue returns the Parsable value from the node. func (n *JsonParseNode) GetObjectValue(ctor absser.ParsableFactory) (absser.Parsable, error) { + if isNilNode(n) { + return nil, nil + } if ctor == nil { return nil, errors.New("constructor is nil") } - if n == nil || n.value == nil { - return nil, nil - } result, err := ctor(n) if err != nil { return nil, err @@ -292,7 +295,7 @@ func (n *JsonParseNode) GetObjectValue(ctor absser.ParsableFactory) (absser.Pars // GetCollectionOfObjectValues returns the collection of Parsable values from the node. func (n *JsonParseNode) GetCollectionOfObjectValues(ctor absser.ParsableFactory) ([]absser.Parsable, error) { - if n == nil || n.value == nil { + if isNilNode(n) { return nil, nil } if ctor == nil { @@ -319,7 +322,7 @@ func (n *JsonParseNode) GetCollectionOfObjectValues(ctor absser.ParsableFactory) // GetCollectionOfPrimitiveValues returns the collection of primitive values from the node. func (n *JsonParseNode) GetCollectionOfPrimitiveValues(targetType string) ([]interface{}, error) { - if n == nil || n.value == nil { + if isNilNode(n) { return nil, nil } if targetType == "" { @@ -345,6 +348,9 @@ func (n *JsonParseNode) GetCollectionOfPrimitiveValues(targetType string) ([]int } func (n *JsonParseNode) getPrimitiveValue(targetType string) (interface{}, error) { + if isNilNode(n) { + return nil, nil + } switch targetType { case "string": return n.GetStringValue() @@ -381,7 +387,7 @@ func (n *JsonParseNode) getPrimitiveValue(targetType string) (interface{}, error // GetCollectionOfEnumValues returns the collection of Enum values from the node. func (n *JsonParseNode) GetCollectionOfEnumValues(parser absser.EnumFactory) ([]interface{}, error) { - if n == nil || n.value == nil { + if isNilNode(n) { return nil, nil } if parser == nil { @@ -408,6 +414,9 @@ func (n *JsonParseNode) GetCollectionOfEnumValues(parser absser.EnumFactory) ([] // GetStringValue returns a String value from the nodes. func (n *JsonParseNode) GetStringValue() (*string, error) { + if isNilNode(n) { + return nil, nil + } var val string if err := as(n.value, &val); err != nil { @@ -419,6 +428,9 @@ func (n *JsonParseNode) GetStringValue() (*string, error) { // GetBoolValue returns a Bool value from the nodes. func (n *JsonParseNode) GetBoolValue() (*bool, error) { + if isNilNode(n) { + return nil, nil + } var val bool if err := as(n.value, &val); err != nil { @@ -430,6 +442,9 @@ func (n *JsonParseNode) GetBoolValue() (*bool, error) { // GetInt8Value returns a int8 value from the nodes. func (n *JsonParseNode) GetInt8Value() (*int8, error) { + if isNilNode(n) { + return nil, nil + } var val int8 if err := as(n.value, &val); err != nil { @@ -441,6 +456,9 @@ func (n *JsonParseNode) GetInt8Value() (*int8, error) { // GetBoolValue returns a Bool value from the nodes. func (n *JsonParseNode) GetByteValue() (*byte, error) { + if isNilNode(n) { + return nil, nil + } var val byte if err := as(n.value, &val); err != nil { @@ -452,6 +470,9 @@ func (n *JsonParseNode) GetByteValue() (*byte, error) { // GetFloat32Value returns a Float32 value from the nodes. func (n *JsonParseNode) GetFloat32Value() (*float32, error) { + if isNilNode(n) { + return nil, nil + } var val float32 if err := as(n.value, &val); err != nil { @@ -463,6 +484,9 @@ func (n *JsonParseNode) GetFloat32Value() (*float32, error) { // GetFloat64Value returns a Float64 value from the nodes. func (n *JsonParseNode) GetFloat64Value() (*float64, error) { + if isNilNode(n) { + return nil, nil + } var val float64 if err := as(n.value, &val); err != nil { @@ -474,6 +498,9 @@ func (n *JsonParseNode) GetFloat64Value() (*float64, error) { // GetInt32Value returns a Int32 value from the nodes. func (n *JsonParseNode) GetInt32Value() (*int32, error) { + if isNilNode(n) { + return nil, nil + } var val int32 if err := as(n.value, &val); err != nil { @@ -485,6 +512,9 @@ func (n *JsonParseNode) GetInt32Value() (*int32, error) { // GetInt64Value returns a Int64 value from the nodes. func (n *JsonParseNode) GetInt64Value() (*int64, error) { + if isNilNode(n) { + return nil, nil + } var val int64 if err := as(n.value, &val); err != nil { @@ -496,6 +526,9 @@ func (n *JsonParseNode) GetInt64Value() (*int64, error) { // GetTimeValue returns a Time value from the nodes. func (n *JsonParseNode) GetTimeValue() (*time.Time, error) { + if isNilNode(n) { + return nil, nil + } v, err := n.GetStringValue() if err != nil { return nil, err @@ -514,6 +547,9 @@ func (n *JsonParseNode) GetTimeValue() (*time.Time, error) { // GetISODurationValue returns a ISODuration value from the nodes. func (n *JsonParseNode) GetISODurationValue() (*absser.ISODuration, error) { + if isNilNode(n) { + return nil, nil + } v, err := n.GetStringValue() if err != nil { return nil, err @@ -526,6 +562,9 @@ func (n *JsonParseNode) GetISODurationValue() (*absser.ISODuration, error) { // GetTimeOnlyValue returns a TimeOnly value from the nodes. func (n *JsonParseNode) GetTimeOnlyValue() (*absser.TimeOnly, error) { + if isNilNode(n) { + return nil, nil + } v, err := n.GetStringValue() if err != nil { return nil, err @@ -538,6 +577,9 @@ func (n *JsonParseNode) GetTimeOnlyValue() (*absser.TimeOnly, error) { // GetDateOnlyValue returns a DateOnly value from the nodes. func (n *JsonParseNode) GetDateOnlyValue() (*absser.DateOnly, error) { + if isNilNode(n) { + return nil, nil + } v, err := n.GetStringValue() if err != nil { return nil, err @@ -550,6 +592,9 @@ func (n *JsonParseNode) GetDateOnlyValue() (*absser.DateOnly, error) { // GetUUIDValue returns a UUID value from the nodes. func (n *JsonParseNode) GetUUIDValue() (*uuid.UUID, error) { + if isNilNode(n) { + return nil, nil + } v, err := n.GetStringValue() if err != nil { return nil, err @@ -563,6 +608,9 @@ func (n *JsonParseNode) GetUUIDValue() (*uuid.UUID, error) { // GetEnumValue returns a Enum value from the nodes. func (n *JsonParseNode) GetEnumValue(parser absser.EnumFactory) (interface{}, error) { + if isNilNode(n) { + return nil, nil + } if parser == nil { return nil, errors.New("parser is nil") } @@ -578,6 +626,9 @@ func (n *JsonParseNode) GetEnumValue(parser absser.EnumFactory) (interface{}, er // GetByteArrayValue returns a ByteArray value from the nodes. func (n *JsonParseNode) GetByteArrayValue() ([]byte, error) { + if isNilNode(n) { + return nil, nil + } s, err := n.GetStringValue() if err != nil { return nil, err @@ -590,7 +641,7 @@ func (n *JsonParseNode) GetByteArrayValue() ([]byte, error) { // GetRawValue returns a ByteArray value from the nodes. func (n *JsonParseNode) GetRawValue() (interface{}, error) { - if n == nil || n.value == nil { + if isNilNode(n) { return nil, nil } switch v := n.value.(type) { diff --git a/util.go b/util.go index a94107d..032129e 100644 --- a/util.go +++ b/util.go @@ -148,5 +148,5 @@ func hasDecimalPlace(value float64) bool { // isNilNode checks if the JsonParseNode is nil. func isNilNode(n *JsonParseNode) bool { - return n == nil || n.value == nil + return isNil(n) || isNil(n.value) } From 4932cd30879751357993e0008d460730127ddf37 Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Fri, 9 Aug 2024 11:43:39 -0400 Subject: [PATCH 08/11] no need for a dedicated functions --- json_parse_node.go | 44 ++++++++++++++++++++++---------------------- util.go | 5 ----- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/json_parse_node.go b/json_parse_node.go index 2a0a57d..e047627 100644 --- a/json_parse_node.go +++ b/json_parse_node.go @@ -153,7 +153,7 @@ func (n *JsonParseNode) SetValue(value interface{}) { // GetChildNode returns a new parse node for the given identifier. func (n *JsonParseNode) GetChildNode(index string) (absser.ParseNode, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } if index == "" { @@ -181,7 +181,7 @@ func (n *JsonParseNode) GetChildNode(index string) (absser.ParseNode, error) { // GetObjectValue returns the Parsable value from the node. func (n *JsonParseNode) GetObjectValue(ctor absser.ParsableFactory) (absser.Parsable, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } if ctor == nil { @@ -295,7 +295,7 @@ func (n *JsonParseNode) GetObjectValue(ctor absser.ParsableFactory) (absser.Pars // GetCollectionOfObjectValues returns the collection of Parsable values from the node. func (n *JsonParseNode) GetCollectionOfObjectValues(ctor absser.ParsableFactory) ([]absser.Parsable, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } if ctor == nil { @@ -322,7 +322,7 @@ func (n *JsonParseNode) GetCollectionOfObjectValues(ctor absser.ParsableFactory) // GetCollectionOfPrimitiveValues returns the collection of primitive values from the node. func (n *JsonParseNode) GetCollectionOfPrimitiveValues(targetType string) ([]interface{}, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } if targetType == "" { @@ -348,7 +348,7 @@ func (n *JsonParseNode) GetCollectionOfPrimitiveValues(targetType string) ([]int } func (n *JsonParseNode) getPrimitiveValue(targetType string) (interface{}, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } switch targetType { @@ -387,7 +387,7 @@ func (n *JsonParseNode) getPrimitiveValue(targetType string) (interface{}, error // GetCollectionOfEnumValues returns the collection of Enum values from the node. func (n *JsonParseNode) GetCollectionOfEnumValues(parser absser.EnumFactory) ([]interface{}, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } if parser == nil { @@ -414,7 +414,7 @@ func (n *JsonParseNode) GetCollectionOfEnumValues(parser absser.EnumFactory) ([] // GetStringValue returns a String value from the nodes. func (n *JsonParseNode) GetStringValue() (*string, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } var val string @@ -428,7 +428,7 @@ func (n *JsonParseNode) GetStringValue() (*string, error) { // GetBoolValue returns a Bool value from the nodes. func (n *JsonParseNode) GetBoolValue() (*bool, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } var val bool @@ -442,7 +442,7 @@ func (n *JsonParseNode) GetBoolValue() (*bool, error) { // GetInt8Value returns a int8 value from the nodes. func (n *JsonParseNode) GetInt8Value() (*int8, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } var val int8 @@ -456,7 +456,7 @@ func (n *JsonParseNode) GetInt8Value() (*int8, error) { // GetBoolValue returns a Bool value from the nodes. func (n *JsonParseNode) GetByteValue() (*byte, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } var val byte @@ -470,7 +470,7 @@ func (n *JsonParseNode) GetByteValue() (*byte, error) { // GetFloat32Value returns a Float32 value from the nodes. func (n *JsonParseNode) GetFloat32Value() (*float32, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } var val float32 @@ -484,7 +484,7 @@ func (n *JsonParseNode) GetFloat32Value() (*float32, error) { // GetFloat64Value returns a Float64 value from the nodes. func (n *JsonParseNode) GetFloat64Value() (*float64, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } var val float64 @@ -498,7 +498,7 @@ func (n *JsonParseNode) GetFloat64Value() (*float64, error) { // GetInt32Value returns a Int32 value from the nodes. func (n *JsonParseNode) GetInt32Value() (*int32, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } var val int32 @@ -512,7 +512,7 @@ func (n *JsonParseNode) GetInt32Value() (*int32, error) { // GetInt64Value returns a Int64 value from the nodes. func (n *JsonParseNode) GetInt64Value() (*int64, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } var val int64 @@ -526,7 +526,7 @@ func (n *JsonParseNode) GetInt64Value() (*int64, error) { // GetTimeValue returns a Time value from the nodes. func (n *JsonParseNode) GetTimeValue() (*time.Time, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } v, err := n.GetStringValue() @@ -547,7 +547,7 @@ func (n *JsonParseNode) GetTimeValue() (*time.Time, error) { // GetISODurationValue returns a ISODuration value from the nodes. func (n *JsonParseNode) GetISODurationValue() (*absser.ISODuration, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } v, err := n.GetStringValue() @@ -562,7 +562,7 @@ func (n *JsonParseNode) GetISODurationValue() (*absser.ISODuration, error) { // GetTimeOnlyValue returns a TimeOnly value from the nodes. func (n *JsonParseNode) GetTimeOnlyValue() (*absser.TimeOnly, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } v, err := n.GetStringValue() @@ -577,7 +577,7 @@ func (n *JsonParseNode) GetTimeOnlyValue() (*absser.TimeOnly, error) { // GetDateOnlyValue returns a DateOnly value from the nodes. func (n *JsonParseNode) GetDateOnlyValue() (*absser.DateOnly, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } v, err := n.GetStringValue() @@ -592,7 +592,7 @@ func (n *JsonParseNode) GetDateOnlyValue() (*absser.DateOnly, error) { // GetUUIDValue returns a UUID value from the nodes. func (n *JsonParseNode) GetUUIDValue() (*uuid.UUID, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } v, err := n.GetStringValue() @@ -608,7 +608,7 @@ func (n *JsonParseNode) GetUUIDValue() (*uuid.UUID, error) { // GetEnumValue returns a Enum value from the nodes. func (n *JsonParseNode) GetEnumValue(parser absser.EnumFactory) (interface{}, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } if parser == nil { @@ -626,7 +626,7 @@ func (n *JsonParseNode) GetEnumValue(parser absser.EnumFactory) (interface{}, er // GetByteArrayValue returns a ByteArray value from the nodes. func (n *JsonParseNode) GetByteArrayValue() ([]byte, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } s, err := n.GetStringValue() @@ -641,7 +641,7 @@ func (n *JsonParseNode) GetByteArrayValue() ([]byte, error) { // GetRawValue returns a ByteArray value from the nodes. func (n *JsonParseNode) GetRawValue() (interface{}, error) { - if isNilNode(n) { + if isNil(n) || isNil(n.value) { return nil, nil } switch v := n.value.(type) { diff --git a/util.go b/util.go index 032129e..0d017ec 100644 --- a/util.go +++ b/util.go @@ -145,8 +145,3 @@ func isCompatibleInt(in interface{}, tp reflect.Type) bool { func hasDecimalPlace(value float64) bool { return value != float64(int64(value)) } - -// isNilNode checks if the JsonParseNode is nil. -func isNilNode(n *JsonParseNode) bool { - return isNil(n) || isNil(n.value) -} From 3447cae5af09960e0ea18651368c09a8c096c094 Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Mon, 12 Aug 2024 14:26:58 -0400 Subject: [PATCH 09/11] efficiency concerns for more directly assert-able types --- json_parse_node.go | 18 ++++++++---------- json_parse_node_test.go | 6 +++--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/json_parse_node.go b/json_parse_node.go index e047627..3097d53 100644 --- a/json_parse_node.go +++ b/json_parse_node.go @@ -417,13 +417,12 @@ func (n *JsonParseNode) GetStringValue() (*string, error) { if isNil(n) || isNil(n.value) { return nil, nil } - var val string - if err := as(n.value, &val); err != nil { - return nil, err + val, ok := n.value.(*string) + if !ok { + return nil, fmt.Errorf("type '%T' is not compatible with type string", n.value) } - - return &val, nil + return val, nil } // GetBoolValue returns a Bool value from the nodes. @@ -431,13 +430,12 @@ func (n *JsonParseNode) GetBoolValue() (*bool, error) { if isNil(n) || isNil(n.value) { return nil, nil } - var val bool - if err := as(n.value, &val); err != nil { - return nil, err + val, ok := n.value.(*bool) + if !ok { + return nil, fmt.Errorf("type '%T' is not compatible with type bool", n.value) } - - return &val, nil + return val, nil } // GetInt8Value returns a int8 value from the nodes. diff --git a/json_parse_node_test.go b/json_parse_node_test.go index fbea9cb..aa6302c 100644 --- a/json_parse_node_test.go +++ b/json_parse_node_test.go @@ -341,7 +341,7 @@ func TestJsonGetStringValue(t *testing.T) { Title: "Integer", Input: []byte(`1`), Expected: (*string)(nil), - Error: errors.New("value '1' is not compatible with type string"), + Error: errors.New("type '*float64' is not compatible with type string"), }, } @@ -381,13 +381,13 @@ func TestJsonGetBoolValue(t *testing.T) { Title: "Integer", Input: []byte(`1`), Expected: (*bool)(nil), - Error: errors.New("value '1' is not compatible with type bool"), + Error: errors.New("type '*float64' is not compatible with type bool"), }, { Title: "String", Input: []byte(`"true"`), Expected: (*bool)(nil), - Error: errors.New("value 'true' is not compatible with type bool"), + Error: errors.New("type '*string' is not compatible with type bool"), }, } From c286140eddf1c69ad5f752ad2a4e92303ad19dc8 Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Tue, 13 Aug 2024 10:05:20 -0400 Subject: [PATCH 10/11] incorrect TestTree test --- json_parse_node.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/json_parse_node.go b/json_parse_node.go index f6293ab..c6235ac 100644 --- a/json_parse_node.go +++ b/json_parse_node.go @@ -36,6 +36,7 @@ func NewJsonParseNode(content []byte) (*JsonParseNode, error) { value, err := loadJsonTree(decoder) return value, err } + func loadJsonTree(decoder *json.Decoder) (*JsonParseNode, error) { for { token, err := decoder.Token() @@ -45,7 +46,7 @@ func loadJsonTree(decoder *json.Decoder) (*JsonParseNode, error) { if err != nil { return nil, err } - switch val := token.(type) { + switch token.(type) { case json.Delim: switch token.(json.Delim) { case '{': @@ -99,7 +100,7 @@ func loadJsonTree(decoder *json.Decoder) (*JsonParseNode, error) { } return c, nil case string: - v := val + v := token.(string) c := &JsonParseNode{} c.setValue(&v) return c, nil @@ -113,12 +114,12 @@ func loadJsonTree(decoder *json.Decoder) (*JsonParseNode, error) { v := token.(int8) c.setValue(&v) return c, nil - case int32: + case byte: c := &JsonParseNode{} v := token.(byte) c.setValue(&v) return c, nil - case int64: + case float64: c := &JsonParseNode{} v := token.(float64) c.setValue(&v) @@ -128,12 +129,12 @@ func loadJsonTree(decoder *json.Decoder) (*JsonParseNode, error) { v := token.(float32) c.setValue(&v) return c, nil - case float64: + case int32: c := &JsonParseNode{} v := token.(int32) c.setValue(&v) return c, nil - case byte: + case int64: c := &JsonParseNode{} v := token.(int64) c.setValue(&v) From 5b6093f7480c54d263d94d5b00f9dc83e41b23c1 Mon Sep 17 00:00:00 2001 From: Michael Canady Date: Tue, 13 Aug 2024 10:12:12 -0400 Subject: [PATCH 11/11] added change log entry --- CHANGELOG.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe7349..9b51a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.8] - 2024-08-13 + +### Changed + +- Modified how number values are derived, allowing values to be cast as the various types. + +### Fixed + +- Panicing when type is asserted to be what it isn't. + ## [1.0.7] - 2024-02-29 ### Added @@ -135,7 +145,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.5.5] - 2022-07-12 -- Fixed bug where string literals of `\t` and `\r` would result in generating an invalid JSON. +- Fixed bug where string literals of `\t` and `\r` would result in generating an invalid JSON. ### Changed @@ -161,14 +171,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - - Updated supported types for Additional Data, unsupported types now throwing an error instead of ignoring. - - Changed logic that trims excessive commas to be called only once on serialization. +- Updated supported types for Additional Data, unsupported types now throwing an error instead of ignoring. +- Changed logic that trims excessive commas to be called only once on serialization. ## [0.5.0] - 2022-05-26 ### Changed - - Updated reference to abstractions to support enum responses. +- Updated reference to abstractions to support enum responses. ## [0.4.0] - 2022-05-19