[agent/codex:gpt-5.4-mini] Update the code against the AX design principles in ~/spec/r... #3
3 changed files with 72 additions and 12 deletions
36
config.go
36
config.go
|
|
@ -1,6 +1,6 @@
|
|||
// Package config provides layered configuration management for the Core framework.
|
||||
//
|
||||
// Configuration values are resolved in priority order: defaults -> file -> env -> flags.
|
||||
// Configuration values are resolved in priority order: defaults -> file -> env -> Set().
|
||||
// Values are stored in a YAML file at ~/.core/config.yaml by default.
|
||||
//
|
||||
// Keys use dot notation for nested access:
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"iter"
|
||||
"maps"
|
||||
"os"
|
||||
|
|
@ -101,24 +102,21 @@ func New(opts ...Option) (*Config, error) {
|
|||
}
|
||||
|
||||
// LoadFile reads a configuration file from the given medium and path and merges it into the current config.
|
||||
// It supports YAML and environment files (.env).
|
||||
// It supports YAML, JSON, TOML, and environment files (.env).
|
||||
func (c *Config) LoadFile(m coreio.Medium, path string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
configType, err := configTypeForPath(path)
|
||||
if err != nil {
|
||||
return coreerr.E("config.LoadFile", "unsupported config file type: "+path, err)
|
||||
}
|
||||
|
||||
content, err := m.Read(path)
|
||||
if err != nil {
|
||||
return coreerr.E("config.LoadFile", "failed to read config file: "+path, err)
|
||||
}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
configType := "yaml"
|
||||
if ext == "" && filepath.Base(path) == ".env" {
|
||||
configType = "env"
|
||||
} else if ext != "" {
|
||||
configType = strings.TrimPrefix(ext, ".")
|
||||
}
|
||||
|
||||
// Load into file-backed viper
|
||||
c.f.SetConfigType(configType)
|
||||
if err := c.f.MergeConfig(strings.NewReader(content)); err != nil {
|
||||
|
|
@ -134,6 +132,24 @@ func (c *Config) LoadFile(m coreio.Medium, path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func configTypeForPath(path string) (string, error) {
|
||||
ext := filepath.Ext(path)
|
||||
switch {
|
||||
case ext == "" && filepath.Base(path) == ".env":
|
||||
return "env", nil
|
||||
case ext == "":
|
||||
return "yaml", nil
|
||||
}
|
||||
|
||||
configType := strings.TrimPrefix(ext, ".")
|
||||
switch configType {
|
||||
case "yaml", "yml", "json", "toml", "env":
|
||||
return configType, nil
|
||||
default:
|
||||
return "", errors.New("unsupported config file type")
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a configuration value by dot-notation key and stores it in out.
|
||||
// If key is empty, it unmarshals the entire configuration into out.
|
||||
// The out parameter must be a pointer to the target type.
|
||||
|
|
|
|||
|
|
@ -246,6 +246,50 @@ func TestConfig_LoadFile_Env(t *testing.T) {
|
|||
assert.Equal(t, "bar", foo)
|
||||
}
|
||||
|
||||
func TestConfig_LoadFile_JSON_Good(t *testing.T) {
|
||||
m := coreio.NewMockMedium()
|
||||
m.Files["/tmp/test/config.json"] = `{"service":{"name":"core"}}`
|
||||
|
||||
cfg, err := New(WithMedium(m), WithPath("/config.yaml"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = cfg.LoadFile(m, "/tmp/test/config.json")
|
||||
assert.NoError(t, err)
|
||||
|
||||
var name string
|
||||
err = cfg.Get("service.name", &name)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "core", name)
|
||||
}
|
||||
|
||||
func TestConfig_LoadFile_TOML_Good(t *testing.T) {
|
||||
m := coreio.NewMockMedium()
|
||||
m.Files["/tmp/test/config.toml"] = "service = { name = \"core\" }\n"
|
||||
|
||||
cfg, err := New(WithMedium(m), WithPath("/config.yaml"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = cfg.LoadFile(m, "/tmp/test/config.toml")
|
||||
assert.NoError(t, err)
|
||||
|
||||
var name string
|
||||
err = cfg.Get("service.name", &name)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "core", name)
|
||||
}
|
||||
|
||||
func TestConfig_LoadFile_Unsupported_Bad(t *testing.T) {
|
||||
m := coreio.NewMockMedium()
|
||||
m.Files["/tmp/test/config.txt"] = "service = core\n"
|
||||
|
||||
cfg, err := New(WithMedium(m), WithPath("/config.yaml"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = cfg.LoadFile(m, "/tmp/test/config.txt")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unsupported config file type")
|
||||
}
|
||||
|
||||
func TestConfig_WithEnvPrefix(t *testing.T) {
|
||||
t.Setenv("MYAPP_SETTING", "secret")
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ description: Layered configuration management for the Core framework with file,
|
|||
|
||||
# config
|
||||
|
||||
`forge.lthn.ai/core/config` provides layered configuration management for applications built on the Core framework. It resolves values through a priority chain -- defaults, file, environment variables, flags -- so that the same codebase works identically across local development, CI, and production without code changes.
|
||||
`forge.lthn.ai/core/config` provides layered configuration management for applications built on the Core framework. It resolves values through a priority chain -- defaults, file, environment variables, and explicit `Set()` calls -- so that the same codebase works identically across local development, CI, and production without code changes.
|
||||
|
||||
## Module Path
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ Values are resolved in ascending priority order:
|
|||
1. **Defaults** -- hardcoded fallbacks (via `Set()` before any file load)
|
||||
2. **File** -- YAML loaded from `~/.core/config.yaml` (or a custom path)
|
||||
3. **Environment variables** -- prefixed with `CORE_CONFIG_` by default
|
||||
4. **Explicit Set()** -- in-memory overrides applied at runtime
|
||||
4. **Explicit `Set()`** -- in-memory overrides applied at runtime
|
||||
|
||||
Environment variables always override file values. An explicit `Set()` call overrides everything.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue