Skip to content

Commit 3ec4b5c

Browse files
committed
feat(os/gcfg): add GetEffective method with standard config priority
Add GetEffective and MustGetEffective methods that follow 12-Factor App methodology for configuration priority: Command line > Environment variables > Config file > Default value This is the standard behavior in frameworks like Spring Boot and Viper, where higher priority sources can override lower priority ones. Key changes: - Add GetEffective/MustGetEffective with standard priority - Use ContainsOpt to properly handle empty string from command line - Add clarifying notes to GetWithEnv/GetWithCmd about their priority - Add comprehensive unit tests Closes gogf#4650
1 parent 6a3ea89 commit 3ec4b5c

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

os/gcfg/gcfg.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ func (c *Config) Get(ctx context.Context, pattern string, def ...any) (*gvar.Var
118118
// It returns the default value `def` if none of them exists.
119119
//
120120
// Fetching Rules: Environment arguments are in uppercase format, eg: GF_PACKAGE_VARIABLE.
121+
//
122+
// Note: This method uses configuration file as the primary source, with environment variable
123+
// as fallback only when config value is not found. If you need standard priority where
124+
// environment variables can override config file values, use GetEffective instead.
121125
func (c *Config) GetWithEnv(ctx context.Context, pattern string, def ...any) (*gvar.Var, error) {
122126
value, err := c.Get(ctx, pattern)
123127
if err != nil && gerror.Code(err) != gcode.CodeNotFound {
@@ -140,6 +144,10 @@ func (c *Config) GetWithEnv(ctx context.Context, pattern string, def ...any) (*g
140144
// It returns the default value `def` if none of them exists.
141145
//
142146
// Fetching Rules: Command line arguments are in lowercase format, eg: gf.package.variable.
147+
//
148+
// Note: This method uses configuration file as the primary source, with command line argument
149+
// as fallback only when config value is not found. If you need standard priority where
150+
// command line arguments can override config file values, use GetEffective instead.
143151
func (c *Config) GetWithCmd(ctx context.Context, pattern string, def ...any) (*gvar.Var, error) {
144152
value, err := c.Get(ctx, pattern)
145153
if err != nil && gerror.Code(err) != gcode.CodeNotFound {
@@ -157,6 +165,48 @@ func (c *Config) GetWithCmd(ctx context.Context, pattern string, def ...any) (*g
157165
return value, nil
158166
}
159167

168+
// GetEffective returns the configuration value with standard priority (highest to lowest):
169+
//
170+
// Command line arguments > Environment variables > Configuration file > Default value
171+
//
172+
// This follows the 12-Factor App methodology where higher priority sources can override
173+
// lower priority ones, allowing runtime configuration without modifying config files.
174+
//
175+
// Key format conversion:
176+
// - Command line: lowercase with dots, eg: gf.package.variable (--gf.package.variable=value)
177+
// - Environment: uppercase with underscores, eg: GF_PACKAGE_VARIABLE
178+
//
179+
// Unlike GetWithEnv/GetWithCmd which use config file as primary source, this method
180+
// treats command line and environment variables as overrides, which is the standard
181+
// behavior in frameworks like Spring Boot and Viper.
182+
func (c *Config) GetEffective(ctx context.Context, pattern string, def ...any) (*gvar.Var, error) {
183+
// 1. Command line arguments (highest priority)
184+
cmdKey := utils.FormatCmdKey(pattern)
185+
if command.ContainsOpt(cmdKey) {
186+
return gvar.New(command.GetOpt(cmdKey)), nil
187+
}
188+
189+
// 2. Environment variables
190+
if v := genv.Get(utils.FormatEnvKey(pattern)); v != nil {
191+
return v, nil
192+
}
193+
194+
// 3. Configuration file
195+
value, err := c.Get(ctx, pattern)
196+
if err != nil && gerror.Code(err) != gcode.CodeNotFound {
197+
return nil, err
198+
}
199+
if value != nil {
200+
return value, nil
201+
}
202+
203+
// 4. Default value
204+
if len(def) > 0 {
205+
return gvar.New(def[0]), nil
206+
}
207+
return nil, nil
208+
}
209+
160210
// Data retrieves and returns all configuration data as map type.
161211
func (c *Config) Data(ctx context.Context) (data map[string]any, err error) {
162212
return c.adapter.Data(ctx)
@@ -192,6 +242,15 @@ func (c *Config) MustGetWithCmd(ctx context.Context, pattern string, def ...any)
192242
return v
193243
}
194244

245+
// MustGetEffective acts as function GetEffective, but it panics if error occurs.
246+
func (c *Config) MustGetEffective(ctx context.Context, pattern string, def ...any) *gvar.Var {
247+
v, err := c.GetEffective(ctx, pattern, def...)
248+
if err != nil {
249+
panic(err)
250+
}
251+
return v
252+
}
253+
195254
// MustData acts as function Data, but it panics if error occurs.
196255
func (c *Config) MustData(ctx context.Context) map[string]any {
197256
v, err := c.Data(ctx)

os/gcfg/gcfg_z_unit_basic_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,49 @@ array = [1,2,3]
226226
t.Assert(c.MustGetWithCmd(ctx, `redis.user`), `2`)
227227
})
228228
}
229+
230+
func Test_GetEffective(t *testing.T) {
231+
content := `
232+
v1 = 1
233+
v2 = "true"
234+
[server]
235+
port = 8080
236+
host = "localhost"
237+
[redis]
238+
disk = "127.0.0.1:6379,0"
239+
cache = "127.0.0.1:6379,1"
240+
`
241+
gtest.C(t, func(t *gtest.T) {
242+
c, err := gcfg.New()
243+
t.AssertNil(err)
244+
c.GetAdapter().(*gcfg.AdapterFile).SetContent(content)
245+
defer c.GetAdapter().(*gcfg.AdapterFile).ClearContent()
246+
247+
// Test 1: Get from config file when no cmd/env set
248+
t.Assert(c.MustGetEffective(ctx, "server.port"), 8080)
249+
t.Assert(c.MustGetEffective(ctx, "server.host"), "localhost")
250+
251+
// Test 2: Environment variable overrides config file
252+
t.Assert(genv.Set("SERVER_PORT", "9090"), nil)
253+
defer genv.Remove("SERVER_PORT")
254+
t.Assert(c.MustGetEffective(ctx, "server.port"), "9090")
255+
256+
// Test 3: Command line overrides environment variable
257+
gcmd.Init([]string{"gf", "--server.port=7070"}...)
258+
t.Assert(c.MustGetEffective(ctx, "server.port"), "7070")
259+
260+
// Test 4: Default value when nothing is set
261+
t.Assert(c.MustGetEffective(ctx, "server.timeout", 30), 30)
262+
263+
// Test 5: Empty string from command line should override
264+
gcmd.Init([]string{"gf", "--server.name="}...)
265+
t.Assert(genv.Set("SERVER_NAME", "from-env"), nil)
266+
defer genv.Remove("SERVER_NAME")
267+
t.Assert(c.MustGetEffective(ctx, "server.name"), "")
268+
269+
// Test 6: Key not in config, only in env
270+
t.Assert(genv.Set("APP_DEBUG", "true"), nil)
271+
defer genv.Remove("APP_DEBUG")
272+
t.Assert(c.MustGetEffective(ctx, "app.debug"), "true")
273+
})
274+
}

0 commit comments

Comments
 (0)