mirror of
https://gitee.com/johng/gf
synced 2026-06-06 16:21:40 +08:00
feat(os/gcfg): add GetEffective method with standard config priority (#4673)
## Summary - Add `GetEffective` and `MustGetEffective` methods following 12-Factor App config priority - Priority: Command line > Environment variables > Config file > Default value - Add clarifying notes to existing `GetWithEnv`/`GetWithCmd` methods - Add comprehensive unit tests ## Test plan - [x] All gcfg unit tests pass (44 tests) - [x] New `Test_GetEffective` covers 6 scenarios: - Config file only - Env overrides config - Cmd overrides env - Default value fallback - Empty string override (industry standard) - Key only in env Closes #4650 --------- Co-authored-by: John Guo <claymore1986@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@ -118,6 +118,10 @@ func (c *Config) Get(ctx context.Context, pattern string, def ...any) (*gvar.Var
|
||||
// It returns the default value `def` if none of them exists.
|
||||
//
|
||||
// Fetching Rules: Environment arguments are in uppercase format, eg: GF_PACKAGE_VARIABLE.
|
||||
//
|
||||
// Note: This method uses the configuration (adapter) as the primary source, with environment
|
||||
// variable as fallback only when the configuration value is not found. If you need standard
|
||||
// priority where environment variables can override configuration values, use GetEffective instead.
|
||||
func (c *Config) GetWithEnv(ctx context.Context, pattern string, def ...any) (*gvar.Var, error) {
|
||||
value, err := c.Get(ctx, pattern)
|
||||
if err != nil && gerror.Code(err) != gcode.CodeNotFound {
|
||||
@ -140,6 +144,10 @@ func (c *Config) GetWithEnv(ctx context.Context, pattern string, def ...any) (*g
|
||||
// It returns the default value `def` if none of them exists.
|
||||
//
|
||||
// Fetching Rules: Command line arguments are in lowercase format, eg: gf.package.variable.
|
||||
//
|
||||
// Note: This method uses configuration file as the primary source, with command line argument
|
||||
// as fallback only when config value is not found. If you need standard priority where
|
||||
// command line arguments can override config file values, use GetEffective instead.
|
||||
func (c *Config) GetWithCmd(ctx context.Context, pattern string, def ...any) (*gvar.Var, error) {
|
||||
value, err := c.Get(ctx, pattern)
|
||||
if err != nil && gerror.Code(err) != gcode.CodeNotFound {
|
||||
@ -157,6 +165,48 @@ func (c *Config) GetWithCmd(ctx context.Context, pattern string, def ...any) (*g
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetEffective returns the configuration value with standard priority (highest to lowest):
|
||||
//
|
||||
// Command line arguments > Environment variables > Configuration file > Default value
|
||||
//
|
||||
// This follows the 12-Factor App methodology where higher priority sources can override
|
||||
// lower priority ones, allowing runtime configuration without modifying config files.
|
||||
//
|
||||
// Key format conversion:
|
||||
// - Command line: lowercase with dots, eg: gf.package.variable (--gf.package.variable=value)
|
||||
// - Environment: uppercase with underscores, eg: GF_PACKAGE_VARIABLE
|
||||
//
|
||||
// Unlike GetWithEnv/GetWithCmd which use config file as primary source, this method
|
||||
// treats command line and environment variables as overrides, which is the standard
|
||||
// behavior in frameworks like Spring Boot and Viper.
|
||||
func (c *Config) GetEffective(ctx context.Context, pattern string, def ...any) (*gvar.Var, error) {
|
||||
// 1. Command line arguments (highest priority)
|
||||
cmdKey := utils.FormatCmdKey(pattern)
|
||||
if command.ContainsOpt(cmdKey) {
|
||||
return gvar.New(command.GetOpt(cmdKey)), nil
|
||||
}
|
||||
|
||||
// 2. Environment variables
|
||||
if v := genv.Get(utils.FormatEnvKey(pattern)); v != nil {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// 3. Configuration file
|
||||
value, err := c.Get(ctx, pattern)
|
||||
if err != nil && gerror.Code(err) != gcode.CodeNotFound {
|
||||
return nil, err
|
||||
}
|
||||
if value != nil {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// 4. Default value
|
||||
if len(def) > 0 {
|
||||
return gvar.New(def[0]), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Data retrieves and returns all configuration data as map type.
|
||||
func (c *Config) Data(ctx context.Context) (data map[string]any, err error) {
|
||||
return c.adapter.Data(ctx)
|
||||
@ -192,6 +242,15 @@ func (c *Config) MustGetWithCmd(ctx context.Context, pattern string, def ...any)
|
||||
return v
|
||||
}
|
||||
|
||||
// MustGetEffective acts as function GetEffective, but it panics if error occurs.
|
||||
func (c *Config) MustGetEffective(ctx context.Context, pattern string, def ...any) *gvar.Var {
|
||||
v, err := c.GetEffective(ctx, pattern, def...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// MustData acts as function Data, but it panics if error occurs.
|
||||
func (c *Config) MustData(ctx context.Context) map[string]any {
|
||||
v, err := c.Data(ctx)
|
||||
|
||||
@ -226,3 +226,49 @@ array = [1,2,3]
|
||||
t.Assert(c.MustGetWithCmd(ctx, `redis.user`), `2`)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_GetEffective(t *testing.T) {
|
||||
content := `
|
||||
v1 = 1
|
||||
v2 = "true"
|
||||
[server]
|
||||
port = 8080
|
||||
host = "localhost"
|
||||
[redis]
|
||||
disk = "127.0.0.1:6379,0"
|
||||
cache = "127.0.0.1:6379,1"
|
||||
`
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
c, err := gcfg.New()
|
||||
t.AssertNil(err)
|
||||
c.GetAdapter().(*gcfg.AdapterFile).SetContent(content)
|
||||
defer c.GetAdapter().(*gcfg.AdapterFile).ClearContent()
|
||||
|
||||
// Test 1: Get from config file when no cmd/env set
|
||||
t.Assert(c.MustGetEffective(ctx, "server.port"), 8080)
|
||||
t.Assert(c.MustGetEffective(ctx, "server.host"), "localhost")
|
||||
|
||||
// Test 2: Environment variable overrides config file
|
||||
t.Assert(genv.Set("SERVER_PORT", "9090"), nil)
|
||||
defer genv.Remove("SERVER_PORT")
|
||||
t.Assert(c.MustGetEffective(ctx, "server.port"), "9090")
|
||||
|
||||
// Test 3: Command line overrides environment variable
|
||||
gcmd.Init([]string{"gf", "--server.port=7070"}...)
|
||||
t.Assert(c.MustGetEffective(ctx, "server.port"), "7070")
|
||||
|
||||
// Test 4: Default value when nothing is set
|
||||
t.Assert(c.MustGetEffective(ctx, "server.timeout", 30), 30)
|
||||
|
||||
// Test 5: Empty string from command line should override
|
||||
gcmd.Init([]string{"gf", "--server.name="}...)
|
||||
t.Assert(genv.Set("SERVER_NAME", "from-env"), nil)
|
||||
defer genv.Remove("SERVER_NAME")
|
||||
t.Assert(c.MustGetEffective(ctx, "server.name"), "")
|
||||
|
||||
// Test 6: Key not in config, only in env
|
||||
t.Assert(genv.Set("APP_DEBUG", "true"), nil)
|
||||
defer genv.Remove("APP_DEBUG")
|
||||
t.Assert(c.MustGetEffective(ctx, "app.debug"), "true")
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user