|
- // Copyright (c) 2013 Kelsey Hightower. All rights reserved.
- // Use of this source code is governed by the MIT License that can be found in
- // the LICENSE file.
-
- package envconfig
-
- import (
- "encoding"
- "errors"
- "fmt"
- "reflect"
- "regexp"
- "strconv"
- "strings"
- "time"
- )
-
- // ErrInvalidSpecification indicates that a specification is of the wrong type.
- var ErrInvalidSpecification = errors.New("specification must be a struct pointer")
-
- // A ParseError occurs when an environment variable cannot be converted to
- // the type required by a struct field during assignment.
- type ParseError struct {
- KeyName string
- FieldName string
- TypeName string
- Value string
- Err error
- }
-
- // Decoder has the same semantics as Setter, but takes higher precedence.
- // It is provided for historical compatibility.
- type Decoder interface {
- Decode(value string) error
- }
-
- // Setter is implemented by types can self-deserialize values.
- // Any type that implements flag.Value also implements Setter.
- type Setter interface {
- Set(value string) error
- }
-
- func (e *ParseError) Error() string {
- return fmt.Sprintf("envconfig.Process: assigning %[1]s to %[2]s: converting '%[3]s' to type %[4]s. details: %[5]s", e.KeyName, e.FieldName, e.Value, e.TypeName, e.Err)
- }
-
- // varInfo maintains information about the configuration variable
- type varInfo struct {
- Name string
- Alt string
- Key string
- Field reflect.Value
- Tags reflect.StructTag
- }
-
- // GatherInfo gathers information about the specified struct
- func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) {
- expr := regexp.MustCompile("([^A-Z]+|[A-Z][^A-Z]+|[A-Z]+)")
- s := reflect.ValueOf(spec)
-
- if s.Kind() != reflect.Ptr {
- return nil, ErrInvalidSpecification
- }
- s = s.Elem()
- if s.Kind() != reflect.Struct {
- return nil, ErrInvalidSpecification
- }
- typeOfSpec := s.Type()
-
- // over allocate an info array, we will extend if needed later
- infos := make([]varInfo, 0, s.NumField())
- for i := 0; i < s.NumField(); i++ {
- f := s.Field(i)
- ftype := typeOfSpec.Field(i)
- if !f.CanSet() || ftype.Tag.Get("ignored") == "true" {
- continue
- }
-
- for f.Kind() == reflect.Ptr {
- if f.IsNil() {
- if f.Type().Elem().Kind() != reflect.Struct {
- // nil pointer to a non-struct: leave it alone
- break
- }
- // nil pointer to struct: create a zero instance
- f.Set(reflect.New(f.Type().Elem()))
- }
- f = f.Elem()
- }
-
- // Capture information about the config variable
- info := varInfo{
- Name: ftype.Name,
- Field: f,
- Tags: ftype.Tag,
- Alt: strings.ToUpper(ftype.Tag.Get("envconfig")),
- }
-
- // Default to the field name as the env var name (will be upcased)
- info.Key = info.Name
-
- // Best effort to un-pick camel casing as separate words
- if ftype.Tag.Get("split_words") == "true" {
- words := expr.FindAllStringSubmatch(ftype.Name, -1)
- if len(words) > 0 {
- var name []string
- for _, words := range words {
- name = append(name, words[0])
- }
-
- info.Key = strings.Join(name, "_")
- }
- }
- if info.Alt != "" {
- info.Key = info.Alt
- }
- if prefix != "" {
- info.Key = fmt.Sprintf("%s_%s", prefix, info.Key)
- }
- info.Key = strings.ToUpper(info.Key)
- infos = append(infos, info)
-
- if f.Kind() == reflect.Struct {
- // honor Decode if present
- if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil {
- innerPrefix := prefix
- if !ftype.Anonymous {
- innerPrefix = info.Key
- }
-
- embeddedPtr := f.Addr().Interface()
- embeddedInfos, err := gatherInfo(innerPrefix, embeddedPtr)
- if err != nil {
- return nil, err
- }
- infos = append(infos[:len(infos)-1], embeddedInfos...)
-
- continue
- }
- }
- }
- return infos, nil
- }
-
- // Process populates the specified struct based on environment variables
- func Process(prefix string, spec interface{}) error {
- infos, err := gatherInfo(prefix, spec)
-
- for _, info := range infos {
-
- // `os.Getenv` cannot differentiate between an explicitly set empty value
- // and an unset value. `os.LookupEnv` is preferred to `syscall.Getenv`,
- // but it is only available in go1.5 or newer. We're using Go build tags
- // here to use os.LookupEnv for >=go1.5
- value, ok := lookupEnv(info.Key)
- if !ok && info.Alt != "" {
- value, ok = lookupEnv(info.Alt)
- }
-
- def := info.Tags.Get("default")
- if def != "" && !ok {
- value = def
- }
-
- req := info.Tags.Get("required")
- if !ok && def == "" {
- if req == "true" {
- return fmt.Errorf("required key %s missing value", info.Key)
- }
- continue
- }
-
- err := processField(value, info.Field)
- if err != nil {
- return &ParseError{
- KeyName: info.Key,
- FieldName: info.Name,
- TypeName: info.Field.Type().String(),
- Value: value,
- Err: err,
- }
- }
- }
-
- return err
- }
-
- // MustProcess is the same as Process but panics if an error occurs
- func MustProcess(prefix string, spec interface{}) {
- if err := Process(prefix, spec); err != nil {
- panic(err)
- }
- }
-
- func processField(value string, field reflect.Value) error {
- typ := field.Type()
-
- decoder := decoderFrom(field)
- if decoder != nil {
- return decoder.Decode(value)
- }
- // look for Set method if Decode not defined
- setter := setterFrom(field)
- if setter != nil {
- return setter.Set(value)
- }
-
- if t := textUnmarshaler(field); t != nil {
- return t.UnmarshalText([]byte(value))
- }
-
- if typ.Kind() == reflect.Ptr {
- typ = typ.Elem()
- if field.IsNil() {
- field.Set(reflect.New(typ))
- }
- field = field.Elem()
- }
-
- switch typ.Kind() {
- case reflect.String:
- field.SetString(value)
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- var (
- val int64
- err error
- )
- if field.Kind() == reflect.Int64 && typ.PkgPath() == "time" && typ.Name() == "Duration" {
- var d time.Duration
- d, err = time.ParseDuration(value)
- val = int64(d)
- } else {
- val, err = strconv.ParseInt(value, 0, typ.Bits())
- }
- if err != nil {
- return err
- }
-
- field.SetInt(val)
- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
- val, err := strconv.ParseUint(value, 0, typ.Bits())
- if err != nil {
- return err
- }
- field.SetUint(val)
- case reflect.Bool:
- val, err := strconv.ParseBool(value)
- if err != nil {
- return err
- }
- field.SetBool(val)
- case reflect.Float32, reflect.Float64:
- val, err := strconv.ParseFloat(value, typ.Bits())
- if err != nil {
- return err
- }
- field.SetFloat(val)
- case reflect.Slice:
- vals := strings.Split(value, ",")
- sl := reflect.MakeSlice(typ, len(vals), len(vals))
- for i, val := range vals {
- err := processField(val, sl.Index(i))
- if err != nil {
- return err
- }
- }
- field.Set(sl)
- case reflect.Map:
- pairs := strings.Split(value, ",")
- mp := reflect.MakeMap(typ)
- for _, pair := range pairs {
- kvpair := strings.Split(pair, ":")
- if len(kvpair) != 2 {
- return fmt.Errorf("invalid map item: %q", pair)
- }
- k := reflect.New(typ.Key()).Elem()
- err := processField(kvpair[0], k)
- if err != nil {
- return err
- }
- v := reflect.New(typ.Elem()).Elem()
- err = processField(kvpair[1], v)
- if err != nil {
- return err
- }
- mp.SetMapIndex(k, v)
- }
- field.Set(mp)
- }
-
- return nil
- }
-
- func interfaceFrom(field reflect.Value, fn func(interface{}, *bool)) {
- // it may be impossible for a struct field to fail this check
- if !field.CanInterface() {
- return
- }
- var ok bool
- fn(field.Interface(), &ok)
- if !ok && field.CanAddr() {
- fn(field.Addr().Interface(), &ok)
- }
- }
-
- func decoderFrom(field reflect.Value) (d Decoder) {
- interfaceFrom(field, func(v interface{}, ok *bool) { d, *ok = v.(Decoder) })
- return d
- }
-
- func setterFrom(field reflect.Value) (s Setter) {
- interfaceFrom(field, func(v interface{}, ok *bool) { s, *ok = v.(Setter) })
- return s
- }
-
- func textUnmarshaler(field reflect.Value) (t encoding.TextUnmarshaler) {
- interfaceFrom(field, func(v interface{}, ok *bool) { t, *ok = v.(encoding.TextUnmarshaler) })
- return t
- }
|