env.go raw

   1  // Package env implements loading environment variables into a config struct.
   2  package env
   3  
   4  import (
   5  	"fmt"
   6  	"os"
   7  	"reflect"
   8  	"strings"
   9  )
  10  
  11  // Options are the options for the [Load] and [Usage] functions.
  12  type Options struct {
  13  	Source   Source // The source of environment variables. The default is [OS].
  14  	SliceSep string // The separator used to parse slice values. The default is space.
  15  	NameSep  string // The separator used to concatenate environment variable names from nested struct tags. The default is an empty string.
  16  }
  17  
  18  // NotSetError is returned when required environment variables are not set.
  19  type NotSetError struct {
  20  	Names []string
  21  }
  22  
  23  // Error implements the error interface.
  24  func (e *NotSetError) Error() string {
  25  	if len(e.Names) == 1 {
  26  		return fmt.Sprintf("env: %s is required but not set", e.Names[0])
  27  	}
  28  	return fmt.Sprintf("env: %s are required but not set", strings.Join(e.Names, " "))
  29  }
  30  
  31  // Load loads environment variables into the given struct.
  32  // cfg must be a non-nil struct pointer, otherwise Load panics.
  33  // If opts is nil, the default [Options] are used.
  34  //
  35  // The struct fields must have the `env:"VAR"` struct tag,
  36  // where VAR is the name of the corresponding environment variable.
  37  // Unexported fields are ignored.
  38  //
  39  // The following types are supported:
  40  //   - int (any kind)
  41  //   - float (any kind)
  42  //   - bool
  43  //   - string
  44  //   - [time.Duration]
  45  //   - [encoding.TextUnmarshaler]
  46  //   - slices of any type above
  47  //   - nested structs of any depth
  48  //
  49  // See the [strconv].Parse* functions for the parsing rules.
  50  // User-defined types can be used by implementing the [encoding.TextUnmarshaler] interface.
  51  //
  52  // Nested struct of any depth level are supported,
  53  // allowing grouping of related environment variables.
  54  // If a nested struct has the optional `env:"PREFIX"` tag,
  55  // the environment variables declared by its fields are prefixed with PREFIX.
  56  //
  57  // Default values can be specified using the `default:"VALUE"` struct tag.
  58  //
  59  // The name of an environment variable can be followed by comma-separated options:
  60  //   - required: marks the environment variable as required
  61  //   - expand: expands the value of the environment variable using [os.Expand]
  62  func Load(cfg any, opts *Options) error {
  63  	pv := reflect.ValueOf(cfg)
  64  	if !structPtr(pv) {
  65  		panic("env: cfg must be a non-nil struct pointer")
  66  	}
  67  
  68  	opts = setDefaultOptions(opts)
  69  
  70  	v := pv.Elem()
  71  	vars := parseVars(v, opts)
  72  	cache[v.Type()] = vars
  73  
  74  	var notset []string
  75  	for _, v := range vars {
  76  		value, ok := lookupEnv(opts.Source, v.Name, v.Expand)
  77  		if !ok {
  78  			if v.Required {
  79  				notset = append(notset, v.Name)
  80  				continue
  81  			}
  82  			if !v.hasDefaultTag {
  83  				continue // nothing to set.
  84  			}
  85  			value = v.Default
  86  		}
  87  
  88  		var err error
  89  		if kindOf(v.structField, reflect.Slice) && !implements(v.structField, unmarshalerIface) {
  90  			err = setSlice(v.structField, strings.Split(value, opts.SliceSep))
  91  		} else {
  92  			err = setValue(v.structField, value)
  93  		}
  94  		if err != nil {
  95  			return err
  96  		}
  97  	}
  98  
  99  	if len(notset) > 0 {
 100  		return &NotSetError{Names: notset}
 101  	}
 102  
 103  	return nil
 104  }
 105  
 106  func setDefaultOptions(opts *Options) *Options {
 107  	if opts == nil {
 108  		opts = new(Options)
 109  	}
 110  	if opts.Source == nil {
 111  		opts.Source = OS
 112  	}
 113  	if opts.SliceSep == "" {
 114  		opts.SliceSep = " "
 115  	}
 116  	return opts
 117  }
 118  
 119  func parseVars(v reflect.Value, opts *Options) []Var {
 120  	var vars []Var
 121  
 122  	for i := 0; i < v.NumField(); i++ {
 123  		field := v.Field(i)
 124  		if !field.CanSet() {
 125  			continue
 126  		}
 127  
 128  		tags := v.Type().Field(i).Tag
 129  
 130  		if kindOf(field, reflect.Struct) && !implements(field, unmarshalerIface) {
 131  			var prefix string
 132  			if value, ok := tags.Lookup("env"); ok {
 133  				prefix = value + opts.NameSep
 134  			}
 135  			for _, v := range parseVars(field, opts) {
 136  				v.Name = prefix + v.Name
 137  				vars = append(vars, v)
 138  			}
 139  			continue
 140  		}
 141  
 142  		value, ok := tags.Lookup("env")
 143  		if !ok {
 144  			continue
 145  		}
 146  
 147  		parts := strings.Split(value, ",")
 148  		name, options := parts[0], parts[1:]
 149  		if name == "" {
 150  			panic("env: empty tag name is not allowed")
 151  		}
 152  
 153  		var required, expand bool
 154  		for _, option := range options {
 155  			switch option {
 156  			case "required":
 157  				required = true
 158  			case "expand":
 159  				expand = true
 160  			default:
 161  				panic(fmt.Sprintf("env: invalid tag option `%s`", option))
 162  			}
 163  		}
 164  
 165  		defValue, defSet := tags.Lookup("default")
 166  		switch {
 167  		case defSet && required:
 168  			panic("env: `required` and `default` can't be used simultaneously")
 169  		case !defSet && !required:
 170  			defValue = fmt.Sprintf("%v", field.Interface())
 171  		}
 172  
 173  		vars = append(vars, Var{
 174  			Name:          name,
 175  			Type:          field.Type(),
 176  			Usage:         tags.Get("usage"),
 177  			Default:       defValue,
 178  			Required:      required,
 179  			Expand:        expand,
 180  			structField:   field,
 181  			hasDefaultTag: defSet,
 182  		})
 183  	}
 184  
 185  	return vars
 186  }
 187  
 188  func lookupEnv(src Source, key string, expand bool) (string, bool) {
 189  	value, ok := src.LookupEnv(key)
 190  	if !ok {
 191  		return "", false
 192  	}
 193  	if !expand {
 194  		return value, true
 195  	}
 196  	mapping := func(key string) string {
 197  		v, _ := src.LookupEnv(key)
 198  		return v
 199  	}
 200  	return os.Expand(value, mapping), true
 201  }
 202