Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 29 additions & 40 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"
)

// DecoderFunc is a function that converts a string value to a specific type.
type DecoderFunc func(key, value string) (reflect.Value, error)

var defaultDecoders = map[reflect.Type]DecoderFunc{
Expand Down Expand Up @@ -60,75 +61,68 @@ var defaultDecoders = map[reflect.Type]DecoderFunc{
},
}

// Setter is an interface that can be implemented by types that want to self-configure.
type Setter interface {
Set(value string) error
}

// setFieldValue determines the type of a config field, and branch out to the correct
// function to populate that data type.
func (s settings) setFieldValue(
configFieldValue reflect.Value,
entry entry,
) error {
fieldAddr := configFieldValue.Addr()
// setFieldValue sets the value of a field.
func (s *settings) setFieldValue(fieldValue reflect.Value, key, value string) error {
fieldAddr := fieldValue.Addr()

if setter, ok := fieldAddr.Interface().(Setter); ok {
return setter.Set(entry.value)
return setter.Set(value)
}

if dec, ok := s.decoders[configFieldValue.Type()]; ok {
decodedValue, err := dec(entry.key, entry.value)
if dec, ok := s.decoders[fieldValue.Type()]; ok {
decodedValue, err := dec(key, value)
if err != nil {
return err
}
configFieldValue.Set(decodedValue)
fieldValue.Set(decodedValue)
return nil
}

switch configFieldValue.Interface().(type) {
switch fieldValue.Interface().(type) {
case string:
configFieldValue.SetString(entry.value)
fieldValue.SetString(value)
case []string:
return setStringSliceFieldValue(configFieldValue, entry.value)
return setStringSliceFieldValue(fieldValue, value)
case []int:
return setIntSliceFieldValue(configFieldValue, entry)
return setIntSliceFieldValue(fieldValue, key, value)
case []float64:
return setFloatSliceFieldValue(configFieldValue, entry)
return setFloatSliceFieldValue(fieldValue, key, value)
default:
return &UnsupportedFieldTypeError{FieldType: configFieldValue.Interface()}
return &UnsupportedFieldTypeError{FieldType: fieldValue.Interface()}
}

return nil
}

func setStringSliceFieldValue(configFieldValue reflect.Value, environmentValue string) error {
values := strings.Split(environmentValue, ",")
slice := reflect.MakeSlice(configFieldValue.Type(), len(values), len(values))
func setStringSliceFieldValue(fieldValue reflect.Value, value string) error {
values := strings.Split(value, ",")
slice := reflect.MakeSlice(fieldValue.Type(), len(values), len(values))

for i, v := range values {
v = strings.TrimSpace(v)
slice.Index(i).SetString(v)
}

configFieldValue.Set(slice)

fieldValue.Set(slice)
return nil
}

func setIntSliceFieldValue(
configFieldValue reflect.Value,
entry entry,
) error {
values := strings.Split(entry.value, ",")
slice := reflect.MakeSlice(configFieldValue.Type(), len(values), len(values))
func setIntSliceFieldValue(fieldValue reflect.Value, key, value string) error {
values := strings.Split(value, ",")
slice := reflect.MakeSlice(fieldValue.Type(), len(values), len(values))

for i, v := range values {
v = strings.TrimSpace(v)

parsed, err := strconv.Atoi(v)
if err != nil {
return &FieldConversionError{
FieldName: entry.key,
FieldName: key,
TargetType: "[]int",
Err: err,
}
Expand All @@ -137,25 +131,21 @@ func setIntSliceFieldValue(
slice.Index(i).SetInt(int64(parsed))
}

configFieldValue.Set(slice)

fieldValue.Set(slice)
return nil
}

func setFloatSliceFieldValue(
configFieldValue reflect.Value,
entry entry,
) error {
values := strings.Split(entry.value, ",")
slice := reflect.MakeSlice(configFieldValue.Type(), len(values), len(values))
func setFloatSliceFieldValue(fieldValue reflect.Value, key, value string) error {
values := strings.Split(value, ",")
slice := reflect.MakeSlice(fieldValue.Type(), len(values), len(values))

for i, v := range values {
v = strings.TrimSpace(v)

parsed, err := strconv.ParseFloat(v, 64)
if err != nil {
return &FieldConversionError{
FieldName: entry.key,
FieldName: key,
TargetType: "[]float64",
Err: err,
}
Expand All @@ -164,7 +154,6 @@ func setFloatSliceFieldValue(
slice.Index(i).SetFloat(parsed)
}

configFieldValue.Set(slice)

fieldValue.Set(slice)
return nil
}
171 changes: 78 additions & 93 deletions envconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import (
"strings"
)

type entry struct {
key, value string
}
const envExtension = ".env"

// textReplacementRegex is used to detect text replacement in environment variables.
var textReplacementRegex = regexp.MustCompile(`\${[^}]+}`)

// Set will parse multiple sources for config values, and use these values to populate the passed in config struct.
// Set parses multiple sources for config values and populates the passed config struct.
func Set(config any, opts ...option) error {
s := &settings{
source: map[string]string{},
Expand All @@ -28,22 +26,28 @@ func Set(config any, opts ...option) error {
opt(s)
}

s.sources = append(s.sources, EnvironmentVariableSource{}, FlagSource{})

if s.activeProfile != "" {
if s.filepath == "" {
return fmt.Errorf("assign active profile: %w", &IncompatibleOptionsError{
FirstOption: "WithActiveProfile()",
SecondOption: "WithFilepath()",
Reason: "directory in filepath option must be provided when using active profile",
})
// Process filepaths and create FileSources
for _, f := range s.filepaths {
path := f
if s.activeProfile != "" {
dir, _ := filepath.Split(path)
path = filepath.Join(dir, s.activeProfile+envExtension)
}
s.sources = append(s.sources, FileSource{Filepath: path})
}

dir, _ := filepath.Split(s.filepath)

s.filepath = dir + s.activeProfile + envExtension
if s.activeProfile != "" && len(s.filepaths) == 0 {
return fmt.Errorf("assign active profile: %w", &IncompatibleOptionsError{
FirstOption: "WithActiveProfile()",
SecondOption: "WithFilepath()",
Reason: "directory in filepath option must be provided when using active profile",
})
}

// Add EnvironmentVariableSource and FlagSource at the end
s.sources = append(s.sources, EnvironmentVariableSource{}, FlagSource{})

// Load all sources
for _, source := range s.sources {
values, err := source.Load()
if err != nil {
Expand All @@ -62,118 +66,99 @@ func Set(config any, opts ...option) error {
return nil
}

// populateStruct uses the items in settings.source to populate the passed in config struct.
func (s settings) populateStruct(config any) error {
configStruct := reflect.ValueOf(config)
if configStruct.Kind() != reflect.Pointer || configStruct.Elem().Kind() != reflect.Struct {
// populateStruct populates the config struct using the loaded values.
func (s *settings) populateStruct(config any) error {
configValue := reflect.ValueOf(config)
if configValue.Kind() != reflect.Pointer || configValue.Elem().Kind() != reflect.Struct {
return &InvalidConfigTypeError{ProvidedType: config}
}

configValue := reflect.ValueOf(config).Elem()
// Pass s.prefix to the recursive function so top-level fields respect the prefix option.
return s.populateStructRecursive(configValue.Elem(), s.prefix)
}

for i := range configValue.NumField() {
field := configValue.Type().Field(i)
configFieldValue := configValue.Field(i)
func (s *settings) populateStructRecursive(structValue reflect.Value, prefix string) error {
structType := structValue.Type()

// Ignore fields that are not exported.
if !configFieldValue.CanSet() {
for i := 0; i < structValue.NumField(); i++ {
field := structType.Field(i)
fieldValue := structValue.Field(i)

if !fieldValue.CanSet() {
continue
}

jsonOptionValue, jsonOptionSet := field.Tag.Lookup(tagJSON)
if jsonOptionSet {
err := json.Unmarshal([]byte(s.source[jsonOptionValue]), configFieldValue.Addr().Interface())
if err != nil {
return fmt.Errorf("unmarshal JSON: %w", err)
// Handle JSON tag
if jsonKey, ok := field.Tag.Lookup(tagJSON); ok {
if val, exists := s.source[jsonKey]; exists {
if err := json.Unmarshal([]byte(val), fieldValue.Addr().Interface()); err != nil {
return fmt.Errorf("unmarshal JSON for field %s: %w", field.Name, err)
}
continue
}
continue
}

if err := s.handlePrefixTag(field, configFieldValue, ""); err != nil {
return fmt.Errorf("handle prefix tag: %w", err)
// Handle Prefix tag (Nested Structs)
if prefixTag, ok := field.Tag.Lookup(tagPrefix); ok {
if fieldValue.Kind() == reflect.Struct {
if err := s.populateStructRecursive(fieldValue, prefix+prefixTag); err != nil {
return fmt.Errorf("populate nested struct %s: %w", field.Name, err)
}
continue
}
}

key := field.Tag.Get(tagEnv)
if key == "" {
// Handle Env tag
envKey := field.Tag.Get(tagEnv)
if envKey == "" {
continue
}

value := s.source[key]
fullKey := prefix + envKey
value := s.source[fullKey]

// Handle Default / Required
if value == "" {
if err := checkRequiredTag(key, field); err != nil {
return fmt.Errorf("check required tag: %w", err)
if err := checkRequiredTag(fullKey, field); err != nil {
return err
}

value = field.Tag.Get(tagDefault)
}

value, err := s.resolveReplacement(value)
if err != nil {
return fmt.Errorf("resolve replacement: %w", err)
// Handle Text Replacement
if value != "" {
var err error
value, err = s.resolveReplacement(value)
if err != nil {
return err
}
}

if err := s.setFieldValue(
configFieldValue, entry{key, value}); err != nil {
return fmt.Errorf("set field value: %w", err)
// Set Value
if value != "" {
// Pass fullKey and value directly, no entry struct.
if err := s.setFieldValue(fieldValue, fullKey, value); err != nil {
return fmt.Errorf("set field %s: %w", field.Name, err)
}
}
}

return nil
}

// resolveReplacement checks if a string has the pattern of ${...}, and if so, uses values in settings.source to
// replace the pattern, and returns the newly created string.
func (s settings) resolveReplacement(value string) (string, error) {
// resolveReplacement resolves ${VAR} patterns.
func (s *settings) resolveReplacement(value string) (string, error) {
match := textReplacementRegex.FindStringSubmatch(value)

for _, m := range match {
environmentValue := strings.TrimPrefix(m, "${")
environmentValue = strings.TrimSuffix(environmentValue, "}")
key := strings.TrimSuffix(strings.TrimPrefix(m, "${"), "}")

replacementValue := s.source[environmentValue]
if replacementValue == "" {
return "", &ReplacementError{VariableName: environmentValue}
replacement, ok := s.source[key]
if !ok || replacement == "" {
return "", &ReplacementError{VariableName: key}
}

value = strings.ReplaceAll(value, m, replacementValue)
value = strings.ReplaceAll(value, m, replacement)
}

return value, nil
}

// populateNestedConfig populates a nested struct.
func (s settings) populateNestedConfig(nestedConfig reflect.Value, prefix string) error {
for i := range nestedConfig.NumField() {
field := nestedConfig.Type().Field(i)
configFieldValue := nestedConfig.Field(i)

if !configFieldValue.CanSet() || !configFieldValue.IsZero() {
continue
}

jsonOptionValue, jsonOptionSet := field.Tag.Lookup(tagJSON)
if jsonOptionSet {
err := json.Unmarshal([]byte(s.source[jsonOptionValue]), &configFieldValue)
if err != nil {
return fmt.Errorf("handle JSON tag: %w", err)
}

continue
}

if err := s.handlePrefixTag(field, configFieldValue, prefix); err != nil {
return fmt.Errorf("handle prefix tag: %w", err)
}

environmentVariableKey := prefix + field.Tag.Get(tagEnv)
if environmentVariableKey == prefix { // Ensure tag is set.
continue
}
if err := s.setFieldValue(
configFieldValue, entry{environmentVariableKey, s.source[environmentVariableKey]}); err != nil {
return fmt.Errorf("set field value: %w", err)
}
}

return nil
}
Loading