feat: unify generated configuration loading and extensible decoders
This commit is contained in:
21
examples/configuration/README.md
Normal file
21
examples/configuration/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 统一配置加载示例
|
||||
|
||||
从框架根目录运行,无需数据库或外部服务:
|
||||
|
||||
```sh
|
||||
go run ./examples/configuration
|
||||
go run ./examples/configuration -format json
|
||||
go run ./examples/configuration -format xml
|
||||
DEMO_CLIENT_TIMEOUT=500ms DEMO_MAX_ITEMS=0 go run ./examples/configuration
|
||||
DEMO_MAX_ITEMS=wrong go run ./examples/configuration
|
||||
```
|
||||
|
||||
前三次都输出 `timeout=2s maxItems=100 allowed=[json xml]`。环境覆盖允许显式零值。
|
||||
最后一条输出定位 `maxItems` / `DEMO_MAX_ITEMS` 的安全错误,不打印输入原值。
|
||||
也可 `-config /path/to/app.yaml` 读取自己的文件。示例没有建立网络连接。
|
||||
|
||||
`Config` 定义字段,`envDefault` 定义缺省值,`Validate` 用 `config.Invalid` 返回字段与公开原因。
|
||||
`newLoader` 额外注册 `.custom`(演示复用 JSON decoder);同一个 `load` 可读取所有格式。
|
||||
JSON/YAML/XML 直接可用,无需注册。新增格式只修改解析器注册,不改业务结构或校验。
|
||||
|
||||
只打印选定非敏感字段,不输出整个配置。完整格式、错误定位和旧项目迁移见 [CONFIG.md](../../CONFIG.md)。
|
||||
5
examples/configuration/config.json
Normal file
5
examples/configuration/config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"client": {"endpoint": "https://example.com", "timeout": "2s"},
|
||||
"maxItems": 100,
|
||||
"allowed": ["json", "xml"]
|
||||
}
|
||||
8
examples/configuration/config.xml
Normal file
8
examples/configuration/config.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<config>
|
||||
<client>
|
||||
<endpoint>https://example.com</endpoint>
|
||||
<timeout>2s</timeout>
|
||||
</client>
|
||||
<maxItems>100</maxItems>
|
||||
<allowed type="array"><item>json</item><item>xml</item></allowed>
|
||||
</config>
|
||||
5
examples/configuration/config.yaml
Normal file
5
examples/configuration/config.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
client:
|
||||
endpoint: https://example.com
|
||||
timeout: 2s
|
||||
maxItems: 100
|
||||
allowed: [json, xml]
|
||||
85
examples/configuration/main.go
Normal file
85
examples/configuration/main.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Run with go run ./examples/configuration -format xml, or -config /path/to/app.yaml.
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/config"
|
||||
)
|
||||
|
||||
//go:embed config.json config.yaml config.xml
|
||||
var examples embed.FS
|
||||
|
||||
type ClientConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Timeout time.Duration `json:"timeout" envDefault:"3s"`
|
||||
// This field is deliberately unavailable to configuration files.
|
||||
Token string `json:"-" env:"DEMO_CLIENT_TOKEN,secret"`
|
||||
}
|
||||
type Config struct {
|
||||
Client ClientConfig `json:"client"`
|
||||
MaxItems int `json:"maxItems" envDefault:"20"`
|
||||
Allowed []string `json:"allowed"`
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.Client.Timeout <= 0 {
|
||||
return config.Invalid("client.timeout", "must be positive")
|
||||
}
|
||||
if c.MaxItems < 0 {
|
||||
return config.Invalid("maxItems", "must not be negative")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func newLoader() (*config.Loader, error) {
|
||||
// Example of an instance-local extension. A TOML/etc. adapter can implement
|
||||
// Decoder and return Document using that parser instead; no global registry.
|
||||
return config.NewLoader(config.WithDecoder("custom", config.DecoderFunc(func(data []byte) (config.Document, error) {
|
||||
return (config.JSONDecoder{}).Decode(data)
|
||||
})))
|
||||
}
|
||||
func load(path string) (Config, error) {
|
||||
loader, err := newLoader()
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return config.LoadProject(path, Config{}, config.EnvOptions{Prefix: "DEMO_", Empty: "reject"}, loader)
|
||||
}
|
||||
func run() error {
|
||||
path := flag.String("config", "", "external configuration file")
|
||||
format := flag.String("format", "yaml", "embedded example: json, yaml, xml")
|
||||
flag.Parse()
|
||||
if *path == "" {
|
||||
data, err := examples.ReadFile("config." + *format)
|
||||
if err != nil {
|
||||
return fmt.Errorf("choose json, yaml or xml")
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "goOmniConfiguration-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
*path = filepath.Join(dir, "config."+*format)
|
||||
if err = os.WriteFile(*path, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cfg, err := load(*path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Print selected non-sensitive fields, never the whole configuration.
|
||||
fmt.Printf("timeout=%s maxItems=%d allowed=%v\n", cfg.Client.Timeout, cfg.MaxItems, cfg.Allowed)
|
||||
return nil
|
||||
}
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
34
examples/configuration/main_test.go
Normal file
34
examples/configuration/main_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExamples(t *testing.T) {
|
||||
var previous Config
|
||||
for i, ext := range []string{"json", "yaml", "xml", "custom"} {
|
||||
source := ext
|
||||
if source == "custom" {
|
||||
source = "json"
|
||||
}
|
||||
data, err := examples.ReadFile("config." + source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "app."+ext)
|
||||
if err = os.WriteFile(path, data, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := load(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if i > 0 && !reflect.DeepEqual(previous, got) {
|
||||
t.Fatal("format difference")
|
||||
}
|
||||
previous = got
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user