package config import ( "fmt" "os" "reflect" "strconv" "strings" ) type ConfigBuilder struct { ConfigFetchMethod func()[]string typeParsers map[reflect.Kind]func(string)(interface{}, error) } func NewBuilder() *ConfigBuilder { return &ConfigBuilder{ ConfigFetchMethod: os.Environ, typeParsers: map[reflect.Kind]func(string)(interface{}, error) { reflect.String: func(value string) (interface{}, error) { return value, nil }, reflect.Int: func(value string) (interface{}, error) { i, err := strconv.Atoi(value) if err != nil { return nil, err } return i, nil }, reflect.Bool: func(value string) (interface{}, error) { b, err := strconv.ParseBool(value) if err != nil { return nil, err } return b, nil }, }, } } func (cb *ConfigBuilder) Build(prefix string, configStruct interface{}) error { assertIsPtr(configStruct) fieldNames := getConfigStructFieldNames(configStruct) config := cb.loadConfig(prefix) structRef := reflect.ValueOf(configStruct) for _, fieldName := range fieldNames { value, ok := config[fieldName] if !ok { // no such field loaded, avoid overwriting default if field is optional field, _ := reflect.TypeOf(configStruct).Elem().FieldByName(fieldName) if strings.Contains(string(field.Tag), "optional") { continue } return fmt.Errorf("no config found for required field '%s'", fieldName) } field := reflect.Indirect(structRef).FieldByName(fieldName) typeRef := field.Type() typedValue, err := cb.tryParseString(value, typeRef.Kind()) if err != nil { return err } field.Set(reflect.ValueOf(typedValue).Convert(typeRef)) } return nil } func assertIsPtr(configStruct interface{}) { r := reflect.ValueOf(configStruct) if r.Kind() != reflect.Ptr { panic(fmt.Errorf("supplied value is not a pointer to a struct")) } } func getConfigStructFieldNames(configStruct interface{}) []string { configFields := []string{} structValue := reflect.ValueOf(configStruct).Elem() for i := 0; i < structValue.NumField(); i++ { configFields = append(configFields, structValue.Type().Field(i).Name) } return configFields } func (cb *ConfigBuilder) loadConfig(prefix string) map[string]string { config := map[string]string{} for _, env := range cb.ConfigFetchMethod() { parts := strings.SplitN(env, "=", 2) key, value := parts[0], parts[1] if strings.HasPrefix(key, prefix) { key = strings.TrimPrefix(key, prefix) config[key] = value } } return config } func (cb *ConfigBuilder) tryParseString(value string, toType reflect.Kind) (interface{}, error) { parser, ok := cb.typeParsers[toType] if !ok { return nil, fmt.Errorf("no parser found for type %v", toType) } parsed_val, err := parser(value) if err != nil { return nil, err } return parsed_val, nil } func (cb *ConfigBuilder) RegisterParser(toType reflect.Kind, parser func(string)(interface{}, error)) { cb.typeParsers[toType] = parser }