/* * Copyright (C) 2017 eschao * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package config import ( "bytes" "fmt" "io" "os" "reflect" "strings" ) // ParseValueEnvFunc func to parse env value type ParseValueEnvFunc func(value reflect.Value, prefix string) error // Parse parses given structure interface, extracts environment definitions // from its tag and sets structure with defined environment variables func ParseEnv(out interface{}) error { return ParseEnvWith(out, "", parseValueEnv) } // UsageEnv prints usage description of config i to out func UsageEnv(out io.Writer, in interface{}) error { if _, err := out.Write([]byte("Environment Usage:\n")); err != nil { return fmt.Errorf("write usage: %w", err) } buf := bytes.NewBufferString("") if err := ParseEnvWith(in, "", func(value reflect.Value, prefix string) error { return usageValueEnv(buf, value, prefix) }); err != nil { return err } var tabPos int for _, line := range strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") { if pos := strings.Index(line, "\t"); pos > tabPos { tabPos = pos } } const TabSize = 8 for _, line := range strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") { if pos := strings.Index(line, "\t"); pos >= 0 { if count := tabPos/TabSize - pos/TabSize; count > 0 { line = line[:pos] + strings.Repeat("\t", count) + line[pos:] } } if _, err := out.Write([]byte(line + "\n")); err != nil { return fmt.Errorf("write usage: %w", err) } } return nil } // ParseWith parses with given structure interface and environment name prefix // It is normally used in nested structure. // For example: we have such structure // type Database struct { // Host string `env:"HOST"` // } // type Server struct { // Server string `env:"SERVER"` // DB Database `env:"DB_"` // } // // The Server.DB.Host will be mapped to environment variable: DB_HOST which is // concatenated from DB tag in Server struct and Host tag in Database struct func ParseEnvWith(out interface{}, prefix string, parser ParseValueEnvFunc) error { ptrRef := reflect.ValueOf(out) if ptrRef.IsNil() || ptrRef.Kind() != reflect.Ptr { return fmt.Errorf("%w: %s", errExpectStructPointerInsteadOf, ptrRef.Kind().String()) } valueOfStruct := ptrRef.Elem() if valueOfStruct.Kind() != reflect.Struct { return fmt.Errorf("%w: %s", errExpectStructPointerInsteadOf, valueOfStruct.Kind().String()) } return parser(valueOfStruct, prefix) } func usageValueEnv(out io.Writer, value reflect.Value, prefix string) error { var err error typeOfStruct := value.Type() for i := 0; i < value.NumField() && err == nil; i++ { valueOfField := value.Field(i) kindOfField := valueOfField.Kind() structOfField := typeOfStruct.Field(i) // Recursively unmarshal if value is ptr type if kindOfField == reflect.Ptr { if !valueOfField.IsNil() && valueOfField.CanSet() { err = ParseEnvWith( valueOfField.Interface(), prefix+structOfField.Tag.Get("env"), func(value reflect.Value, prefix string) error { return usageValueEnv(out, value, prefix) }, ) } else { continue } } else if kindOfField == reflect.Struct { err = usageValueEnv(out, valueOfField, prefix+structOfField.Tag.Get("env")) } if err != nil { return err } if kindOfField != reflect.Struct { err = usageFieldValueEnv(out, structOfField, prefix) } } return err } // parseValue parses a reflect.Value object func parseValueEnv(value reflect.Value, prefix string) error { var err error typeOfStruct := value.Type() for i := 0; i < value.NumField() && err == nil; i++ { valueOfField := value.Field(i) kindOfField := valueOfField.Kind() structOfField := typeOfStruct.Field(i) // Recursively unmarshal if value is ptr type if kindOfField == reflect.Ptr { if !valueOfField.IsNil() && valueOfField.CanSet() { err = ParseEnvWith( valueOfField.Interface(), prefix+structOfField.Tag.Get("env"), parseValueEnv, ) } else { continue } } else if kindOfField == reflect.Struct { err = parseValueEnv(valueOfField, prefix+structOfField.Tag.Get("env")) } if err != nil { return err } if kindOfField != reflect.Struct { err = setFieldValueEnv(valueOfField, structOfField, prefix) } } return err } // setFieldValue sets a reflect.Value with environment value func setFieldValueEnv(value reflect.Value, field reflect.StructField, prefix string) error { envName := field.Tag.Get("env") if envName == "" { return nil } envValue, ok := os.LookupEnv(prefix + envName) if !ok { return nil } if !value.CanSet() { return fmt.Errorf("%w: %s", errValueCannotBeChanged, field.Name) } if setUnmarshalTextValue(value, envValue) { return nil } if ok, err := setDurationValue(value, envValue); ok { return err } return setSimpleEnvValue(value, field, envValue) } func setSimpleEnvValue(value reflect.Value, field reflect.StructField, str string) error { var err error kind := value.Kind() switch kind { case reflect.Bool: err = setValueWithBool(value, str) case reflect.String: value.SetString(str) case reflect.Int8: err = setValueWithIntX(value, str, Int8Size) case reflect.Int16: err = setValueWithIntX(value, str, Int16Size) case reflect.Int, reflect.Int32: err = setValueWithIntX(value, str, Int32Size) case reflect.Int64: err = setValueWithIntX(value, str, Int64Size) case reflect.Uint8: err = setValueWithUintX(value, str, Uint8Size) case reflect.Uint16: err = setValueWithUintX(value, str, Uint16Size) case reflect.Uint, reflect.Uint32: err = setValueWithUintX(value, str, Uint32Size) case reflect.Uint64: err = setValueWithUintX(value, str, Uint64Size) case reflect.Float32: err = setValueWithFloatX(value, str, Float32Size) case reflect.Float64: err = setValueWithFloatX(value, str, Float64Size) case reflect.Slice: sp, ok := field.Tag.Lookup("separator") if !ok { sp = ":" } err = setValueWithSlice(value, str, sp) default: return fmt.Errorf("%w: %s", errUnsupportedType, kind.String()) } if err != nil { err = fmt.Errorf("%s: %w", field.Name, err) } return err } func usageFieldValueEnv(out io.Writer, field reflect.StructField, prefix string) error { envName := field.Tag.Get("env") if envName == "" { return nil } envName = prefix + envName msg := " " + envName + "\t" + field.Tag.Get("usage") if def := field.Tag.Get("default"); def != "" { msg += " (default: " + def + ")" } if _, err := out.Write([]byte(msg + "\n")); err != nil { return fmt.Errorf("write usage: %w", err) } return nil }