diff --git a/config_sources.go b/config_sources.go index 87e2522..963b644 100644 --- a/config_sources.go +++ b/config_sources.go @@ -1,7 +1,6 @@ package envconf import ( - "errors" "flag" "os" "reflect" @@ -22,14 +21,6 @@ const ( valDefault = "*" ) -var ( - //errors - errInvalidFiled = errors.New("invalid field") - errFiledIsNotSettable = errors.New("field is not settable") - ErrUnsupportedType = errors.New("unsupported type") - errConfigurationNotSpecified = errors.New("configuration not specified") -) - type flagSource struct { name string v string @@ -116,14 +107,6 @@ func newExternalValueSource(f field, ext *externalConfig) *externalValueSource { } } -func (s *externalValueSource) Name() string { - name, ok := s.f.structField().Tag.Lookup(tagEnv) - if !ok { - name = s.f.name() - } - return name -} - func (s *externalValueSource) Value() (interface{}, bool) { return s.ext.get(s.f) } @@ -139,10 +122,6 @@ func newDefaultValueSource(tag reflect.StructField) *defaultValueSource { return &s } -func (s *defaultValueSource) Name() string { - return tagDefault -} - func (s *defaultValueSource) Value() (interface{}, bool) { return s.v, s.defined } diff --git a/error.go b/error.go index 6b1bd91..dfa0387 100644 --- a/error.go +++ b/error.go @@ -1,6 +1,16 @@ package envconf -import "fmt" +import ( + "errors" + "fmt" +) + +var ( + // ErrNilData mean that exists nil pointer inside data struct + ErrNilData = errors.New("nil data") + ErrUnsupportedType = errors.New("unsupported type") + ErrConfigurationNotFound = errors.New("configuration not found") +) type Error struct { Inner error diff --git a/external/external_test.go b/external/external_test.go index 9e563e6..1239cb5 100644 --- a/external/external_test.go +++ b/external/external_test.go @@ -1,15 +1,67 @@ package external import ( + "errors" + "io" + "os" "testing" "github.com/antonmashko/envconf/option" ) func TestWithFlagConfigFile_Ok(t *testing.T) { - opt := WithFlagConfigFile("config", "", "", func(b []byte) error { + f, err := os.CreateTemp("", "envconf.tmp") + if err != nil { + t.Fatal("os.Create:", err) + } + defer f.Close() + defer os.Remove(f.Name()) + const content = `{"foo":"bar"}` + _, err = io.WriteString(f, content) + if err != nil { + t.Fatal("io.WriteString: ", err) + } + var result string + opt := WithFlagConfigFile("config1", f.Name(), "", func(b []byte) error { + result = string(b) return nil }) opts := &option.Options{} opt.Apply(opts) + if err = opts.FlagParsed()(); err != nil { + t.Fatal("opts.FlagParsed(): ", err) + } + if result != content { + t.Fatal("unexpected result: ", result) + } +} + +func TestWithFlagConfigFile_NotExist_Err(t *testing.T) { + opt := WithFlagConfigFile("config2", "./conf.json", "", func(b []byte) error { + return nil + }) + opts := &option.Options{} + opt.Apply(opts) + if err := opts.FlagParsed()(); err == nil { + t.Fatal("expected error but got nil") + } +} + +func TestWithFlagConfigFile_CustomError_Err(t *testing.T) { + f, err := os.CreateTemp("", "envconf.tmp") + if err != nil { + t.Fatal("os.Create:", err) + } + defer f.Close() + defer os.Remove(f.Name()) + + cErr := errors.New("custom error") + opt := WithFlagConfigFile("config3", f.Name(), "", func(b []byte) error { + return cErr + }) + opts := &option.Options{} + opt.Apply(opts) + if err = opts.FlagParsed()(); err == nil || err != cErr { + t.Fatal("opts.FlagParsed() unexpected error: ", err) + } } diff --git a/field.go b/field.go index 675b400..6527934 100644 --- a/field.go +++ b/field.go @@ -1,9 +1,8 @@ package envconf import ( - "net/url" + "encoding" "reflect" - "time" ) const fieldNameDelim = "." @@ -48,14 +47,18 @@ func (emptyField) structField() reflect.StructField { } func createFieldFromValue(v reflect.Value, p *structType, t reflect.StructField) field { + // validate reflect value + if !v.CanInterface() { + return emptyField{} + } switch v.Kind() { case reflect.Struct: - switch v.Interface().(type) { - case url.URL, time.Time: - return newPrimitiveType(v, p, t) - default: - return newStructType(v, p, t) + // implementations check + implF := asImpl(v) + if implF != nil { + return newFieldType(v, p, t) } + return newStructType(v, p, t) case reflect.Ptr: return newPtrType(v, p, t) case reflect.Interface: @@ -64,7 +67,7 @@ func createFieldFromValue(v reflect.Value, p *structType, t reflect.StructField) // unsupported types return emptyField{} default: - return newPrimitiveType(v, p, t) + return newFieldType(v, p, t) } } @@ -82,3 +85,35 @@ func fullname(f field) string { } return name } + +func asImpl(field reflect.Value) func([]byte) error { + f := func(v interface{}) func([]byte) error { + // encoding.TextUnmarshaler + tu, ok := v.(encoding.TextUnmarshaler) + if ok { + return tu.UnmarshalText + } + // encoding.BinaryUnmarshaller + bu, ok := v.(encoding.BinaryUnmarshaler) + if ok { + return bu.UnmarshalBinary + } + // ---- + return nil + } + // NOTE: max double pointer support + for i := 0; i < 3; i++ { + resF := f(field.Interface()) + if resF != nil { + return resF + } + if !field.CanAddr() { + return nil + } + field = field.Addr() + if !field.CanInterface() { + return nil + } + } + return nil +} diff --git a/field_test.go b/field_test.go new file mode 100644 index 0000000..a25a580 --- /dev/null +++ b/field_test.go @@ -0,0 +1,27 @@ +package envconf + +import ( + "testing" +) + +func TestEmptyField_Ok(t *testing.T) { + et := emptyField{} + if err := et.init(); err != nil { + t.Fatal("emptyField.init: ", err) + } + if err := et.define(); err != nil { + t.Fatal("emptyField.define: ", err) + } + if et.isSet() { + t.Fatal("emptyField.isSet: true") + } + if et.name() != "" { + t.Fatal("emptyField.name: ", et.name()) + } + if et.parent() != nil { + t.Fatal("emptyField.parent: ", et.parent()) + } + if et.structField().Tag != "" { + t.Fatal("emptyField.structField: ", et.structField().Tag) + } +} diff --git a/parser.go b/parser.go index 0e8de0f..1997b27 100644 --- a/parser.go +++ b/parser.go @@ -1,15 +1,11 @@ package envconf import ( - "errors" "flag" "github.com/antonmashko/envconf/option" ) -// ErrNilData mean that exists nil pointer inside data struct -var ErrNilData = errors.New("nil data") - type EnvConf struct { external *externalConfig opts *option.Options @@ -27,7 +23,7 @@ func NewWithExternal(e External) *EnvConf { } func (e *EnvConf) fieldInitialized(f field) { - pt, ok := f.(*primitiveType) + pt, ok := f.(*fieldType) if !ok { return } @@ -45,7 +41,7 @@ func (e *EnvConf) fieldInitialized(f field) { } func (e *EnvConf) fieldDefined(f field) { - pt, ok := f.(*primitiveType) + pt, ok := f.(*fieldType) if !ok { return } @@ -68,7 +64,7 @@ func (e *EnvConf) fieldDefined(f field) { } func (e *EnvConf) fieldNotDefined(f field, err error) { - pt, ok := f.(*primitiveType) + pt, ok := f.(*fieldType) if !ok { return } diff --git a/parser_primitives_test.go b/parser_field_test.go similarity index 98% rename from parser_primitives_test.go rename to parser_field_test.go index 367026a..3223d58 100644 --- a/parser_primitives_test.go +++ b/parser_field_test.go @@ -373,3 +373,13 @@ func TestParse_ValueWithSpace_Ok(t *testing.T) { t.Errorf("incorrect result:%#v", cfg) } } + +func TestParse_PrivateField_Ok(t *testing.T) { + cfg := struct { + field2 string `default:"f2"` + }{} + + if err := envconf.Parse(&cfg); err != nil { + t.Fatal(err) + } +} diff --git a/parser_implementation_test.go b/parser_implementation_test.go new file mode 100644 index 0000000..0f089f3 --- /dev/null +++ b/parser_implementation_test.go @@ -0,0 +1,103 @@ +package envconf_test + +import ( + "testing" + + "github.com/antonmashko/envconf" +) + +type stringTextUnmarshaller string + +func (tu *stringTextUnmarshaller) UnmarshalText(text []byte) error { + *tu = stringTextUnmarshaller("txt") + return nil +} + +func TestParse_StringTextUnmarshaller_Ok(t *testing.T) { + tc := struct { + Field1 stringTextUnmarshaller `default:"10"` + }{} + err := envconf.Parse(&tc) + if err != nil { + t.Fatal(err) + } + if tc.Field1 != "txt" { + t.Fatal("unexpected result: ", tc.Field1) + } +} + +func TestParse_PointerStringTextUnmarshaller_Ok(t *testing.T) { + tc := struct { + Field1 *stringTextUnmarshaller `default:"10"` + }{} + err := envconf.Parse(&tc) + if err != nil { + t.Fatal(err) + } + if *tc.Field1 != "txt" { + t.Fatal("unexpected result: ", *tc.Field1) + } +} + +func TestParse_DoublePointerStringTextUnmarshaller_Ok(t *testing.T) { + tc := struct { + Field1 **stringTextUnmarshaller `default:"10"` + }{} + err := envconf.Parse(&tc) + if err != nil { + t.Fatal(err) + } + if **tc.Field1 != "txt" { + t.Fatal("unexpected result: ", **tc.Field1) + } +} + +type structTextUnmarshaller struct { + data string + invoked bool +} + +func (tu *structTextUnmarshaller) UnmarshalText(text []byte) error { + tu.invoked = true + tu.data = "txt" + return nil +} + +func TestParse_StructTextUnmarshaller_Ok(t *testing.T) { + tc := struct { + Field1 structTextUnmarshaller `default:"10"` + }{} + err := envconf.Parse(&tc) + if err != nil { + t.Fatal(err) + } + if tc.Field1.data != "txt" || !tc.Field1.invoked { + t.Fatal("unexpected result: ", tc.Field1) + } +} + +func TestParse_PointerStructTextUnmarshaller_Ok(t *testing.T) { + tc := struct { + Field1 *structTextUnmarshaller `default:"10"` + }{} + err := envconf.Parse(&tc) + if err != nil { + t.Fatal(err) + } + if tc.Field1.data != "txt" || !tc.Field1.invoked { + t.Fatal("unexpected result: ", tc.Field1) + } +} + +func TestParse_DoublePointerStructTextUnmarshaller_Ok(t *testing.T) { + tc := struct { + Field1 **structTextUnmarshaller `default:"10"` + }{} + err := envconf.Parse(&tc) + if err != nil { + t.Fatal(err) + } + if (*tc.Field1).data != "txt" || !(*tc.Field1).invoked { + t.Fatal("unexpected result: ", *tc.Field1) + } +} diff --git a/readme.md b/readme.md index 504bfff..325bd4b 100644 --- a/readme.md +++ b/readme.md @@ -35,10 +35,8 @@ Use tags for getting values from different configuration sources. - Array and Slice - comma-separated string can be converted into slice or array. NOTE: if elements in string more than len of array EnvConf will panic with `index out of range`. - Map - comma-separated string with a colon-separated key and value can be converted into map. example input: `key1:value1, key2:value2` 3. Golang types: - - time.Duration; - - time.Time - using `time.RFC3339` as a time.Parse layout argument; - - net.IP; - - url.URL; + - time.Duration + - Types that are implementing [encoding.TextUnmarshaller](https://pkg.go.dev/encoding#TextUnmarshaler) and [encoding.BinaryUnmarshaller](https://pkg.go.dev/encoding#BinaryUnmarshaler). e.g. time.Time, net.ID, url.URL; ### Example Let's take a look at a simple example. Here we're creating struct with 3 tags for different configuration sources: flag, env, and default value. **NOTE**: It's not necessary to specify tags for each configuration type, add desired only diff --git a/type_primitive.go b/type_field.go similarity index 85% rename from type_primitive.go rename to type_field.go index bfb5e3c..383faff 100644 --- a/type_primitive.go +++ b/type_field.go @@ -1,9 +1,8 @@ package envconf import ( + "errors" "fmt" - "net" - "net/url" "reflect" "strconv" "strings" @@ -21,7 +20,7 @@ type definedValue struct { value interface{} } -type primitiveType struct { +type fieldType struct { v reflect.Value p *structType sf reflect.StructField @@ -36,10 +35,10 @@ type primitiveType struct { definedValue *definedValue } -func newPrimitiveType(v reflect.Value, p *structType, sf reflect.StructField) *primitiveType { +func newFieldType(v reflect.Value, p *structType, sf reflect.StructField) *fieldType { desc := sf.Tag.Get(tagDescription) required, _ := strconv.ParseBool(sf.Tag.Get(tagRequired)) - f := &primitiveType{ + f := &fieldType{ p: p, v: v, sf: sf, @@ -53,39 +52,31 @@ func newPrimitiveType(v reflect.Value, p *structType, sf reflect.StructField) *p return f } -func (t *primitiveType) name() string { +func (t *fieldType) name() string { return t.sf.Name } -func (t *primitiveType) parent() field { +func (t *fieldType) parent() field { return t.p } -func (t *primitiveType) isSet() bool { +func (t *fieldType) isSet() bool { return t.definedValue != nil } -func (t *primitiveType) structField() reflect.StructField { +func (t *fieldType) structField() reflect.StructField { return t.sf } -func (t *primitiveType) IsRequired() bool { +func (t *fieldType) IsRequired() bool { return t.required } -func (t *primitiveType) init() error { +func (t *fieldType) init() error { return nil } -func (t *primitiveType) define() error { - // validate reflect value - if !t.v.IsValid() { - return errInvalidFiled - } - if !t.v.CanSet() { - return fmt.Errorf("%s: %w", t.name(), errFiledIsNotSettable) - } - +func (t *fieldType) define() error { // create correct parse priority priority := t.p.parser.PriorityOrder() for _, p := range priority { @@ -125,21 +116,80 @@ func (t *primitiveType) define() error { return nil } - return errConfigurationNotSpecified + return ErrConfigurationNotFound +} + +func setFromInterface(field reflect.Value, value interface{}) error { + ival := reflect.ValueOf(value) + itype := ival.Type() + if field.Type() == itype { + field.Set(ival) + return nil + } + + switch field.Kind() { + case reflect.Array: + if ikind := itype.Kind(); ikind != reflect.Array && ikind != reflect.Slice { + return fmt.Errorf("unable to cast %s to array", itype) + } + length := ival.Len() + for i := 0; i < length; i++ { + setFromString(field.Index(i), fmt.Sprint(ival.Index(i).Interface())) + } + return nil + case reflect.Slice: + if ikind := itype.Kind(); ikind != reflect.Array && ikind != reflect.Slice { + return fmt.Errorf("unable to cast %s to array", itype) + } + length := ival.Len() + vtype := field.Type() + rsl := reflect.MakeSlice(vtype, ival.Cap(), length) + for i := 0; i < length; i++ { + if err := setFromString(rsl.Index(i), fmt.Sprint(ival.Index(i).Interface())); err != nil { + return err + } + } + field.Set(rsl) + return nil + case reflect.Map: + if itype.Kind() != reflect.Map { + return fmt.Errorf("unable to cast %s to array", itype) + } + ftype := field.Type() + rmp := reflect.MakeMap(ftype) + key := ftype.Key() + elem := ftype.Elem() + iter := ival.MapRange() + for iter.Next() { + rvkey := reflect.New(key).Elem() + if err := setFromString(rvkey, fmt.Sprint(iter.Key().Interface())); err != nil { + return err + } + rvval := reflect.New(elem).Elem() + if err := setFromString(rvval, fmt.Sprint(iter.Value().Interface())); err != nil { + return err + } + rmp.SetMapIndex(rvkey, rvval) + } + field.Set(rmp) + return nil + default: + return setFromString(field, fmt.Sprint(value)) + } } func setFromString(field reflect.Value, value string) error { + if implF := asImpl(field); implF != nil { + return implF([]byte(value)) + } oval := value value = strings.Trim(value, " ") + if !field.CanSet() { + return errors.New("reflect: cannot set") + } + // native complex types switch field.Interface().(type) { - case url.URL: - url, err := url.Parse(value) - if err != nil { - return err - } - field.Set(reflect.ValueOf(*url)) - return nil case time.Duration: d, err := time.ParseDuration(value) if err != nil { @@ -147,16 +197,6 @@ func setFromString(field reflect.Value, value string) error { } field.SetInt(d.Nanoseconds()) return nil - case net.IP: - field.Set(reflect.ValueOf(net.ParseIP(value))) - return nil - case time.Time: - dt, err := time.Parse(time.RFC3339, value) - if err != nil { - return err - } - field.Set(reflect.ValueOf(dt)) - return nil } // primitives and collections @@ -241,62 +281,3 @@ func setFromString(field reflect.Value, value string) error { } return nil } - -func setFromInterface(field reflect.Value, value interface{}) error { - ival := reflect.ValueOf(value) - itype := ival.Type() - if field.Type() == itype { - field.Set(ival) - return nil - } - - switch field.Kind() { - case reflect.Array: - if ikind := itype.Kind(); ikind != reflect.Array && ikind != reflect.Slice { - return fmt.Errorf("unable to cast %s to array", itype) - } - length := ival.Len() - for i := 0; i < length; i++ { - setFromString(field.Index(i), fmt.Sprint(ival.Index(i).Interface())) - } - return nil - case reflect.Slice: - if ikind := itype.Kind(); ikind != reflect.Array && ikind != reflect.Slice { - return fmt.Errorf("unable to cast %s to array", itype) - } - length := ival.Len() - vtype := field.Type() - rsl := reflect.MakeSlice(vtype, ival.Cap(), length) - for i := 0; i < length; i++ { - if err := setFromString(rsl.Index(i), fmt.Sprint(ival.Index(i).Interface())); err != nil { - return err - } - } - field.Set(rsl) - return nil - case reflect.Map: - if itype.Kind() != reflect.Map { - return fmt.Errorf("unable to cast %s to array", itype) - } - ftype := field.Type() - rmp := reflect.MakeMap(ftype) - key := ftype.Key() - elem := ftype.Elem() - iter := ival.MapRange() - for iter.Next() { - rvkey := reflect.New(key).Elem() - if err := setFromString(rvkey, fmt.Sprint(iter.Key().Interface())); err != nil { - return err - } - rvval := reflect.New(elem).Elem() - if err := setFromString(rvval, fmt.Sprint(iter.Value().Interface())); err != nil { - return err - } - rmp.SetMapIndex(rvkey, rvval) - } - field.Set(rmp) - return nil - default: - return setFromString(field, fmt.Sprint(value)) - } -} diff --git a/type_field_test.go b/type_field_test.go new file mode 100644 index 0000000..81d4467 --- /dev/null +++ b/type_field_test.go @@ -0,0 +1,27 @@ +package envconf + +import ( + "reflect" + "testing" +) + +func Test_setFromString(t *testing.T) { + type args struct { + field reflect.Value + value string + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := setFromString(tt.args.field, tt.args.value); (err != nil) != tt.wantErr { + t.Errorf("setFromString() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/type_struct.go b/type_struct.go index 289d59a..0f3db57 100644 --- a/type_struct.go +++ b/type_struct.go @@ -99,7 +99,7 @@ func (s *structType) define() error { } s.parser.fieldNotDefined(f, err) - if err == errConfigurationNotSpecified { + if err == ErrConfigurationNotFound { continue } return err