From 12fe3557332a9e8b6f3222115ca27fdd3da97da6 Mon Sep 17 00:00:00 2001 From: zak905 Date: Fri, 30 Jul 2021 16:06:14 +0200 Subject: [PATCH 1/5] add possibility to set default values when decoding using 'default' tag --- cache.go | 22 ++++---- converter.go | 49 +++++++++++++++++ decoder.go | 34 ++++++++++++ decoder_test.go | 140 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 236 insertions(+), 9 deletions(-) diff --git a/cache.go b/cache.go index bf21697..5f9d397 100644 --- a/cache.go +++ b/cache.go @@ -17,19 +17,21 @@ var errInvalidPath = errors.New("schema: invalid path") // newCache returns a new cache. func newCache() *cache { c := cache{ - m: make(map[reflect.Type]*structInfo), - regconv: make(map[reflect.Type]Converter), - tag: "schema", + m: make(map[reflect.Type]*structInfo), + regconv: make(map[reflect.Type]Converter), + tag: "schema", + defaultTag: "default", } return &c } // cache caches meta-data about a struct. type cache struct { - l sync.RWMutex - m map[reflect.Type]*structInfo - regconv map[reflect.Type]Converter - tag string + l sync.RWMutex + m map[reflect.Type]*structInfo + regconv map[reflect.Type]Converter + tag string + defaultTag string } // registerConverter registers a converter function for a custom type. @@ -197,6 +199,7 @@ func (c *cache) createField(field reflect.StructField, parentAlias string) *fiel isSliceOfStructs: isSlice && isStruct, isAnonymous: field.Anonymous, isRequired: options.Contains("required"), + defaultValue: field.Tag.Get(c.defaultTag), } } @@ -246,8 +249,9 @@ type fieldInfo struct { // isSliceOfStructs indicates if the field type is a slice of structs. isSliceOfStructs bool // isAnonymous indicates whether the field is embedded in the struct. - isAnonymous bool - isRequired bool + isAnonymous bool + isRequired bool + defaultValue string } func (f *fieldInfo) paths(prefix string) []string { diff --git a/converter.go b/converter.go index 4f2116a..9adf7fe 100644 --- a/converter.go +++ b/converter.go @@ -143,3 +143,52 @@ func convertUint64(value string) reflect.Value { } return invalidValue } + +func convertPointer(k reflect.Kind, value string) reflect.Value { + switch k { + case boolType: + v := convertBool(value).Bool() + return reflect.ValueOf(&v) + case float32Type: + v := float32(convertFloat32(value).Float()) + return reflect.ValueOf(&v) + case float64Type: + v := float64(convertFloat64(value).Float()) + return reflect.ValueOf(&v) + case intType: + v := int(convertInt(value).Int()) + return reflect.ValueOf(&v) + case int8Type: + v := int8(convertInt8(value).Int()) + return reflect.ValueOf(&v) + case int16Type: + v := int16(convertInt16(value).Int()) + return reflect.ValueOf(&v) + case int32Type: + v := int32(convertInt32(value).Int()) + return reflect.ValueOf(&v) + case int64Type: + v := int64(convertInt64(value).Int()) + return reflect.ValueOf(&v) + case stringType: + v := convertString(value).String() + return reflect.ValueOf(&v) + case uintType: + v := uint(convertUint(value).Uint()) + return reflect.ValueOf(&v) + case uint8Type: + v := uint8(convertUint8(value).Uint()) + return reflect.ValueOf(&v) + case uint16Type: + v := uint16(convertUint16(value).Uint()) + return reflect.ValueOf(&v) + case uint32Type: + v := uint32(convertUint32(value).Uint()) + return reflect.ValueOf(&v) + case uint64Type: + v := uint64(convertUint64(value).Uint()) + return reflect.ValueOf(&v) + } + + return invalidValue +} diff --git a/decoder.go b/decoder.go index 28b560b..6fe07eb 100644 --- a/decoder.go +++ b/decoder.go @@ -84,6 +84,9 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { errors[path] = UnknownKeyError{Key: path} } } + if err := d.setDefaults(t, v); err != nil { + errors.merge(MultiError{"defaults": err}) + } errors.merge(d.checkRequired(t, src)) if len(errors) > 0 { return errors @@ -91,6 +94,37 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { return nil } +func (d *Decoder) setDefaults(t reflect.Type, v reflect.Value) error { + struc := d.cache.get(t) + if struc == nil { + // unexpect, cache.get never return nil + return errors.New("cache fail") + } + + for i, f := range struc.fields { + if f.defaultValue != "" && f.isRequired { + return errors.New("required fields cannot have a default value") + } else if f.defaultValue != "" && v.Field(i).IsZero() && !f.isRequired { + if f.typ.Kind() == reflect.Struct || f.typ.Kind() == reflect.Slice { + return errors.New("default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers") + } else if f.typ.Kind() == reflect.Ptr { + t1 := f.typ.Elem() + + if t1.Kind() == reflect.Struct || t1.Kind() == reflect.Slice { + return errors.New("default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers") + } + + v.Field(i).Set(convertPointer(t1.Kind(), f.defaultValue)) + } else { + v.Field(i).Set(builtinConverters[f.typ.Kind()](f.defaultValue)) + } + + } + } + + return nil +} + // checkRequired checks whether required fields are empty // // check type t recursively if t has struct fields. diff --git a/decoder_test.go b/decoder_test.go index f89a4c3..e35cc8b 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -2055,3 +2055,143 @@ func TestUnmashalPointerToEmbedded(t *testing.T) { t.Errorf("Expected %v errors, got %v", expected, s.Value) } } + +func TestDefaultValuesAreSet(t *testing.T) { + + type D struct { + S string `schema:"s" default:"test1"` + I int `schema:"i" default:"21"` + B bool `schema:"b" default:"false"` + F float64 `schema:"f" default:"3.14"` + U uint `schema:"u" default:"1"` + } + + data := map[string][]string{} + + d := D{} + + decoder := NewDecoder() + + if err := decoder.Decode(&d, data); err != nil { + t.Fatal("Error while decoding:", err) + } + + expected := D{ + S: "test1", + I: 21, + B: false, + F: 3.14, + U: 1, + } + + if !reflect.DeepEqual(expected, d) { + t.Errorf("Expected %v, got %v", expected, d) + } + + type P struct { + S *string `schema:"s" default:"test1"` + I *int `schema:"i" default:"21"` + B *bool `schema:"b" default:"false"` + F *float64 `schema:"f" default:"3.14"` + U *uint `schema:"u" default:"1"` + } + + p := P{} + + if err := decoder.Decode(&p, data); err != nil { + t.Fatal("Error while decoding:", err) + } + + vExpected := reflect.ValueOf(expected) + vActual := reflect.ValueOf(p) + + i := 0 + + for i < vExpected.NumField() { + if !reflect.DeepEqual(vExpected.Field(i).Interface(), reflect.Indirect(vActual.Field(i)).Interface()) { + t.Errorf("Expected %v, got %v", vExpected.Field(i).Interface(), reflect.Indirect(vActual.Field(i)).Interface()) + } + i++ + } +} + +func TestDefaultValuesAreIgnoredIfValuesAreProvided(t *testing.T) { + type D struct { + S string `schema:"s" default:"test1"` + I int `schema:"i" default:"21"` + B bool `schema:"b" default:"false"` + F float64 `schema:"f" default:"3.14"` + U uint `schema:"u" default:"1"` + } + + data := map[string][]string{"s": {"s"}, "i": {"1"}, "b": {"true"}, "f": {"0.22"}, "u": {"14"}} + + d := D{} + + decoder := NewDecoder() + + if err := decoder.Decode(&d, data); err != nil { + t.Fatal("Error while decoding:", err) + } + + expected := D{ + S: "s", + I: 1, + B: true, + F: 0.22, + U: 14, + } + + if !reflect.DeepEqual(expected, d) { + t.Errorf("Expected %v, got %v", expected, d) + } +} + +func TestRequiredFieldsCannotHaveDefaults(t *testing.T) { + + type D struct { + S string `schema:"s,required" default:"test1"` + I int `schema:"i,required" default:"21"` + B bool `schema:"b,required" default:"false"` + F float64 `schema:"f,required" default:"3.14"` + U uint `schema:"u,required" default:"1"` + } + + data := map[string][]string{"s": {"s"}, "i": {"1"}, "b": {"true"}, "f": {"0.22"}, "u": {"14"}} + + d := D{} + + decoder := NewDecoder() + + err := decoder.Decode(&d, data) + + expected := "required fields cannot have a default value" + + if err == nil || err.Error() != expected { + t.Errorf("decoding should fail with error msg %s got %q", expected, err) + } + +} + +func TestDefaultsAreNotSupportedForStructsAndSlices(t *testing.T) { + + type D struct { + S S1 `schema:"s" default:"{f1:0}"` + A []string `schema:"s" default:"test1,test2"` + } + + d := D{} + + data := map[string][]string{} + + decoder := NewDecoder() + + err := decoder.Decode(&d, data) + + expected := "default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers" + + if err == nil || err.Error() != expected { + t.Errorf("decoding should fail with error msg %s got %q", expected, err) + } + +} From c0f761481f50b9fb8366af8aac99c88443219a26 Mon Sep 17 00:00:00 2001 From: zak905 Date: Sun, 1 Aug 2021 17:07:46 +0200 Subject: [PATCH 2/5] improve setting defaults to include nested structs --- decoder.go | 41 ++++++++++++++++++++++++++++------------- decoder_test.go | 19 ++++++++++++++++--- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/decoder.go b/decoder.go index 6fe07eb..336e921 100644 --- a/decoder.go +++ b/decoder.go @@ -84,9 +84,7 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { errors[path] = UnknownKeyError{Key: path} } } - if err := d.setDefaults(t, v); err != nil { - errors.merge(MultiError{"defaults": err}) - } + errors.merge(d.setDefaults(t, v)) errors.merge(d.checkRequired(t, src)) if len(errors) > 0 { return errors @@ -94,35 +92,52 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { return nil } -func (d *Decoder) setDefaults(t reflect.Type, v reflect.Value) error { +//setDefaults sets the default values when the `default` tag is specified, +//default is supported on basic/primitive types and their pointers, +//nested structs can also have default tags +func (d *Decoder) setDefaults(t reflect.Type, v reflect.Value) MultiError { struc := d.cache.get(t) if struc == nil { // unexpect, cache.get never return nil - return errors.New("cache fail") + return MultiError{"default-" + t.Name(): errors.New("cache fail")} } - for i, f := range struc.fields { + errs := MultiError{} + + for _, f := range struc.fields { + vCurrent := v.FieldByName(f.name) + + if vCurrent.Type().Kind() == reflect.Struct && f.defaultValue == "" { + errs.merge(d.setDefaults(vCurrent.Type(), vCurrent)) + } else if isPointerToStruct(vCurrent) && f.defaultValue == "" { + errs.merge(d.setDefaults(vCurrent.Elem().Type(), vCurrent.Elem())) + } + if f.defaultValue != "" && f.isRequired { - return errors.New("required fields cannot have a default value") - } else if f.defaultValue != "" && v.Field(i).IsZero() && !f.isRequired { + errs.merge(MultiError{"default-" + f.name: errors.New("required fields cannot have a default value")}) + } else if f.defaultValue != "" && vCurrent.IsZero() && !f.isRequired { if f.typ.Kind() == reflect.Struct || f.typ.Kind() == reflect.Slice { - return errors.New("default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers") + errs.merge(MultiError{"default-" + f.name: errors.New("default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers")}) } else if f.typ.Kind() == reflect.Ptr { t1 := f.typ.Elem() if t1.Kind() == reflect.Struct || t1.Kind() == reflect.Slice { - return errors.New("default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers") + errs.merge(MultiError{"default-" + f.name: errors.New("default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers")}) } - v.Field(i).Set(convertPointer(t1.Kind(), f.defaultValue)) + vCurrent.Set(convertPointer(t1.Kind(), f.defaultValue)) } else { - v.Field(i).Set(builtinConverters[f.typ.Kind()](f.defaultValue)) + vCurrent.Set(builtinConverters[f.typ.Kind()](f.defaultValue)) } } } - return nil + return errs +} + +func isPointerToStruct(v reflect.Value) bool { + return !v.IsZero() && v.Type().Kind() == reflect.Ptr && v.Elem().Type().Kind() == reflect.Struct } // checkRequired checks whether required fields are empty diff --git a/decoder_test.go b/decoder_test.go index e35cc8b..d8a975e 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -2058,7 +2058,15 @@ func TestUnmashalPointerToEmbedded(t *testing.T) { func TestDefaultValuesAreSet(t *testing.T) { + //TODO: Test nesting and update repos + + type N struct { + S1 string `schema:"s1" default:"test1"` + I2 int `schema:"i2" default:"22"` + } + type D struct { + N S string `schema:"s" default:"test1"` I int `schema:"i" default:"21"` B bool `schema:"b" default:"false"` @@ -2077,6 +2085,10 @@ func TestDefaultValuesAreSet(t *testing.T) { } expected := D{ + N: N{ + S1: "test1", + I2: 22, + }, S: "test1", I: 21, B: false, @@ -2089,6 +2101,7 @@ func TestDefaultValuesAreSet(t *testing.T) { } type P struct { + *N S *string `schema:"s" default:"test1"` I *int `schema:"i" default:"21"` B *bool `schema:"b" default:"false"` @@ -2096,7 +2109,7 @@ func TestDefaultValuesAreSet(t *testing.T) { U *uint `schema:"u" default:"1"` } - p := P{} + p := P{N: &N{}} if err := decoder.Decode(&p, data); err != nil { t.Fatal("Error while decoding:", err) @@ -2167,7 +2180,7 @@ func TestRequiredFieldsCannotHaveDefaults(t *testing.T) { expected := "required fields cannot have a default value" - if err == nil || err.Error() != expected { + if err == nil || !strings.Contains(err.Error(), expected) { t.Errorf("decoding should fail with error msg %s got %q", expected, err) } @@ -2190,7 +2203,7 @@ func TestDefaultsAreNotSupportedForStructsAndSlices(t *testing.T) { expected := "default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers" - if err == nil || err.Error() != expected { + if err == nil || !strings.Contains(err.Error(), expected) { t.Errorf("decoding should fail with error msg %s got %q", expected, err) } From a128c31dd90b9c318d2e89c27f556bb0b980915a Mon Sep 17 00:00:00 2001 From: zak905 Date: Sat, 12 Mar 2022 16:47:37 +0100 Subject: [PATCH 3/5] use option instead of tag --- cache.go | 31 +++++++++++++++++++---------- decoder.go | 4 ++-- decoder_test.go | 53 +++++++++++++++++++++++-------------------------- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/cache.go b/cache.go index 5f9d397..2c02890 100644 --- a/cache.go +++ b/cache.go @@ -17,21 +17,19 @@ var errInvalidPath = errors.New("schema: invalid path") // newCache returns a new cache. func newCache() *cache { c := cache{ - m: make(map[reflect.Type]*structInfo), - regconv: make(map[reflect.Type]Converter), - tag: "schema", - defaultTag: "default", + m: make(map[reflect.Type]*structInfo), + regconv: make(map[reflect.Type]Converter), + tag: "schema", } return &c } // cache caches meta-data about a struct. type cache struct { - l sync.RWMutex - m map[reflect.Type]*structInfo - regconv map[reflect.Type]Converter - tag string - defaultTag string + l sync.RWMutex + m map[reflect.Type]*structInfo + regconv map[reflect.Type]Converter + tag string } // registerConverter registers a converter function for a custom type. @@ -199,7 +197,7 @@ func (c *cache) createField(field reflect.StructField, parentAlias string) *fiel isSliceOfStructs: isSlice && isStruct, isAnonymous: field.Anonymous, isRequired: options.Contains("required"), - defaultValue: field.Tag.Get(c.defaultTag), + defaultValue: options.getDefaultOptionValue(), } } @@ -307,3 +305,16 @@ func (o tagOptions) Contains(option string) bool { } return false } + +func (o tagOptions) getDefaultOptionValue() string { + for _, s := range o { + if strings.HasPrefix(s, "default:") { + if t := strings.Split(s, ":"); len(t) > 0 { + return t[1] + } + break + } + } + + return "" +} diff --git a/decoder.go b/decoder.go index 336e921..832a9cf 100644 --- a/decoder.go +++ b/decoder.go @@ -117,12 +117,12 @@ func (d *Decoder) setDefaults(t reflect.Type, v reflect.Value) MultiError { errs.merge(MultiError{"default-" + f.name: errors.New("required fields cannot have a default value")}) } else if f.defaultValue != "" && vCurrent.IsZero() && !f.isRequired { if f.typ.Kind() == reflect.Struct || f.typ.Kind() == reflect.Slice { - errs.merge(MultiError{"default-" + f.name: errors.New("default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers")}) + errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers")}) } else if f.typ.Kind() == reflect.Ptr { t1 := f.typ.Elem() if t1.Kind() == reflect.Struct || t1.Kind() == reflect.Slice { - errs.merge(MultiError{"default-" + f.name: errors.New("default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers")}) + errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers")}) } vCurrent.Set(convertPointer(t1.Kind(), f.defaultValue)) diff --git a/decoder_test.go b/decoder_test.go index d8a975e..c1cc449 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -2058,20 +2058,18 @@ func TestUnmashalPointerToEmbedded(t *testing.T) { func TestDefaultValuesAreSet(t *testing.T) { - //TODO: Test nesting and update repos - type N struct { - S1 string `schema:"s1" default:"test1"` - I2 int `schema:"i2" default:"22"` + S1 string `schema:"s1,default:test1"` + I2 int `schema:"i2,default:22"` } type D struct { N - S string `schema:"s" default:"test1"` - I int `schema:"i" default:"21"` - B bool `schema:"b" default:"false"` - F float64 `schema:"f" default:"3.14"` - U uint `schema:"u" default:"1"` + S string `schema:"s,default:test1"` + I int `schema:"i,default:21"` + B bool `schema:"b,default:false"` + F float64 `schema:"f,default:3.14"` + U uint `schema:"u,default:1"` } data := map[string][]string{} @@ -2102,11 +2100,11 @@ func TestDefaultValuesAreSet(t *testing.T) { type P struct { *N - S *string `schema:"s" default:"test1"` - I *int `schema:"i" default:"21"` - B *bool `schema:"b" default:"false"` - F *float64 `schema:"f" default:"3.14"` - U *uint `schema:"u" default:"1"` + S *string `schema:"s,default:test1"` + I *int `schema:"i,default:21"` + B *bool `schema:"b,default:false"` + F *float64 `schema:"f,default:3.14"` + U *uint `schema:"u,default:1"` } p := P{N: &N{}} @@ -2130,11 +2128,11 @@ func TestDefaultValuesAreSet(t *testing.T) { func TestDefaultValuesAreIgnoredIfValuesAreProvided(t *testing.T) { type D struct { - S string `schema:"s" default:"test1"` - I int `schema:"i" default:"21"` - B bool `schema:"b" default:"false"` - F float64 `schema:"f" default:"3.14"` - U uint `schema:"u" default:"1"` + S string `schema:"s,default:test1"` + I int `schema:"i,default:21"` + B bool `schema:"b,default:false"` + F float64 `schema:"f,default:3.14"` + U uint `schema:"u,default:1"` } data := map[string][]string{"s": {"s"}, "i": {"1"}, "b": {"true"}, "f": {"0.22"}, "u": {"14"}} @@ -2163,11 +2161,11 @@ func TestDefaultValuesAreIgnoredIfValuesAreProvided(t *testing.T) { func TestRequiredFieldsCannotHaveDefaults(t *testing.T) { type D struct { - S string `schema:"s,required" default:"test1"` - I int `schema:"i,required" default:"21"` - B bool `schema:"b,required" default:"false"` - F float64 `schema:"f,required" default:"3.14"` - U uint `schema:"u,required" default:"1"` + S string `schema:"s,required,default:test1"` + I int `schema:"i,required,default:21"` + B bool `schema:"b,required,default:false"` + F float64 `schema:"f,required,default:3.14"` + U uint `schema:"u,required,default:1"` } data := map[string][]string{"s": {"s"}, "i": {"1"}, "b": {"true"}, "f": {"0.22"}, "u": {"14"}} @@ -2189,8 +2187,8 @@ func TestRequiredFieldsCannotHaveDefaults(t *testing.T) { func TestDefaultsAreNotSupportedForStructsAndSlices(t *testing.T) { type D struct { - S S1 `schema:"s" default:"{f1:0}"` - A []string `schema:"s" default:"test1,test2"` + S S1 `schema:"s,default:{f1:0}"` + A []string `schema:"s,default:test1,test2"` } d := D{} @@ -2201,10 +2199,9 @@ func TestDefaultsAreNotSupportedForStructsAndSlices(t *testing.T) { err := decoder.Decode(&d, data) - expected := "default tag is supported only on: bool, float variants, string, unit variants types or their corresponding pointers" + expected := "default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers" if err == nil || !strings.Contains(err.Error(), expected) { t.Errorf("decoding should fail with error msg %s got %q", expected, err) } - } From 62c85eddd6e8667345159b0e753157521a955b53 Mon Sep 17 00:00:00 2001 From: zak905 Date: Sat, 12 Mar 2022 21:40:55 +0100 Subject: [PATCH 4/5] handle slices and cases when invalid default values are provided --- converter.go | 84 ++++++++++++++++++++++++++++++++----------------- decoder.go | 34 ++++++++++++++++---- decoder_test.go | 60 ++++++++++++++++++++++++++++------- 3 files changed, 133 insertions(+), 45 deletions(-) diff --git a/converter.go b/converter.go index 9adf7fe..4bae6df 100644 --- a/converter.go +++ b/converter.go @@ -147,47 +147,75 @@ func convertUint64(value string) reflect.Value { func convertPointer(k reflect.Kind, value string) reflect.Value { switch k { case boolType: - v := convertBool(value).Bool() - return reflect.ValueOf(&v) + if v := convertBool(value); v.IsValid() { + converted := v.Bool() + return reflect.ValueOf(&converted) + } case float32Type: - v := float32(convertFloat32(value).Float()) - return reflect.ValueOf(&v) + if v := convertFloat32(value); v.IsValid() { + converted := float32(v.Float()) + return reflect.ValueOf(&converted) + } case float64Type: - v := float64(convertFloat64(value).Float()) - return reflect.ValueOf(&v) + if v := convertFloat64(value); v.IsValid() { + converted := float64(v.Float()) + return reflect.ValueOf(&converted) + } case intType: - v := int(convertInt(value).Int()) - return reflect.ValueOf(&v) + if v := convertInt(value); v.IsValid() { + converted := int(v.Int()) + return reflect.ValueOf(&converted) + } case int8Type: - v := int8(convertInt8(value).Int()) - return reflect.ValueOf(&v) + if v := convertInt8(value); v.IsValid() { + converted := int8(v.Int()) + return reflect.ValueOf(&converted) + } case int16Type: - v := int16(convertInt16(value).Int()) - return reflect.ValueOf(&v) + if v := convertInt16(value); v.IsValid() { + converted := int16(v.Int()) + return reflect.ValueOf(&converted) + } case int32Type: - v := int32(convertInt32(value).Int()) - return reflect.ValueOf(&v) + if v := convertInt32(value); v.IsValid() { + converted := int32(v.Int()) + return reflect.ValueOf(&converted) + } case int64Type: - v := int64(convertInt64(value).Int()) - return reflect.ValueOf(&v) + if v := convertInt64(value); v.IsValid() { + converted := int64(v.Int()) + return reflect.ValueOf(&converted) + } case stringType: - v := convertString(value).String() - return reflect.ValueOf(&v) + if v := convertString(value); v.IsValid() { + converted := v.String() + return reflect.ValueOf(&converted) + } case uintType: - v := uint(convertUint(value).Uint()) - return reflect.ValueOf(&v) + if v := convertUint(value); v.IsValid() { + converted := uint(v.Uint()) + return reflect.ValueOf(&converted) + } case uint8Type: - v := uint8(convertUint8(value).Uint()) - return reflect.ValueOf(&v) + if v := convertUint8(value); v.IsValid() { + converted := uint8(v.Uint()) + return reflect.ValueOf(&converted) + } case uint16Type: - v := uint16(convertUint16(value).Uint()) - return reflect.ValueOf(&v) + if v := convertUint16(value); v.IsValid() { + converted := uint16(v.Uint()) + return reflect.ValueOf(&converted) + } case uint32Type: - v := uint32(convertUint32(value).Uint()) - return reflect.ValueOf(&v) + if v := convertUint32(value); v.IsValid() { + converted := uint32(v.Uint()) + return reflect.ValueOf(&converted) + } case uint64Type: - v := uint64(convertUint64(value).Uint()) - return reflect.ValueOf(&v) + if v := convertUint64(value); v.IsValid() { + converted := uint64(v.Uint()) + return reflect.ValueOf(&converted) + } } return invalidValue diff --git a/decoder.go b/decoder.go index 832a9cf..98f072e 100644 --- a/decoder.go +++ b/decoder.go @@ -116,20 +116,42 @@ func (d *Decoder) setDefaults(t reflect.Type, v reflect.Value) MultiError { if f.defaultValue != "" && f.isRequired { errs.merge(MultiError{"default-" + f.name: errors.New("required fields cannot have a default value")}) } else if f.defaultValue != "" && vCurrent.IsZero() && !f.isRequired { - if f.typ.Kind() == reflect.Struct || f.typ.Kind() == reflect.Slice { - errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers")}) + if f.typ.Kind() == reflect.Struct { + errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices")}) + } else if f.typ.Kind() == reflect.Slice { + vals := strings.Split(f.defaultValue, "|") + + //check if slice has one of the supported types for defaults + if _, ok := builtinConverters[f.typ.Elem().Kind()]; !ok { + errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices")}) + continue + } + + defaultSlice := reflect.MakeSlice(f.typ, 0, cap(vals)) + for _, val := range vals { + //this check is to handle if the wrong value is provided + if convertedVal := builtinConverters[f.typ.Elem().Kind()](val); convertedVal.IsValid() { + defaultSlice = reflect.Append(defaultSlice, convertedVal) + } + } + vCurrent.Set(defaultSlice) } else if f.typ.Kind() == reflect.Ptr { t1 := f.typ.Elem() if t1.Kind() == reflect.Struct || t1.Kind() == reflect.Slice { - errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers")}) + errs.merge(MultiError{"default-" + f.name: errors.New("default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices")}) } - vCurrent.Set(convertPointer(t1.Kind(), f.defaultValue)) + //this check is to handle if the wrong value is provided + if convertedVal := convertPointer(t1.Kind(), f.defaultValue); convertedVal.IsValid() { + vCurrent.Set(convertedVal) + } } else { - vCurrent.Set(builtinConverters[f.typ.Kind()](f.defaultValue)) + //this check is to handle if the wrong value is provided + if convertedVal := builtinConverters[f.typ.Kind()](f.defaultValue); convertedVal.IsValid() { + vCurrent.Set(builtinConverters[f.typ.Kind()](f.defaultValue)) + } } - } } diff --git a/decoder_test.go b/decoder_test.go index c1cc449..f1eb33b 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -2059,17 +2059,19 @@ func TestUnmashalPointerToEmbedded(t *testing.T) { func TestDefaultValuesAreSet(t *testing.T) { type N struct { - S1 string `schema:"s1,default:test1"` - I2 int `schema:"i2,default:22"` + S1 string `schema:"s1,default:test1"` + I2 int `schema:"i2,default:22"` + R2 []float64 `schema:"r2,default:2|3.5|11.01"` } type D struct { N - S string `schema:"s,default:test1"` - I int `schema:"i,default:21"` - B bool `schema:"b,default:false"` - F float64 `schema:"f,default:3.14"` - U uint `schema:"u,default:1"` + S string `schema:"s,default:test1"` + I int `schema:"i,default:21"` + B bool `schema:"b,default:false"` + F float64 `schema:"f,default:3.14"` + U uint `schema:"u,default:1"` + X []string `schema:"x,default:x1|x2"` } data := map[string][]string{} @@ -2086,12 +2088,14 @@ func TestDefaultValuesAreSet(t *testing.T) { N: N{ S1: "test1", I2: 22, + R2: []float64{2, 3.5, 11.01}, }, S: "test1", I: 21, B: false, F: 3.14, U: 1, + X: []string{"x1", "x2"}, } if !reflect.DeepEqual(expected, d) { @@ -2105,6 +2109,7 @@ func TestDefaultValuesAreSet(t *testing.T) { B *bool `schema:"b,default:false"` F *float64 `schema:"f,default:3.14"` U *uint `schema:"u,default:1"` + X []string `schema:"x,default:x1|x2"` } p := P{N: &N{}} @@ -2184,11 +2189,44 @@ func TestRequiredFieldsCannotHaveDefaults(t *testing.T) { } -func TestDefaultsAreNotSupportedForStructsAndSlices(t *testing.T) { +func TestInvalidDefaultsValuesHaveNoEffect(t *testing.T) { + + type D struct { + A []int `schema:"a,default:wrong1|wrong2"` + B bool `schema:"b,default:invalid"` + C *float32 `schema:"c,default:notAFloat"` + D uint8 `schema:"d,default:8000000"` + } + + d := D{} + + expected := D{A: []int{}} + + data := map[string][]string{} + + decoder := NewDecoder() + + err := decoder.Decode(&d, data) + + if err != nil { + t.Errorf("decoding should succeed but got error: %q", err) + } + + if !reflect.DeepEqual(expected, d) { + t.Errorf("expected %v but got %v", expected, d) + } +} + +func TestDefaultsAreNotSupportedForStructsAndStructSlices(t *testing.T) { + + type C struct { + C string `schema:"c"` + } type D struct { - S S1 `schema:"s,default:{f1:0}"` - A []string `schema:"s,default:test1,test2"` + S S1 `schema:"s,default:{f1:0}"` + A []C `schema:"a,default:{c:test1}|{c:test2}"` + B []*int `schema:"b,default:12"` } d := D{} @@ -2199,7 +2237,7 @@ func TestDefaultsAreNotSupportedForStructsAndSlices(t *testing.T) { err := decoder.Decode(&d, data) - expected := "default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers" + expected := "default option is supported only on: bool, float variants, string, unit variants types or their corresponding pointers or slices" if err == nil || !strings.Contains(err.Error(), expected) { t.Errorf("decoding should fail with error msg %s got %q", expected, err) From 7807359fda1d9f0107f269a01003545096c403bd Mon Sep 17 00:00:00 2001 From: zak905 Date: Fri, 2 Feb 2024 21:39:29 +0100 Subject: [PATCH 5/5] improve unit testing to cover more cases --- cache.go | 5 +--- decoder_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/cache.go b/cache.go index 2c02890..065b8d6 100644 --- a/cache.go +++ b/cache.go @@ -309,10 +309,7 @@ func (o tagOptions) Contains(option string) bool { func (o tagOptions) getDefaultOptionValue() string { for _, s := range o { if strings.HasPrefix(s, "default:") { - if t := strings.Split(s, ":"); len(t) > 0 { - return t[1] - } - break + return strings.Split(s, ":")[1] } } diff --git a/decoder_test.go b/decoder_test.go index f1eb33b..3c12218 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -2057,7 +2057,6 @@ func TestUnmashalPointerToEmbedded(t *testing.T) { } func TestDefaultValuesAreSet(t *testing.T) { - type N struct { S1 string `schema:"s1,default:test1"` I2 int `schema:"i2,default:22"` @@ -2068,9 +2067,18 @@ func TestDefaultValuesAreSet(t *testing.T) { N S string `schema:"s,default:test1"` I int `schema:"i,default:21"` + J int8 `schema:"j,default:2"` + K int16 `schema:"k,default:-455"` + L int32 `schema:"l,default:899"` + M int64 `schema:"m,default:12455"` B bool `schema:"b,default:false"` F float64 `schema:"f,default:3.14"` + G float32 `schema:"g,default:19.12"` U uint `schema:"u,default:1"` + V uint8 `schema:"v,default:190"` + W uint16 `schema:"w,default:20000"` + Y uint32 `schema:"y,default:156666666"` + Z uint64 `schema:"z,default:1545465465465546"` X []string `schema:"x,default:x1|x2"` } @@ -2092,9 +2100,18 @@ func TestDefaultValuesAreSet(t *testing.T) { }, S: "test1", I: 21, + J: 2, + K: -455, + L: 899, + M: 12455, B: false, F: 3.14, + G: 19.12, U: 1, + V: 190, + W: 20000, + Y: 156666666, + Z: 1545465465465546, X: []string{"x1", "x2"}, } @@ -2106,9 +2123,18 @@ func TestDefaultValuesAreSet(t *testing.T) { *N S *string `schema:"s,default:test1"` I *int `schema:"i,default:21"` + J *int8 `schema:"j,default:2"` + K *int16 `schema:"k,default:-455"` + L *int32 `schema:"l,default:899"` + M *int64 `schema:"m,default:12455"` B *bool `schema:"b,default:false"` F *float64 `schema:"f,default:3.14"` + G *float32 `schema:"g,default:19.12"` U *uint `schema:"u,default:1"` + V *uint8 `schema:"v,default:190"` + W *uint16 `schema:"w,default:20000"` + Y *uint32 `schema:"y,default:156666666"` + Z *uint64 `schema:"z,default:1545465465465546"` X []string `schema:"x,default:x1|x2"` } @@ -2164,7 +2190,6 @@ func TestDefaultValuesAreIgnoredIfValuesAreProvided(t *testing.T) { } func TestRequiredFieldsCannotHaveDefaults(t *testing.T) { - type D struct { S string `schema:"s,required,default:test1"` I int `schema:"i,required,default:21"` @@ -2190,12 +2215,38 @@ func TestRequiredFieldsCannotHaveDefaults(t *testing.T) { } func TestInvalidDefaultsValuesHaveNoEffect(t *testing.T) { - type D struct { A []int `schema:"a,default:wrong1|wrong2"` B bool `schema:"b,default:invalid"` C *float32 `schema:"c,default:notAFloat"` - D uint8 `schema:"d,default:8000000"` + //uint types + D uint `schema:"d,default:notUint"` + E uint8 `schema:"e,default:notUint"` + F uint16 `schema:"f,default:notUint"` + G uint32 `schema:"g,default:notUint"` + H uint64 `schema:"h,default:notUint"` + // uint types pointers + I *uint `schema:"i,default:notUint"` + J *uint8 `schema:"j,default:notUint"` + K *uint16 `schema:"k,default:notUint"` + L *uint32 `schema:"l,default:notUint"` + M *uint64 `schema:"m,default:notUint"` + // int types + N int `schema:"n,default:notInt"` + O int8 `schema:"o,default:notInt"` + P int16 `schema:"p,default:notInt"` + Q int32 `schema:"q,default:notInt"` + R int64 `schema:"r,default:notInt"` + // int types pointers + S *int `schema:"s,default:notInt"` + T *int8 `schema:"t,default:notInt"` + U *int16 `schema:"u,default:notInt"` + V *int32 `schema:"v,default:notInt"` + W *int64 `schema:"w,default:notInt"` + // float + X float32 `schema:"c,default:notAFloat"` + Y float64 `schema:"c,default:notAFloat"` + Z *float64 `schema:"c,default:notAFloat"` } d := D{} @@ -2218,7 +2269,6 @@ func TestInvalidDefaultsValuesHaveNoEffect(t *testing.T) { } func TestDefaultsAreNotSupportedForStructsAndStructSlices(t *testing.T) { - type C struct { C string `schema:"c"` } @@ -2227,6 +2277,7 @@ func TestDefaultsAreNotSupportedForStructsAndStructSlices(t *testing.T) { S S1 `schema:"s,default:{f1:0}"` A []C `schema:"a,default:{c:test1}|{c:test2}"` B []*int `schema:"b,default:12"` + E *C `schema:"e,default:{c:test3}"` } d := D{}