Initial QSfera import
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
// Package envdecode is a package for populating structs from environment
|
||||
// variables, using struct tags.
|
||||
package envdecode
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrInvalidTarget indicates that the target value passed to
|
||||
// Decode is invalid. Target must be a non-nil pointer to a struct.
|
||||
var ErrInvalidTarget = errors.New("target must be non-nil pointer to struct that has at least one exported field with a valid env tag")
|
||||
var ErrNoTargetFieldsAreSet = errors.New("none of the target fields were set from environment variables")
|
||||
|
||||
// FailureFunc is called when an error is encountered during a MustDecode
|
||||
// operation. It prints the error and terminates the process.
|
||||
//
|
||||
// This variable can be assigned to another function of the user-programmer's
|
||||
// design, allowing for graceful recovery of the problem, such as loading
|
||||
// from a backup configuration file.
|
||||
var FailureFunc = func(err error) {
|
||||
log.Fatalf("envdecode: an error was encountered while decoding: %v\n", err)
|
||||
}
|
||||
|
||||
// Decoder is the interface implemented by an object that can decode an
|
||||
// environment variable string representation of itself.
|
||||
type Decoder interface {
|
||||
Decode(string) error
|
||||
}
|
||||
|
||||
// Decode environment variables into the provided target. The target
|
||||
// must be a non-nil pointer to a struct. Fields in the struct must
|
||||
// be exported, and tagged with an "env" struct tag with a value
|
||||
// containing the name of the environment variable. An error is
|
||||
// returned if there are no exported members tagged.
|
||||
//
|
||||
// Default values may be provided by appending ",default=value" to the
|
||||
// struct tag. Required values may be marked by appending ",required"
|
||||
// to the struct tag. It is an error to provide both "default" and
|
||||
// "required". Strict values may be marked by appending ",strict" which
|
||||
// will return an error on Decode if there is an error while parsing.
|
||||
// If everything must be strict, consider using StrictDecode instead.
|
||||
//
|
||||
// All primitive types are supported, including bool, floating point,
|
||||
// signed and unsigned integers, and string. Boolean and numeric
|
||||
// types are decoded using the standard strconv Parse functions for
|
||||
// those types. Structs and pointers to structs are decoded
|
||||
// recursively. time.Duration is supported via the
|
||||
// time.ParseDuration() function and *url.URL is supported via the
|
||||
// url.Parse() function. Slices are supported for all above mentioned
|
||||
// primitive types. Semicolon is used as delimiter in environment variables.
|
||||
func Decode(target any) error {
|
||||
nFields, err := decode(target, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we didn't do anything - the user probably did something
|
||||
// wrong like leave all fields unexported.
|
||||
if nFields == 0 {
|
||||
return ErrNoTargetFieldsAreSet
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StrictDecode is similar to Decode except all fields will have an implicit
|
||||
// ",strict" on all fields.
|
||||
func StrictDecode(target any) error {
|
||||
nFields, err := decode(target, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we didn't do anything - the user probably did something
|
||||
// wrong like leave all fields unexported.
|
||||
if nFields == 0 {
|
||||
return ErrInvalidTarget
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decode(target any, strict bool) (int, error) {
|
||||
s := reflect.ValueOf(target)
|
||||
if s.Kind() != reflect.Ptr || s.IsNil() {
|
||||
return 0, ErrInvalidTarget
|
||||
}
|
||||
|
||||
s = s.Elem()
|
||||
if s.Kind() != reflect.Struct {
|
||||
return 0, ErrInvalidTarget
|
||||
}
|
||||
|
||||
t := s.Type()
|
||||
setFieldCount := 0
|
||||
for i := 0; i < s.NumField(); i++ {
|
||||
// Localize the umbrella `strict` value to the specific field.
|
||||
strict := strict
|
||||
|
||||
f := s.Field(i)
|
||||
|
||||
switch f.Kind() {
|
||||
case reflect.Ptr:
|
||||
if f.Elem().Kind() != reflect.Struct {
|
||||
break
|
||||
}
|
||||
|
||||
f = f.Elem()
|
||||
fallthrough
|
||||
|
||||
case reflect.Struct:
|
||||
if !f.Addr().CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
ss := f.Addr().Interface()
|
||||
_, custom := ss.(Decoder)
|
||||
if custom {
|
||||
break
|
||||
}
|
||||
|
||||
n, err := decode(ss, strict)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
setFieldCount += n
|
||||
}
|
||||
|
||||
if !f.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := t.Field(i).Tag.Get("env")
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(tag, ",")
|
||||
overrides := strings.Split(parts[0], `;`)
|
||||
|
||||
var env string
|
||||
var envSet bool
|
||||
for _, override := range overrides {
|
||||
if v, set := os.LookupEnv(override); set {
|
||||
env = v
|
||||
envSet = true
|
||||
}
|
||||
}
|
||||
|
||||
required := false
|
||||
hasDefault := false
|
||||
defaultValue := ""
|
||||
|
||||
for _, o := range parts[1:] {
|
||||
if !required {
|
||||
required = strings.HasPrefix(o, "required")
|
||||
}
|
||||
if strings.HasPrefix(o, "default=") {
|
||||
hasDefault = true
|
||||
defaultValue = o[8:]
|
||||
}
|
||||
if !strict {
|
||||
strict = strings.HasPrefix(o, "strict")
|
||||
}
|
||||
}
|
||||
|
||||
if required && hasDefault {
|
||||
panic(`envdecode: "default" and "required" may not be specified in the same annotation`)
|
||||
}
|
||||
if !envSet && required {
|
||||
return 0, fmt.Errorf("the environment variable \"%s\" is missing", parts[0])
|
||||
}
|
||||
if !envSet {
|
||||
env = defaultValue
|
||||
}
|
||||
if !envSet && env == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
setFieldCount++
|
||||
|
||||
unmarshaler, implementsUnmarshaler := f.Addr().Interface().(encoding.TextUnmarshaler)
|
||||
decoder, implmentsDecoder := f.Addr().Interface().(Decoder)
|
||||
if implmentsDecoder {
|
||||
if err := decoder.Decode(env); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else if implementsUnmarshaler {
|
||||
if err := unmarshaler.UnmarshalText([]byte(env)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else if f.Kind() == reflect.Slice {
|
||||
if err := decodeSlice(&f, env); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
if err := decodePrimitiveType(&f, env); err != nil && strict {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return setFieldCount, nil
|
||||
}
|
||||
|
||||
func decodeSlice(f *reflect.Value, env string) error {
|
||||
parts := strings.Split(env, ",")
|
||||
|
||||
values := parts[:0]
|
||||
for _, x := range parts {
|
||||
if x != "" {
|
||||
values = append(values, strings.TrimSpace(x))
|
||||
}
|
||||
}
|
||||
|
||||
valuesCount := len(values)
|
||||
slice := reflect.MakeSlice(f.Type(), valuesCount, valuesCount)
|
||||
if valuesCount > 0 {
|
||||
for i := 0; i < valuesCount; i++ {
|
||||
e := slice.Index(i)
|
||||
err := decodePrimitiveType(&e, values[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.Set(slice)
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodePrimitiveType(f *reflect.Value, env string) error {
|
||||
switch f.Kind() {
|
||||
case reflect.Bool:
|
||||
v, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetBool(v)
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
bits := f.Type().Bits()
|
||||
v, err := strconv.ParseFloat(env, bits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetFloat(v)
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if t := f.Type(); t.PkgPath() == "time" && t.Name() == "Duration" {
|
||||
v, err := time.ParseDuration(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetInt(int64(v))
|
||||
} else {
|
||||
bits := f.Type().Bits()
|
||||
v, err := strconv.ParseInt(env, 0, bits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetInt(v)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
bits := f.Type().Bits()
|
||||
v, err := strconv.ParseUint(env, 0, bits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetUint(v)
|
||||
|
||||
case reflect.String:
|
||||
f.SetString(env)
|
||||
|
||||
case reflect.Ptr:
|
||||
if t := f.Type().Elem(); t.Kind() == reflect.Struct && t.PkgPath() == "net/url" && t.Name() == "URL" {
|
||||
v, err := url.Parse(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Set(reflect.ValueOf(v))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MustDecode calls Decode and terminates the process if any errors
|
||||
// are encountered.
|
||||
func MustDecode(target any) {
|
||||
if err := Decode(target); err != nil {
|
||||
FailureFunc(err)
|
||||
}
|
||||
}
|
||||
|
||||
// MustStrictDecode calls StrictDecode and terminates the process if any errors
|
||||
// are encountered.
|
||||
func MustStrictDecode(target any) {
|
||||
if err := StrictDecode(target); err != nil {
|
||||
FailureFunc(err)
|
||||
}
|
||||
}
|
||||
|
||||
//// Configuration info for Export
|
||||
|
||||
type ConfigInfo struct {
|
||||
Field string
|
||||
EnvVar string
|
||||
Value string
|
||||
DefaultValue string
|
||||
HasDefault bool
|
||||
Required bool
|
||||
UsesEnv bool
|
||||
}
|
||||
|
||||
type ConfigInfoSlice []*ConfigInfo
|
||||
|
||||
func (c ConfigInfoSlice) Less(i, j int) bool {
|
||||
return c[i].EnvVar < c[j].EnvVar
|
||||
}
|
||||
func (c ConfigInfoSlice) Len() int {
|
||||
return len(c)
|
||||
}
|
||||
func (c ConfigInfoSlice) Swap(i, j int) {
|
||||
c[i], c[j] = c[j], c[i]
|
||||
}
|
||||
|
||||
// Returns a list of final configuration metadata sorted by envvar name
|
||||
func Export(target any) ([]*ConfigInfo, error) {
|
||||
s := reflect.ValueOf(target)
|
||||
if s.Kind() != reflect.Ptr || s.IsNil() {
|
||||
return nil, ErrInvalidTarget
|
||||
}
|
||||
|
||||
cfg := []*ConfigInfo{}
|
||||
|
||||
s = s.Elem()
|
||||
if s.Kind() != reflect.Struct {
|
||||
return nil, ErrInvalidTarget
|
||||
}
|
||||
|
||||
t := s.Type()
|
||||
for i := 0; i < s.NumField(); i++ {
|
||||
f := s.Field(i)
|
||||
fName := t.Field(i).Name
|
||||
|
||||
fElem := f
|
||||
if f.Kind() == reflect.Ptr {
|
||||
fElem = f.Elem()
|
||||
}
|
||||
if fElem.Kind() == reflect.Struct {
|
||||
ss := fElem.Addr().Interface()
|
||||
subCfg, err := Export(ss)
|
||||
if err != ErrInvalidTarget {
|
||||
f = fElem
|
||||
for _, v := range subCfg {
|
||||
v.Field = fmt.Sprintf("%s.%s", fName, v.Field)
|
||||
cfg = append(cfg, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tag := t.Field(i).Tag.Get("env")
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(tag, ",")
|
||||
|
||||
ci := &ConfigInfo{
|
||||
Field: fName,
|
||||
EnvVar: parts[0],
|
||||
UsesEnv: os.Getenv(parts[0]) != "",
|
||||
}
|
||||
|
||||
for _, o := range parts[1:] {
|
||||
if strings.HasPrefix(o, "default=") {
|
||||
ci.HasDefault = true
|
||||
ci.DefaultValue = o[8:]
|
||||
} else if strings.HasPrefix(o, "required") {
|
||||
ci.Required = true
|
||||
}
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Ptr && f.IsNil() {
|
||||
ci.Value = ""
|
||||
} else if stringer, ok := f.Interface().(fmt.Stringer); ok {
|
||||
ci.Value = stringer.String()
|
||||
} else {
|
||||
switch f.Kind() {
|
||||
case reflect.Bool:
|
||||
ci.Value = strconv.FormatBool(f.Bool())
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
bits := f.Type().Bits()
|
||||
ci.Value = strconv.FormatFloat(f.Float(), 'f', -1, bits)
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
ci.Value = strconv.FormatInt(f.Int(), 10)
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
ci.Value = strconv.FormatUint(f.Uint(), 10)
|
||||
|
||||
case reflect.String:
|
||||
ci.Value = f.String()
|
||||
|
||||
case reflect.Slice:
|
||||
ci.Value = fmt.Sprintf("%v", f.Interface())
|
||||
|
||||
default:
|
||||
// Unable to determine string format for value
|
||||
return nil, ErrInvalidTarget
|
||||
}
|
||||
}
|
||||
|
||||
cfg = append(cfg, ci)
|
||||
}
|
||||
|
||||
// No configuration tags found, assume invalid input
|
||||
if len(cfg) == 0 {
|
||||
return nil, ErrInvalidTarget
|
||||
}
|
||||
|
||||
sort.Sort(ConfigInfoSlice(cfg))
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
Reference in New Issue
Block a user