diff --git a/pkg/schema/field/array.go b/pkg/schema/field/array.go index bf1a4819dd075fc5e3f46fc4d1e289c8bd75cf90..682b6e5e7774cbf98e93fc31261e114b574049e7 100644 --- a/pkg/schema/field/array.go +++ b/pkg/schema/field/array.go @@ -132,24 +132,29 @@ func (ArrayType) Walk(ctx context.Context, field *Field, v interface{}, fn WalkF return nil, false, nil } - arr, ok := v.([]interface{}) - if !ok && v != nil { - return nil, false, fmt.Errorf("incorrect type: \"%s\", expected \"[]interface{}\"", reflect.ValueOf(v).Kind()) + arr := reflect.ValueOf(v) + if v != nil && arr.Kind() != reflect.Slice && arr.Kind() != reflect.Array { + return nil, false, fmt.Errorf("incorrect type: %s, expected array or slice", arr.Kind()) + } + + var length int + if v != nil { + length = arr.Len() } // Выполняется обход по схеме - if opts.WalkSchema && len(arr) == 0 { + if opts.WalkSchema && length == 0 { _, _, _ = params.Item.Walk(ctx, nil, fn, WalkOpts(opts)) return nil, false, nil } - m := make([]interface{}, 0, len(arr)) + m := make([]interface{}, 0, length) var merr *multierror.Error - for i, value := range arr { + for i := range length { + value := arr.Index(i).Interface() valueNew, valueChanged, err := params.Item.Walk(ctx, value, fn, WalkOpts(opts)) - if err != nil { merr = multierror.Append(merr, errors.WithField(err, strconv.Itoa(i))) } diff --git a/pkg/schema/field/array_test.go b/pkg/schema/field/array_test.go index b2948e1b12161da604ed22da96e4b8a092c0a99f..ab8eae74376154ba89ce5a41ebc885da7bc15a1a 100644 --- a/pkg/schema/field/array_test.go +++ b/pkg/schema/field/array_test.go @@ -48,7 +48,7 @@ func TestArrayField_Decode(t *testing.T) { "Incorrect type", Array(Number("int")), "1 2 3", - "decode error: incorrect type: \"string\", expected \"[]interface{}\"", + "decode error: incorrect type: string, expected array or slice", true, }, } @@ -86,7 +86,7 @@ func TestArrayField_Encode(t *testing.T) { "Incorrect type", Array(Number("int")), "1 2 3", - "encode error: incorrect type: \"string\", expected \"[]interface{}\"", + "encode error: incorrect type: string, expected array or slice", true, }, } @@ -125,6 +125,18 @@ func TestArrayType_Walk(t *testing.T) { want1: false, wantErr: assert.NoError, }, + { + name: "With nil data and WalkSchema = true", + field: Array(Object("a", String(), "b", String())), + v: nil, + opts: &WalkOptions{WalkSchema: true}, + want: nil, + want1: false, + fn: func(_ context.Context, _ *Field, _ interface{}) (WalkFuncResult, error) { + return WalkFuncResult{}, nil + }, + wantErr: assert.NoError, + }, { name: "With empty data and WalkSchema = false", field: Array(Object("a", String(), "b", String())), @@ -162,3 +174,96 @@ func TestArrayType_Walk(t *testing.T) { }) } } + +type customFloat float64 +type customInt int +type customStr string +type customMap map[string]interface{} + +func TestArrayField_WithType(t *testing.T) { + t.Run("Nil", func(t *testing.T) { + got, err := Decode(context.Background(), Array(Number("float")), nil) + require.NoError(t, err) + assert.ElementsMatch(t, got, nil) + + got, err = Encode(context.Background(), Array(Number("float")), nil) + require.NoError(t, err) + assert.ElementsMatch(t, got, nil) + }) + t.Run("With []float64", func(t *testing.T) { + got, err := Decode(context.Background(), Array(Number("float")), []float64{1.0, 2.0}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{1.0, 2.0}) + + got, err = Encode(context.Background(), Array(Number("float")), []float64{1.0, 2.0}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{1.0, 2.0}) + }) + t.Run("With []int", func(t *testing.T) { + got, err := Decode(context.Background(), Array(Number("int")), []int{1, 2}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{int64(1), int64(2)}) + + got, err = Encode(context.Background(), Array(Number("int")), []int{1, 2}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{int64(1), int64(2)}) + }) + t.Run("With []string", func(t *testing.T) { + got, err := Decode(context.Background(), Array(String()), []string{"1", "2"}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{"1", "2"}) + + got, err = Encode(context.Background(), Array(String()), []string{"1", "2"}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{"1", "2"}) + }) + t.Run("With []map", func(t *testing.T) { + got, err := Decode(context.Background(), Array(Object("a", String(), "b", String())), + []map[string]interface{}{{"a": "1", "b": "2"}}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{map[string]interface{}{"a": "1", "b": "2"}}) + + got, err = Encode(context.Background(), Array(Object("a", String(), "b", String())), + []map[string]interface{}{{"a": "1", "b": "2"}}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{map[string]interface{}{"a": "1", "b": "2"}}) + }) + t.Run("With []customFloat", func(t *testing.T) { + got, err := Decode(context.Background(), Array(Number("float")), []customFloat{1.0, 2.0}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{1.0, 2.0}) + + got, err = Encode(context.Background(), Array(Number("float")), []customFloat{1.0, 2.0}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{1.0, 2.0}) + }) + t.Run("With []customInt", func(t *testing.T) { + got, err := Decode(context.Background(), Array(Number("int")), []customInt{1, 2}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{int64(1), int64(2)}) + + got, err = Encode(context.Background(), Array(Number("int")), []customInt{1, 2}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{int64(1), int64(2)}) + }) + t.Run("With []customStr", func(t *testing.T) { + got, err := Decode(context.Background(), Array(String()), []customStr{"1", "2"}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{"1", "2"}) + + got, err = Encode(context.Background(), Array(String()), []customStr{"1", "2"}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{"1", "2"}) + }) + t.Run("With []customMap", func(t *testing.T) { + got, err := Decode(context.Background(), Array(Object("a", String(), "b", String())), + []customMap{{"a": "1", "b": "2"}}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{map[string]interface{}{"a": "1", "b": "2"}}) + + got, err = Encode(context.Background(), Array(Object("a", String(), "b", String())), + []customMap{{"a": "1", "b": "2"}}) + require.NoError(t, err) + assert.ElementsMatch(t, got, []interface{}{map[string]interface{}{"a": "1", "b": "2"}}) + }) +} diff --git a/pkg/schema/field/number.go b/pkg/schema/field/number.go index dfc8a6635cc0e719bd55e9f680ba23688674edd0..16673917895c4967c5b9ca65c5b6d1a5b397b045 100644 --- a/pkg/schema/field/number.go +++ b/pkg/schema/field/number.go @@ -3,6 +3,7 @@ package field import ( "context" "math" + "reflect" "github.com/pkg/errors" ) @@ -41,6 +42,20 @@ func (NumberType) IsEmpty(v interface{}) bool { return v == nil } +func toNumberReflect(i interface{}) (interface{}, error) { + v := reflect.ValueOf(i) + switch v.Kind() { //nolint:exhaustive //not necessary to add all types + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int(), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint(), nil + case reflect.Float32, reflect.Float64: + return v.Float(), nil + default: + return 0, errors.Errorf("error convert \"%s\" to number", i) + } +} + func ToNumber(i interface{}) (interface{}, error) { switch v := i.(type) { case int64: @@ -63,8 +78,9 @@ func ToNumber(i interface{}) (interface{}, error) { return float64(v), nil case float64: return v, nil + default: + return toNumberReflect(v) } - return 0, errors.Errorf("error convert \"%s\" to number", i) } func (n NumberType) Decode(ctx context.Context, field *Field, v interface{}) (interface{}, error) { diff --git a/pkg/schema/field/number_test.go b/pkg/schema/field/number_test.go index 367ffe4249f36f2dff17aa1e61ef6a753ccabee8..26d76f9527e2a74c3232ff2831660fee02528be2 100644 --- a/pkg/schema/field/number_test.go +++ b/pkg/schema/field/number_test.go @@ -5,8 +5,12 @@ import ( "math" "reflect" "testing" + + "git.perx.ru/perxis/perxis-go/pkg/errors" ) +type CustomInt int + func TestNumberField_Decode(t *testing.T) { tests := []struct { name string @@ -21,6 +25,7 @@ func TestNumberField_Decode(t *testing.T) { {"Correct", Number("int"), float32(2.2), int64(2), false}, // #3 {"Correct", Number("int"), float64(2.6), int64(3), false}, // #4 {"Correct", Number("int"), 2.6, int64(3), false}, // #5 + {"Correct", Number("int"), CustomInt(2), int64(2), false}, // #5 {"MaxInt64", Number(NumberFormatInt), int64(math.MaxInt64), nil, true}, // #6 {"MinInt64", Number(NumberFormatInt), int64(math.MinInt64), nil, true}, // #7 {"maxInt in float", Number(NumberFormatInt), float64(maxInt), int64(maxInt), false}, // #8 @@ -94,3 +99,152 @@ func TestNumberField_Encode(t *testing.T) { }) } } + +// Добавлен полные функции, чтобы результаты теста не поменялись, в случае изменения Decode +// Реализация cast type. +func toNumber(i interface{}) (interface{}, error) { + switch v := i.(type) { + case int64: + return v, nil + case int: + return int64(v), nil + case int8: + return int64(v), nil + case int32: + return int64(v), nil + case uint64: + return v, nil + case uint: + return uint64(v), nil + case uint8: + return uint64(v), nil + case uint32: + return uint64(v), nil + case float32: + return float64(v), nil + case float64: + return v, nil + } + return 0, errors.Errorf("error convert \"%s\" to number", i) +} +func decodeNumber(field *Field, v interface{}) (interface{}, error) { + params, ok := field.Params.(*NumberParameters) + if !ok { + return nil, errors.New("field parameters required") + } + + if v == nil { + return v, nil + } + + n, err := toNumber(v) + if err != nil { + return nil, err + } + + switch params.Format { + case NumberFormatInt: + switch i := n.(type) { + case int64: + if i > maxInt || i < minInt { + return nil, errors.New("integer out of range") + } + return i, nil + case uint64: + if i > maxInt { + return nil, errors.New("integer out of range") + } + return i, nil + case float64: + if i > maxInt || i < minInt { + return nil, errors.New("integer out of range") + } + return int64(math.Round(i)), nil + } + case NumberFormatFloat: + switch i := n.(type) { + case float64: + return i, nil + case int64: + return float64(i), nil + case uint64: + return float64(i), nil + } + } + return n, nil +} + +// Реализация reflect. +func decodeNumberReflect(field *Field, v interface{}) (interface{}, error) { //nolint:unparam // test case + params, ok := field.Params.(*NumberParameters) + if !ok { + return nil, errors.New("field parameters required") + } + + if v == nil { + return v, nil + } + + n, err := toNumberReflect(v) // вызов метода reflect + if err != nil { + return nil, err + } + + switch params.Format { + case NumberFormatInt: + switch i := n.(type) { + case int64: + if i > maxInt || i < minInt { + return nil, errors.New("integer out of range") + } + return i, nil + case uint64: + if i > maxInt { + return nil, errors.New("integer out of range") + } + return i, nil + case float64: + if i > maxInt || i < minInt { + return nil, errors.New("integer out of range") + } + return int64(math.Round(i)), nil + } + case NumberFormatFloat: + switch i := n.(type) { + case float64: + return i, nil + case int64: + return float64(i), nil + case uint64: + return float64(i), nil + } + } + return n, nil +} + +// команда запуска go test -bench=. +func BenchmarkDecodeReflectNumber(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = decodeNumberReflect(Number(NumberFormatInt), i) + } +} +func BenchmarkDecodeReflectCustomNumber(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = decodeNumberReflect(Number(NumberFormatInt), customInt(i)) + } +} +func BenchmarkDecodeNumber(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = decodeNumber(Number(NumberFormatInt), i) + } +} +func BenchmarkDecodeNumberCurrentSolution(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = NumberType{}.decode(context.Background(), Number(NumberFormatInt), i) + } +} +func BenchmarkDecodeCustomNumberCurrentSolution(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = NumberType{}.decode(context.Background(), Number(NumberFormatInt), customInt(i)) + } +} diff --git a/pkg/schema/field/string.go b/pkg/schema/field/string.go index 17b5e7ae086d3fac1b25d8c332c33d83fecedfb8..96b4e8dd4d4d0d193a91718698906dda15c0cc85 100644 --- a/pkg/schema/field/string.go +++ b/pkg/schema/field/string.go @@ -37,6 +37,10 @@ func (StringType) Decode(_ context.Context, _ *Field, v interface{}) (interface{ if _, ok := v.(string); ok { return v, nil } + t := reflect.ValueOf(v) + if t.Kind() == reflect.String { + return t.String(), nil + } return nil, fmt.Errorf("StringField decode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind()) } @@ -47,6 +51,10 @@ func (StringType) Encode(_ context.Context, _ *Field, v interface{}) (interface{ if _, ok := v.(string); ok { return v, nil } + t := reflect.ValueOf(v) + if t.Kind() == reflect.String { + return t.String(), nil + } return nil, fmt.Errorf("StringField encode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind()) } diff --git a/pkg/schema/field/string_test.go b/pkg/schema/field/string_test.go index 16b7538fb86797946a4a3c3ba709e6e30cf4d5d8..c858f607a7e13719152b3308773024e4d1919e5a 100644 --- a/pkg/schema/field/string_test.go +++ b/pkg/schema/field/string_test.go @@ -2,10 +2,14 @@ package field import ( "context" + "fmt" "reflect" + "strconv" "testing" ) +type customString string + func TestStringField_Decode(t *testing.T) { tests := []struct { name string @@ -15,6 +19,7 @@ func TestStringField_Decode(t *testing.T) { wantErr bool }{ {"Correct", String(), "string", "string", false}, + {"Correct", String(), customString("string"), "string", false}, {"Wrong data", String(), 2, nil, true}, } for _, tt := range tests { @@ -55,3 +60,54 @@ func TestStringField_Encode(t *testing.T) { }) } } + +// Добавлен полные функции, чтобы результаты теста не поменялись, в случае изменения Decode +// Старая реализация. +func decodeStringReflect(v interface{}) (interface{}, error) { //nolint:unparam // test case + if v == nil { + return nil, nil //nolint:nilnil // test case + } + t := reflect.ValueOf(v) + if t.Kind() == reflect.String { + return t.String(), nil + } + return nil, fmt.Errorf("StringField decode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind()) +} + +// Новая реализация. +func decodeString(v interface{}) (interface{}, error) { + if v == nil { + return nil, nil //nolint:nilnil // test case + } + if _, ok := v.(string); ok { + return v, nil + } + return nil, fmt.Errorf("StringField decode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind()) +} + +// команда запуска go test -bench=. +func BenchmarkDecodeStringReflect(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = decodeStringReflect(strconv.Itoa(i)) + } +} +func BenchmarkDecodeCustomStringReflect(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = decodeStringReflect(customString(strconv.Itoa(i))) + } +} +func BenchmarkDecodeString(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = decodeString(strconv.Itoa(i)) + } +} +func BenchmarkDecodeStringCurrentSolution(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = StringType{}.Decode(context.Background(), nil, strconv.Itoa(i)) + } +} +func BenchmarkDecodeCustomStringCurrentSolution(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = StringType{}.Decode(context.Background(), nil, customString(strconv.Itoa(i))) + } +} diff --git a/pkg/schema/validate/array_test.go b/pkg/schema/validate/array_test.go index ec9e9d818d5b86d7fe9e8647d862e73f44232c65..465401779153449ad04d342d41d4e7990e0fa302 100644 --- a/pkg/schema/validate/array_test.go +++ b/pkg/schema/validate/array_test.go @@ -50,8 +50,8 @@ func TestArrayValidate(t *testing.T) { }{ {"Nil Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), nil, false, ""}, {"Nil Min Items", field.Array(field.String()).AddOptions(MinItems(1)), nil, false, ""}, - {"Array Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), [1]interface{}{1}, true, "validation error: incorrect type: \"array\", expected \"[]interface{}\""}, - {"Array Min Items", field.Array(field.String()).AddOptions(MinItems(1)), [1]interface{}{1}, true, "validation error: incorrect type: \"array\", expected \"[]interface{}\""}, + {"Array Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), [1]interface{}{1}, false, ""}, + {"Array Min Items", field.Array(field.String()).AddOptions(MinItems(1)), [1]interface{}{1}, false, ""}, {"Slice Max Items", field.Array(field.String()).AddOptions(MaxItems(0)), []interface{}{}, false, ""}, {"Slice Min Items", field.Array(field.String()).AddOptions(MinItems(0)), []interface{}{}, false, ""}, {"Bool Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), true, true, "validation error: incorrect type: \"bool\", expected \"array\""},