package config import ( "encoding/json" "os" "reflect" "regexp" "sort" "strconv" "strings" "gitlab.com/uafrica/go-utils/errors" ) func Load(prefix string, configPtr interface{}) error { if !prefixRegex.MatchString(prefix) { return errors.Errorf("invalid config prefix \"%s\"", prefix) } if configPtr == nil { return errors.Errorf("Load(nil)") } t := reflect.TypeOf(configPtr) if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct { return errors.Errorf("%T is not &struct", configPtr) } v := reflect.ValueOf(configPtr) if err := load(prefix, t.Elem(), v.Elem()); err != nil { return errors.Wrapf(err, "failed to load config with prefix %s", prefix) } if validator, ok := configPtr.(Validator); ok { if err := validator.Validate(); err != nil { return errors.Wrapf(err, "invalid config with prefix %s", prefix) } } return nil } type nameValue struct { name string value string } func load(prefix string, t reflect.Type, ptrValue reflect.Value) error { switch t.Kind() { case reflect.Struct: for i := 0; i < t.NumField(); i++ { f := t.Field(i) if err := load(prefix+"_"+strings.ToUpper(f.Name), f.Type, ptrValue.Field(i)); err != nil { return errors.Wrapf(err, "cannot load field") } } case reflect.Slice: //expect JSON list of values or just one value s := os.Getenv(prefix) if s != "" { if err := json.Unmarshal([]byte(s), ptrValue.Addr().Interface()); err != nil { return errors.Wrapf(err, "cannot read env %s=%s into %s", prefix, s, t.Name()) } } else { //see if _1, _2, ... is used then construct a list with those values //(only applies to list of strings) values := map[string]string{} for _, x := range os.Environ() { parts := strings.SplitN(x, "=", 2) if len(parts) == 2 && strings.HasPrefix(parts[0], prefix+"_") { values[parts[0]] = parts[1] } } if len(values) > 0 { //add in sorted order list := []nameValue{} for n, v := range values { list = append(list, nameValue{name: n, value: v}) } sort.Slice(list, func(i, j int) bool { return list[i].name < list[j].name }) s := "" for _, nv := range list { if t.Elem().Kind() == reflect.String { s += ",\"" + nv.value + "\"" //quoted } else { s += "," + nv.value //unquoted } } s = "[" + s[1:] + "]" if err := json.Unmarshal([]byte(s), ptrValue.Addr().Interface()); err != nil { return errors.Wrapf(err, "cannot read env %s=%s into %s", prefix, s, t.Name()) } } } case reflect.String: s := os.Getenv(prefix) if s != "" { ptrValue.Set(reflect.ValueOf(s)) } case reflect.Int64: s := os.Getenv(prefix) if s != "" { i64, err := strconv.ParseInt(s, 10, 64) if err != nil { return errors.Errorf("%s=%s not integer value", prefix, s) } ptrValue.Set(reflect.ValueOf(i64)) } default: return errors.Errorf("cannot load config %s_... into %s kind %s", prefix, t.Name(), t.Kind()) } return nil } const prefixPattern = `[A-Z]([A-Z0-9_]*[A-Z0-9])*` var prefixRegex = regexp.MustCompile("^" + prefixPattern + "$")