From 44615a868f72e910705e391b9b71addc554c28b5 Mon Sep 17 00:00:00 2001 From: Mojtaba Arezoomand Date: Sat, 2 Mar 2024 14:44:37 +0330 Subject: [PATCH 1/2] feat: add constant-case strategy in building env key names --- fig.go | 54 +++++++++++++++++++++++++++++++++++++++++++++-------- fig_test.go | 23 +++++++++++++++++++++++ option.go | 13 +++++++++++++ 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/fig.go b/fig.go index 7632ae4..110fccf 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 == len(runes)-1 { + if i-1 >= 0 && unicode.IsLower(runes[i-1]) { + b.WriteString("_") + b.WriteRune(runes[i]) + } else { + b.WriteRune(runes[i]) + } + continue + } + + // Is next letter lower case. + if unicode.IsLower(runes[i+1]) { + b.WriteString("_") + b.WriteRune(runes[i]) + continue + } + + 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..1fd8a76 100644 --- a/fig_test.go +++ b/fig_test.go @@ -1289,6 +1289,29 @@ 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"}, + } + + for i, tc := range testCases { + t.Run(fmt.Sprint(i), 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 + } +} From 9c0b6ef4a3a915a2c2c46af6731177a124a20d54 Mon Sep 17 00:00:00 2001 From: Mojtaba Arezoomand Date: Tue, 5 Mar 2024 02:30:22 +0330 Subject: [PATCH 2/2] fix: use a better algorithm to detect more cases --- fig.go | 20 ++++++++++---------- fig_test.go | 10 ++++++++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/fig.go b/fig.go index 110fccf..b3a3b10 100644 --- a/fig.go +++ b/fig.go @@ -333,24 +333,24 @@ func (f *fig) toConstantCase(key string) string { continue } - if i == len(runes)-1 { - if i-1 >= 0 && unicode.IsLower(runes[i-1]) { + 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]) } - continue - } - - // Is next letter lower case. - if unicode.IsLower(runes[i+1]) { + } else { b.WriteString("_") b.WriteRune(runes[i]) - continue } - - b.WriteRune(runes[i]) } return b.String() diff --git a/fig_test.go b/fig_test.go index 1fd8a76..4db00ea 100644 --- a/fig_test.go +++ b/fig_test.go @@ -1300,10 +1300,16 @@ func Test_fig_toConstantCase(t *testing.T) { {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 i, tc := range testCases { - t.Run(fmt.Sprint(i), func(t *testing.T) { + 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)