Skip to content
Snippets Groups Projects
Commit 23f78314 authored by Pavel Antonov's avatar Pavel Antonov :asterisk:
Browse files

Merge branch 'feature/2890-ArrayInSchema' into 'master'

Добавлена возможность использования в схеме типов соответствующих данным массива, строки и числа

See merge request perxis/perxis-go!398
parents 91e0fd73 c55180ef
No related branches found
No related tags found
No related merge requests found
......@@ -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)))
}
......
......@@ -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"}})
})
}
......@@ -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) {
......
......@@ -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))
}
}
......@@ -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())
}
......
......@@ -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)))
}
}
......@@ -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\""},
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment