diff --git a/env.go b/env.go index 6560d9d..27c7e6f 100644 --- a/env.go +++ b/env.go @@ -30,6 +30,11 @@ type EnvProvider struct { Required bool } +var ( + _ Provider = (*EnvProvider)(nil) + _ Filler = (*EnvProvider)(nil) +) + // NewEnvProvider creates a new EnvProvider func NewEnvProvider() *EnvProvider { return &EnvProvider{ @@ -42,18 +47,6 @@ func NewEnvProvider() *EnvProvider { } } -// NewEnvFileProvider creates a new EnvProvider from .env file -func NewEnvFileProvider(path string) *EnvProvider { - return &EnvProvider{ - EnvPrefix: "", - SnakeCase: true, - UpperCase: true, - FieldSeparator: "_", - Source: path, - Required: false, - } -} - // Name of provider func (ep *EnvProvider) Name() string { return "ENV provider" @@ -76,7 +69,7 @@ func (ep *EnvProvider) Fill(in *Input) error { return err } - err = in.setValue(f, value) + err = in.SetValue(f, value) if err != nil { return err } @@ -87,8 +80,7 @@ func (ep *EnvProvider) Fill(in *Input) error { return nil } -// envMap returns environment variables map from either OS or file specified by source -// Defaults to operating system env variables +// envMap joins env vars from OS and optional env file and returns corresponding map func (ep *EnvProvider) envMap() (map[string]string, error) { envs := envFromOS() var fileEnvs map[string]string diff --git a/file.go b/file.go index 08811dc..faddd5a 100644 --- a/file.go +++ b/file.go @@ -32,6 +32,12 @@ type FileProvider struct { Required bool } +var ( + _ Provider = (*FileProvider)(nil) + _ Unmarshaler = (*FileProvider)(nil) + _ Filler = (*FileProvider)(nil) +) + // NewFileProvider creates a new FileProvider from specified path func NewFileProvider(path string) *FileProvider { return &FileProvider{ @@ -43,7 +49,7 @@ func NewFileProvider(path string) *FileProvider { // Name of provider func (fp *FileProvider) Name() string { - return "File provider" + return fmt.Sprintf("File provider (%v)", fp.FileExt[1:]) } // UnmarshalStruct takes a struct pointer and loads values from provided file into it @@ -73,8 +79,7 @@ func (fp *FileProvider) Fill(in *Input) error { key = f.Tags.Toml } - _, err := fp.provide(content, key, f.Path) - if err == nil { + if _, err := fp.provide(content, key, f.Path); err == nil { f.IsSet = true } } @@ -90,11 +95,11 @@ func (fp *FileProvider) decode(i interface{}) (err error) { return nil } - return fmt.Errorf("file provider: %w", err) + return err } defer func() { - if cerr := f.Close(); cerr != nil && err == nil { - err = cerr + if cErr := f.Close(); cErr != nil && err == nil { + err = cErr } }() @@ -121,7 +126,13 @@ func (fp *FileProvider) decode(i interface{}) (err error) { // provide find a value from file content based on specified key and path func (fp *FileProvider) provide(content map[string]interface{}, key string, path []string) (string, error) { - return traverseMap(content, fp.buildPath(key, path)) + builtPath := fp.buildPath(key, path) + value, exists := traverseMap(content, builtPath) + if !exists { + return "", ErrKeyNotFound + } + + return value, nil } // buildPath makes a path from key and path slice diff --git a/gonfig.go b/gonfig.go index c386eae..061ec8e 100644 --- a/gonfig.go +++ b/gonfig.go @@ -16,23 +16,6 @@ type Config struct { ce ConfigErrors } -// Provider is used to provide values -// It can implement either Unmarshaler or Filler interface or both -// Name method is used for error messages -type Provider interface { - Name() string -} - -// Unmarshaler can be implemented by providers to receive struct pointer and unmarshal values into it -type Unmarshaler interface { - UnmarshalStruct(i interface{}) (err error) -} - -// Filler can be implemented by providers to receive struct fields and set their value -type Filler interface { - Fill(in *Input) (err error) -} - // Load creates a new Config object func Load() *Config { return &Config{} @@ -40,33 +23,24 @@ func Load() *Config { // FromEnv adds an EnvProvider to Providers list func (c *Config) FromEnv() *Config { - return c.FromEnvWithConfig(NewEnvProvider()) -} - -// FromEnvWithConfig adds an EnvProvider to Providers list with specified config -func (c *Config) FromEnvWithConfig(ep *EnvProvider) *Config { - c.Providers = append(c.Providers, ep) - return c + return c.AddProvider(NewEnvProvider()) } // FromFile adds a FileProvider to Providers list // In case of .env file, it adds a EnvProvider to the list func (c *Config) FromFile(path string) *Config { if filepath.Ext(path) == ENV { - return c.FromEnvWithConfig(NewEnvFileProvider(path)) + ep := NewEnvProvider() + ep.Source = path + return c.AddProvider(NewEnvProvider()) } - return c.FromFileWithConfig(NewFileProvider(path)) + return c.AddProvider(NewFileProvider(path)) } -// FromRequiredFile adds a FileProvider to Providers list with specified config -// In case of .env file, it adds a EnvProvider to the list -func (c *Config) FromFileWithConfig(fp *FileProvider) *Config { - if fp.FileExt == ENV || filepath.Ext(fp.FilePath) == ENV { - return c.FromEnvWithConfig(NewEnvFileProvider(fp.FilePath)) - } - - c.Providers = append(c.Providers, fp) +// AddProvider adds a Provider to Providers list +func (c *Config) AddProvider(p Provider) *Config { + c.Providers = append(c.Providers, p) return c } @@ -82,13 +56,13 @@ func (c *Config) Into(i interface{}) error { for _, p := range c.Providers { if u, ok := p.(Unmarshaler); ok { if err := u.UnmarshalStruct(i); err != nil { - c.collectError(err) + c.collectError(fmt.Errorf("%v: %w", p.Name(), err)) } } if f, ok := p.(Filler); ok { if err := f.Fill(in); err != nil { - c.collectError(err) + c.collectError(fmt.Errorf("%v: %w", p.Name(), err)) } } } @@ -98,7 +72,7 @@ func (c *Config) Into(i interface{}) error { if f.Tags.Required { c.collectError(fmt.Errorf(requiredFieldErrFormat, ErrRequiredField, in.getPath(f.Path))) } else if f.Tags.Default != "" { - err := in.setValue(f, f.Tags.Default) + err := in.SetValue(f, f.Tags.Default) if err != nil { c.collectError(err) } @@ -106,7 +80,7 @@ func (c *Config) Into(i interface{}) error { } if f.Tags.Expand && f.Value.Kind() == reflect.String { - err := in.setValue(f, f.Value.String()) + err := in.SetValue(f, f.Value.String()) if err != nil { c.collectError(err) } diff --git a/input.go b/input.go index 87bb888..19a8e6a 100644 --- a/input.go +++ b/input.go @@ -91,7 +91,7 @@ func (in *Input) traverseFiled(f *Field) error { for i := 0; i < f.Value.NumField(); i++ { nestedField := Field{ Value: f.Value.Field(i), - Tags: getTags(f.Value.Type().Field(i).Tag), + Tags: extractTags(f.Value.Type().Field(i).Tag), Path: append(f.Path, f.Value.Type().Field(i).Name), } @@ -151,232 +151,301 @@ func (in *Input) collectField(f *Field) { in.Fields = append(in.Fields, f) } -// setValue validates and sets the value of a struct field -func (in *Input) setValue(f *Field, value string) error { +// SetValue validates and sets the value of a struct field +func (in *Input) SetValue(f *Field, value string) error { if f.Tags.Expand { value = os.ExpandEnv(value) } switch f.Value.Kind() { case reflect.String: - f.Value.SetString(value) + return in.setString(f, value) case reflect.Bool: - b, err := strconv.ParseBool(value) - if err != nil { - return fmt.Errorf( - parseErrFormat, - ErrParsing, in.getPath(f.Path), err, - ) - } - - f.Value.SetBool(b) + return in.setBool(f, value) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - var d time.Duration - var i int64 - var err error - if isDuration(f.Value) { - d, err = time.ParseDuration(value) - if err != nil { - return fmt.Errorf( - parseErrFormat, - ErrParsing, in.getPath(f.Path), err, - ) - } - - i = int64(d) - } else { - i, err = strconv.ParseInt(value, 0, 64) - if err != nil { - return fmt.Errorf( - parseErrFormat, - ErrParsing, in.getPath(f.Path), err, - ) - } - } - - if f.Value.OverflowInt(i) { - return fmt.Errorf( - overflowErrFormat, - ErrValueOverflow, i, f.Value.Kind(), in.getPath(f.Path), - ) + return in.setDuration(f, value) } - f.Value.SetInt(i) + return in.setInt(f, value) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - i, err := strconv.ParseUint(value, 0, 64) - if err != nil { - return fmt.Errorf( - parseErrFormat, - ErrParsing, in.getPath(f.Path), err, - ) - } + return in.setUint(f, value) - if f.Value.OverflowUint(i) { - return fmt.Errorf( - overflowErrFormat, - ErrValueOverflow, i, f.Value.Kind(), in.getPath(f.Path), - ) - } + case reflect.Float32, reflect.Float64: + return in.setFloat(f, value) - f.Value.SetUint(i) + case reflect.Complex64, reflect.Complex128: + return in.setComplex(f, value) - case reflect.Float32, reflect.Float64: - fv, err := strconv.ParseFloat(value, 64) - if err != nil { - return fmt.Errorf( - parseErrFormat, - ErrParsing, in.getPath(f.Path), err, - ) - } + case reflect.Slice, reflect.Array: + return in.setList(f, value) - if f.Value.OverflowFloat(fv) { - return fmt.Errorf( - overflowErrFormat, - ErrValueOverflow, fv, f.Value.Kind(), in.getPath(f.Path), - ) - } + case reflect.Map: + return in.setMap(f, value) - f.Value.SetFloat(fv) + case reflect.Ptr: + return in.setPointer(f, value) - case reflect.Complex64, reflect.Complex128: - cx, err := strconv.ParseComplex(value, 64) - if err != nil { - return fmt.Errorf( - parseErrFormat, - ErrParsing, in.getPath(f.Path), err, - ) + case reflect.Struct: + if isTime(f.Value) { + return in.setTime(f, value) } - if f.Value.OverflowComplex(cx) { - return fmt.Errorf( - overflowErrFormat, - ErrValueOverflow, cx, f.Value.Kind(), in.getPath(f.Path), - ) + if isURL(f.Value) { + return in.setUrl(f, value) } - f.Value.SetComplex(cx) + default: + return fmt.Errorf( + unsupportedTypeErrFormat, + ErrUnsupportedType, f.Value.Kind(), in.getPath(f.Path), + ) + } - case reflect.Slice, reflect.Array: - switch f.Value.Type().Elem().Kind() { - case reflect.Slice, - reflect.Array, - reflect.Uintptr, - reflect.Chan, - reflect.Func, - reflect.Interface, - reflect.UnsafePointer: - return fmt.Errorf( - unsupportedElementTypeErrFormat, - ErrUnsupportedType, f.Value.Type().Elem().Kind(), in.getPath(f.Path), - ) - } + return nil +} - var items []string - for _, v := range strings.Split(value, f.Tags.Separator) { - item := strings.TrimSpace(v) - if len(item) > 0 { - items = append(items, item) - } - } - if len(items) == 0 { - return nil - } +func (in *Input) setString(f *Field, value string) error { + f.Value.SetString(value) + return nil +} - switch f.Value.Kind() { - case reflect.Slice: - size := len(items) - sv := reflect.MakeSlice(reflect.SliceOf(f.Value.Type().Elem()), size, size) - - for i := range items { - nestedField := Field{ - Value: sv.Index(i), - Tags: f.Tags, - Path: f.Path, - } - - if err := in.setValue(&nestedField, items[i]); err != nil { - return err - } - } +func (in *Input) setBool(f *Field, value string) error { + b, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf( + parseErrFormat, + ErrParsing, in.getPath(f.Path), err, + ) + } - f.Value.Set(sv) + f.Value.SetBool(b) + return nil +} - case reflect.Array: - size := f.Value.Len() - if size == 0 { - return nil - } +func (in *Input) setInt(f *Field, value string) error { + i, err := strconv.ParseInt(value, 0, 64) + if err != nil { + return fmt.Errorf( + parseErrFormat, + ErrParsing, in.getPath(f.Path), err, + ) + } + if f.Value.OverflowInt(i) { + return fmt.Errorf( + overflowErrFormat, + ErrValueOverflow, i, f.Value.Kind(), in.getPath(f.Path), + ) + } - at := reflect.ArrayOf(size, f.Value.Type().Elem()) - av := reflect.New(at).Elem() + f.Value.SetInt(i) + return nil +} - for i := 0; i < size; i++ { - nestedField := Field{ - Value: av.Index(i), - Tags: f.Tags, - Path: f.Path, - } +func (in *Input) setUint(f *Field, value string) error { + i, err := strconv.ParseUint(value, 0, 64) + if err != nil { + return fmt.Errorf( + parseErrFormat, + ErrParsing, in.getPath(f.Path), err, + ) + } + if f.Value.OverflowUint(i) { + return fmt.Errorf( + overflowErrFormat, + ErrValueOverflow, i, f.Value.Kind(), in.getPath(f.Path), + ) + } - if err := in.setValue(&nestedField, items[i]); err != nil { - return err - } - } + f.Value.SetUint(i) + return nil +} - f.Value.Set(av) +func (in *Input) setFloat(f *Field, value string) error { + fv, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf( + parseErrFormat, + ErrParsing, in.getPath(f.Path), err, + ) + } + if f.Value.OverflowFloat(fv) { + return fmt.Errorf( + overflowErrFormat, + ErrValueOverflow, fv, f.Value.Kind(), in.getPath(f.Path), + ) + } + + f.Value.SetFloat(fv) + return nil +} + +func (in *Input) setComplex(f *Field, value string) error { + c, err := strconv.ParseComplex(value, 64) + if err != nil { + return fmt.Errorf( + parseErrFormat, + ErrParsing, in.getPath(f.Path), err, + ) + } + if f.Value.OverflowComplex(c) { + return fmt.Errorf( + overflowErrFormat, + ErrValueOverflow, c, f.Value.Kind(), in.getPath(f.Path), + ) + } + + f.Value.SetComplex(c) + return nil +} + +func (in *Input) setList(f *Field, value string) error { + switch f.Value.Type().Elem().Kind() { + case reflect.Slice, + reflect.Array, + reflect.Uintptr, + reflect.Chan, + reflect.Func, + reflect.Interface, + reflect.UnsafePointer: + return fmt.Errorf( + unsupportedElementTypeErrFormat, + ErrUnsupportedType, f.Value.Type().Elem().Kind(), in.getPath(f.Path), + ) + } + + var items []string + for _, v := range strings.Split(value, f.Tags.Separator) { + item := strings.TrimSpace(v) + if len(item) > 0 { + items = append(items, item) } + } + if len(items) == 0 { + return nil + } - case reflect.Map: - // TODO + switch f.Value.Kind() { + case reflect.Slice: + return in.setSlice(f, items) - case reflect.Ptr: - pv := reflect.New(f.Value.Type().Elem()) - f.Value.Set(pv) - pointedField := Field{ - Value: pv.Elem(), + case reflect.Array: + return in.setArray(f, items) + } + + return nil +} + +func (in *Input) setSlice(f *Field, items []string) error { + size := len(items) + if size == 0 { + return nil + } + s := reflect.MakeSlice(reflect.SliceOf(f.Value.Type().Elem()), size, size) + + for i := range items { + nestedField := Field{ + Value: s.Index(i), Tags: f.Tags, Path: f.Path, } - return in.setValue(&pointedField, value) + if err := in.SetValue(&nestedField, items[i]); err != nil { + return err + } + } - case reflect.Struct: - if isTime(f.Value) { - t, err := time.Parse(f.Tags.Format, value) - if err != nil { - return fmt.Errorf( - parseErrFormat, - ErrParsing, in.getPath(f.Path), err, - ) - } + f.Value.Set(s) + return nil +} - f.Value.Set(reflect.ValueOf(t)) - return nil - } +func (in *Input) setArray(f *Field, items []string) error { + size := f.Value.Len() + if size == 0 || len(items) == 0 { + return nil + } - if isURL(f.Value) { - u, err := url.Parse(value) - if err != nil { - return fmt.Errorf( - parseErrFormat, - ErrParsing, in.getPath(f.Path), err, - ) - } + at := reflect.ArrayOf(size, f.Value.Type().Elem()) + av := reflect.New(at).Elem() - f.Value.Set(reflect.ValueOf(*u)) - return nil + for i := 0; i < size; i++ { + nestedField := Field{ + Value: av.Index(i), + Tags: f.Tags, + Path: f.Path, } - default: + if err := in.SetValue(&nestedField, items[i]); err != nil { + return err + } + } + + f.Value.Set(av) + return nil +} + +func (in *Input) setMap(f *Field, value string) error { + // TODO + return nil +} + +func (in *Input) setPointer(f *Field, value string) error { + p := reflect.New(f.Value.Type().Elem()) + f.Value.Set(p) + pointedField := Field{ + Value: p.Elem(), + Tags: f.Tags, + Path: f.Path, + } + + return in.SetValue(&pointedField, value) +} + +func (in *Input) setDuration(f *Field, value string) error { + d, err := time.ParseDuration(value) + if err != nil { return fmt.Errorf( - unsupportedTypeErrFormat, - ErrUnsupportedType, f.Value.Kind(), in.getPath(f.Path), + parseErrFormat, + ErrParsing, in.getPath(f.Path), err, + ) + } + if f.Value.OverflowInt(int64(d)) { + return fmt.Errorf( + overflowErrFormat, + ErrValueOverflow, d, f.Value.Kind(), in.getPath(f.Path), + ) + } + + f.Value.SetInt(int64(d)) + return nil +} + +func (in *Input) setTime(f *Field, value string) error { + t, err := time.Parse(f.Tags.Format, value) + if err != nil { + return fmt.Errorf( + parseErrFormat, + ErrParsing, in.getPath(f.Path), err, + ) + } + + f.Value.Set(reflect.ValueOf(t)) + return nil +} + +func (in *Input) setUrl(f *Field, value string) error { + u, err := url.Parse(value) + if err != nil { + return fmt.Errorf( + parseErrFormat, + ErrParsing, in.getPath(f.Path), err, ) } + f.Value.Set(reflect.ValueOf(*u)) return nil } diff --git a/interfaces.go b/interfaces.go new file mode 100644 index 0000000..eac8b2e --- /dev/null +++ b/interfaces.go @@ -0,0 +1,18 @@ +package gonfig + +// Provider is used to provide values +// It can implement either Unmarshaler or Filler interface or both +// Name method is used for error messages +type Provider interface { + Name() string +} + +// Unmarshaler can be implemented by providers to receive struct pointer and unmarshal values into it +type Unmarshaler interface { + UnmarshalStruct(i interface{}) (err error) +} + +// Filler can be implemented by providers to receive struct fields and set their value +type Filler interface { + Fill(in *Input) (err error) +} diff --git a/tags.go b/tags.go index c702a58..1c56f1c 100644 --- a/tags.go +++ b/tags.go @@ -46,7 +46,7 @@ type ConfigTags struct { } // Returns default config tags. -func getTags(st reflect.StructTag) *ConfigTags { +func extractTags(st reflect.StructTag) *ConfigTags { tags := ConfigTags{ Config: st.Get("config"), Default: st.Get("default"), diff --git a/utils.go b/utils.go index 762cfdc..09b8420 100644 --- a/utils.go +++ b/utils.go @@ -34,9 +34,9 @@ func isURL(v reflect.Value) bool { } // traverseMap finds a value in a map based on provided path -func traverseMap(m map[string]interface{}, path []string) (string, error) { +func traverseMap(m map[string]interface{}, path []string) (string, bool) { if len(path) == 0 { - return "", ErrKeyNotFound + return "", false } first, path := path[0], path[1:] @@ -44,17 +44,17 @@ func traverseMap(m map[string]interface{}, path []string) (string, error) { if !exists { value, exists = m[strings.ToLower(first)] if !exists { - return "", ErrKeyNotFound + return "", false } } if len(path) == 0 { - return fmt.Sprint(value), nil + return fmt.Sprint(value), true } nestedMap, ok := value.(map[string]interface{}) if !ok { - return "", ErrKeyNotFound + return "", false } return traverseMap(nestedMap, path)