diff --git a/fig.go b/fig.go index 7632ae4..b3a3b10 100644 --- a/fig.go +++ b/fig.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "time" + "unicode" "github.com/mitchellh/mapstructure" "github.com/pelletier/go-toml/v2" @@ -112,14 +113,15 @@ func defaultFig() *fig { } type fig struct { - filename string - dirs []string - tag string - timeLayout string - useEnv bool - useStrict bool - ignoreFile bool - envPrefix string + filename string + dirs []string + tag string + timeLayout string + useEnv bool + useConstantCase bool + useStrict bool + ignoreFile bool + envPrefix string } func (f *fig) Load(cfg interface{}) error { @@ -312,12 +314,48 @@ func (f *fig) setFromEnv(fv reflect.Value, key string) error { func (f *fig) formatEnvKey(key string) string { // loggers[0].level --> loggers_0_level key = strings.NewReplacer(".", "_", "[", "_", "]", "").Replace(key) + if f.useConstantCase { + key = f.toConstantCase(key) + } if f.envPrefix != "" { key = fmt.Sprintf("%s_%s", f.envPrefix, key) } return strings.ToUpper(key) } +func (f *fig) toConstantCase(key string) string { + var b strings.Builder + runes := []rune(key) + + for i := 0; i < len(runes); i++ { + if !unicode.IsUpper(runes[i]) { + b.WriteRune(runes[i]) + continue + } + + if i == 0 { + b.WriteRune(runes[i]) + continue + } + + if unicode.IsUpper(runes[i-1]) || !unicode.IsLetter(runes[i-1]) { + if len(runes) == i+1 { + b.WriteRune(runes[i]) + } else if unicode.IsLower(runes[i+1]) { + b.WriteString("_") + b.WriteRune(runes[i]) + } else { + b.WriteRune(runes[i]) + } + } else { + b.WriteString("_") + b.WriteRune(runes[i]) + } + } + + return b.String() +} + // setDefaultValue calls setValue but disallows booleans from // being set. func (f *fig) setDefaultValue(fv reflect.Value, val string) error { diff --git a/fig_test.go b/fig_test.go index 36e0573..4db00ea 100644 --- a/fig_test.go +++ b/fig_test.go @@ -1289,6 +1289,35 @@ func Test_fig_setSlice(t *testing.T) { }) } +func Test_fig_toConstantCase(t *testing.T) { + fig := defaultFig() + + testCases := []struct { + key string + expected string + }{ + {key: "fig_serviceName", expected: "fig_service_Name"}, + {key: "fig_USA_test", expected: "fig_USA_test"}, + {key: "fig_runT", expected: "fig_run_T"}, + {key: "fig_0_test", expected: "fig_0_test"}, + {key: "USACountry", expected: "USA_Country"}, + {key: "helloWORLD", expected: "hello_WORLD"}, + {key: "USA", expected: "USA"}, + {key: "___", expected: "___"}, + {key: "a__b__c", expected: "a__b__c"}, + {key: "a__B__c", expected: "a__B__c"}, + } + + for _, tc := range testCases { + t.Run(tc.key, func(t *testing.T) { + res := fig.toConstantCase(tc.key) + if res != tc.expected { + t.Errorf("expected %s, got %s", tc.expected, res) + } + }) + } +} + func setenv(t *testing.T, key, value string) { t.Helper() t.Setenv(key, value) diff --git a/option.go b/option.go index 1fd9ea7..dec7d4b 100644 --- a/option.go +++ b/option.go @@ -118,3 +118,16 @@ func UseStrict() Option { f.useStrict = true } } + +// EnvConstantCaseStrategy returns an option that configures fig to +// convert env key names to constant-case naming convention. +// e.g. converts `app_serverPort` to `APP_SERVER_PORT`. +// +// fig.Load(&cfg, fig.EnvConstantCaseStrategy()) +// +// If this option is not used then fig builds env key names as explained in UseEnv option. +func EnvConstantCaseStrategy() Option { + return func(f *fig) { + f.useConstantCase = true + } +}