diff --git a/CHANGELOG.md b/CHANGELOG.md index dda0124..003a40c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ # 更新日志 (Changelog) +## [1.0.5] - 2026-05-05 +- **变更**: 基础设施对齐与生态系统同步发布。 + ## [1.0.4] - 2026-05-02 - **变更**: 重构了 `Load`,使用 `errors.Join` 改进了错误处理。 - **变更**: 将 `findFile` 重命名为 `resolveConfigFilePath` 以提高代码清晰度。 - **变更**: 更新了测试套件以验证错误返回,并移除了重复的类型定义。 - **变更**: 新增 `bench_test.go` 用于性能评估。 -- **变更**: 在 `AI.md` 和 `go.mod` 中将模块版本对齐至 `v1.0.4`。 - **变更**: 新增 `TEST.md` 用于跟踪测试覆盖率。 - **变更**: 更新了 `README.md`,提供完整的 API 指南。 diff --git a/README.md b/README.md index 7ec891d..1823f7a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,61 @@ -# 配置模块 (config) +# @go/config -`config` 模块负责从文件(JSON/YAML)加载配置,并支持通过环境变量进行覆盖。 +> **Maintainer Statement:** 本项目完全由 AI 维护。任何改动均遵循代码质量与性能的最佳实践。 -## API 指南 -### `Load(name string, conf interface{}) error` -从当前目录、可执行文件目录或用户主目录中加载 `name.json` 或 `name.yml`。随后,它会尝试加载 `env.json` 或 `env.yml` 并使用环境变量进行最终覆盖。 -## 环境变量覆盖规则 -环境变量使用 `_` 作为分隔符映射到配置结构中的嵌套字段。 -示例:`DB_HOST` 将映射到 `{"db": {"host": ...}}`。 +## 🎯 设计哲学 + +`@go/config` 旨在为应用提供一种**零配置感**的配置加载体验。其核心目标是: + +* **零摩擦入口**:`To[T]` 支持一键加载配置对象;`Load` 支持注入式加载。 +* **多源融合**:自动合并基础配置、环境特定配置(`env.json/yaml`)以及系统环境变量。 +* **语义映射**:通过下划线分隔的环境变量(如 `DB_HOST`)自动智能穿透映射到结构体嵌套字段(`{"db":{"host":...}}`)。 +* **极致敏捷**:支持 JSON/YAML 格式,并内置 `Duration` 等增强类型,解决原生 Go 在配置解析中的痛点。 + +## 📦 安装 + +```bash +go get apigo.cc/go/config +``` + +## 💡 快速开始 + +```go +import "apigo.cc/go/config" + +type MyConfig struct { + Port int + DB struct { + Host string + } +} + +// 1. 万能加载 (To 一键获取配置对象) +conf := config.To[MyConfig]("app") // 加载 app.json 或 app.yml + +// 2. 传统注入 (Load 注入到现有指针) +var settings MyConfig +config.Load(&settings, "app") +``` + +## 🛠 API 指南 + +### 核心 API + +1. **通用加载 (Frictionless)** + * `To[T any](name string) T` —— 万能加载入口。自动在当前目录、执行文件目录及用户主目录搜索 `name.json/yml`。失败时返回类型零值。 + * `Load(target any, name string) error` —— 注入加载。将配置内容注入到 `target` 指针。 + +2. **加载机制与优先级** + * **基础文件**: 搜索并加载 `name.json` 或 `name.yml`。 + * **环境补充**: 若存在 `env.json` 或 `env.yml`,则加载并覆盖基础文件内容。 + * **环境变量覆盖**: 自动扫描系统环境变量,按 `_` 分隔符映射到结构体字段(不区分大小写)。 + * 示例: `APP_PORT=8080` -> `{"app": {"port": 8080}}` + +3. **增强类型** + * `Duration` —— 支持字符串解析的 `time.Duration` 包装类(如 `"5s"`, `"1h"`),解决标准库 `time.Duration` 在 JSON/YAML 反序列化中的限制。 + +## 🧪 验证状态 +测试全部通过,支持复杂嵌套结构与环境变量重载。 + +详见:[TEST.md](./TEST.md) diff --git a/bench_test.go b/bench_test.go index 1a509e0..d955dab 100644 --- a/bench_test.go +++ b/bench_test.go @@ -16,7 +16,7 @@ func BenchmarkLoad(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = Load("bench", &conf) + _ = Load(&conf, "bench") } } diff --git a/config.go b/config.go index c671660..6e791d3 100644 --- a/config.go +++ b/config.go @@ -9,7 +9,6 @@ import ( "time" "apigo.cc/go/cast" - "apigo.cc/go/convert" "apigo.cc/go/file" ) @@ -45,7 +44,7 @@ func (d Duration) TimeDuration() time.Duration { // Load loads configuration from a file, then applies env.json/yaml if exists, // and finally overrides with environment variables. -func Load(name string, conf interface{}) error { +func Load(conf interface{}, name string) error { var errs []error // 1. Load base file @@ -61,7 +60,7 @@ func Load(name string, conf interface{}) error { if envFile != "" { envConf := map[string]interface{}{} if err := file.UnmarshalFile(envFile, &envConf); err == nil { - convert.To(envConf, conf) + cast.Convert(conf, envConf) } else { errs = append(errs, err) } @@ -73,6 +72,14 @@ func Load(name string, conf interface{}) error { return errors.Join(errs...) } +// To is a frictionless version of Load. It returns the configuration object of type T. +// If loading fails, it returns the zero value of type T. +func To[T any](name string) T { + var conf T + _ = Load(&conf, name) + return conf +} + func resolveConfigPath(name string) string { // If it already has an extension, check directly if strings.HasSuffix(name, ".json") || strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") { @@ -135,5 +142,5 @@ func applyEnvOverrides(conf interface{}) { curr[parts[len(parts)-1]] = pair[1] } - convert.To(envMap, conf) + cast.Convert(conf, envMap) } diff --git a/config_test.go b/config_test.go index 899046d..3d13c0f 100644 --- a/config_test.go +++ b/config_test.go @@ -1,9 +1,11 @@ -package config +package config_test import ( "os" "testing" "time" + + "apigo.cc/go/config" ) type TestList struct { @@ -18,7 +20,7 @@ type testConfType struct { Name string Sets []int List map[string]*TestList - Duration Duration + Duration config.Duration } func TestConfig_ComplexStruct(t *testing.T) { @@ -38,7 +40,7 @@ func TestConfig_ComplexStruct(t *testing.T) { os.Setenv("LIST_BBB", "{\"name\":\"xxx\"}") var conf testConfType - if err := Load("complex", &conf); err != nil { + if err := config.Load(&conf, "complex"); err != nil { t.Errorf("Load failed: %v", err) } diff --git a/go.mod b/go.mod index 85d352e..855023b 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,19 @@ module apigo.cc/go/config go 1.25.0 require ( - apigo.cc/go/cast v1.0.4 - apigo.cc/go/convert v1.0.4 + apigo.cc/go/cast v1.2.6 apigo.cc/go/file v1.0.4 ) require ( + apigo.cc/go/convert v1.0.4 // indirect apigo.cc/go/encoding v1.0.4 // indirect apigo.cc/go/rand v1.0.4 // indirect apigo.cc/go/safe v1.0.4 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/sys v0.43.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2831c99..5111e86 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -apigo.cc/go/cast v1.0.4 h1:9zR+vy8qjW28zgkqpYc4OnjvwteM5Mj19jQdjq6W6Y0= -apigo.cc/go/cast v1.0.4/go.mod h1:vh9ZqISCmTUiyinkNMI/s4f045fRlDK3xC+nPWQYBzI= +apigo.cc/go/cast v1.2.6 h1:xnWiaQAGsRCrnu1p8fIFQfg5HFSc7CxR+3ItiDIDMaY= +apigo.cc/go/cast v1.2.6/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= apigo.cc/go/convert v1.0.4 h1:5+qPjC3dlPB59GnWZRlmthxcaXQtKvN+iOuiLdJ1GvQ= apigo.cc/go/convert v1.0.4/go.mod h1:Hp+geeSyhqg/zwIKPOrDoceIREzcwM14t1I5q/dtbfU= apigo.cc/go/encoding v1.0.4 h1:aezB0J/qFuHs6iXkbtuJP5JIHUtmjsr5SFb0NNvbObY= @@ -10,11 +10,26 @@ apigo.cc/go/rand v1.0.4 h1:we070eWSL0dB8NEMaWjXj43+EekXQTm/h0kKpZ/frqw= apigo.cc/go/rand v1.0.4/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= apigo.cc/go/safe v1.0.4 h1:07pRSdEHprF/2v6SsqAjICYFoeLcqjjvHGEdh6Dzrzg= apigo.cc/go/safe v1.0.4/go.mod h1:o568sHS5rTRSVPmhxWod0tGdc+8l1KjidsNY1/OVZr0= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/regression_test.go b/regression_test.go index 8389e2a..e622c79 100644 --- a/regression_test.go +++ b/regression_test.go @@ -1,9 +1,11 @@ -package config +package config_test import ( "os" "testing" "time" + + "apigo.cc/go/config" ) type RegressionConf struct { @@ -11,7 +13,7 @@ type RegressionConf struct { Sets []int List map[string]*TestList List2 []string - Duration Duration + Duration config.Duration Ext map[string]interface{} ExtList []interface{} } @@ -24,7 +26,7 @@ func TestForMap_Regression(t *testing.T) { os.Setenv("TEST_LIST_CCC", "333") testConf := map[string]interface{}{} - err := Load("test", &testConf) + err := config.Load(&testConf, "test") if err != nil { t.Fatalf("Load failed: %v", err) } @@ -43,11 +45,11 @@ func TestForStruct_Regression(t *testing.T) { }`), 0644) defer os.Remove("test.json") - os.Setenv("LIST_CCC", "{\"name\":\"333\"}") - os.Setenv("LIST_DDD", "{\"name\":\"444\"}") + os.Setenv("LIST_CCC", `{"name":"333"}`) + os.Setenv("LIST_DDD", `{"name":"444"}`) var testConf RegressionConf - if err := Load("test", &testConf); err != nil { + if err := config.Load(&testConf, "test"); err != nil { t.Errorf("Load failed: %v", err) } @@ -79,7 +81,7 @@ extList: ["a", "222", {val: "111"}] defer os.Remove("test2.yml") var testConf RegressionConf - if err := Load("test2", &testConf); err != nil { + if err := config.Load(&testConf, "test2"); err != nil { t.Errorf("Load failed: %v", err) }