diff --git a/os/gcfg/gcfg.go b/os/gcfg/gcfg.go index d41f8a187..f72bd3748 100644 --- a/os/gcfg/gcfg.go +++ b/os/gcfg/gcfg.go @@ -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) diff --git a/os/gcfg/gcfg_z_unit_basic_test.go b/os/gcfg/gcfg_z_unit_basic_test.go index cf5ebd26f..bf4d46fd5 100644 --- a/os/gcfg/gcfg_z_unit_basic_test.go +++ b/os/gcfg/gcfg_z_unit_basic_test.go @@ -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") + }) +}