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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"generatorConfig": true,
|
||||
"templateVersion": "v0.19.0-preview",
|
||||
"templateVersion": "v0.20.0-preview",
|
||||
"profile": {
|
||||
"database": "none",
|
||||
"storage": "none",
|
||||
@@ -12,6 +12,7 @@
|
||||
"entry": "api/managed.api",
|
||||
"generated": {
|
||||
"docs/openapi.json": "0370fd6bd6a387e2ef21f6b42b009ce66749846b8bea3d481009a34f3af039a5",
|
||||
"internal/config/loadGen.go": "5d630b894abdbbd66ddfa7d7d30f7573fc4ff995020a32a9f9157cd4af3a5655",
|
||||
"internal/handler/appGen.go": "f5251b4dc33e2312b0bdb71ead70d2171977d851265e124f640458d705a2cc9d",
|
||||
"internal/handler/demo/handlersGen.go": "3c76d85dbe89eb5127d9d134dc625203ecf9719dd11d174c1f335691b99e1375",
|
||||
"internal/handler/openapiGen.go": "f24cab3abbb8557a3fc4dd93fba23cc0b093f3e0c944d3ba1bd5b0eeff2d96f3",
|
||||
|
||||
@@ -2,7 +2,7 @@ module example.com/managed
|
||||
|
||||
go 1.22.0
|
||||
|
||||
require git.yuntuo.hk/k2safe/go-omni v0.19.0-preview
|
||||
require git.yuntuo.hk/k2safe/go-omni v0.20.0-preview
|
||||
|
||||
require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"git.yuntuo.hk/k2safe/go-omni/config"
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/config"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -22,42 +21,10 @@ func Default() Config {
|
||||
return Config{Server: server}
|
||||
}
|
||||
|
||||
// Load applies defaults -> file -> typed environment -> validation.
|
||||
// New fields automatically use GO_OMNI_<JSON_PATH>, or an explicit env tag.
|
||||
func Load(path string) (Config, error) {
|
||||
cfg := Default()
|
||||
var err error
|
||||
cfg, err = config.ApplyDefaults(cfg)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if path != "" {
|
||||
cfg, err = config.DecodeFile(path, cfg)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
}
|
||||
cfg, err = config.OverlayEnv(cfg, config.EnvOptions{Prefix: "GO_OMNI_", Empty: "ignore", Aliases: config.ServerEnvAliases("GO_OMNI_", "server")})
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
output, outputSet := os.LookupEnv("GO_OMNI_SERVER_LOG_OUTPUT")
|
||||
if !outputSet {
|
||||
output = os.Getenv("GO_OMNI_LOG_OUTPUT")
|
||||
}
|
||||
file, fileSet := os.LookupEnv("GO_OMNI_SERVER_LOG_FILE")
|
||||
if !fileSet {
|
||||
file = os.Getenv("GO_OMNI_LOG_FILE")
|
||||
}
|
||||
if output != "" && file == "" {
|
||||
cfg.Server.Log.File = ""
|
||||
cfg.Server.Log.Rotation = logx.RotationConfig{}
|
||||
}
|
||||
return cfg, cfg.Validate()
|
||||
}
|
||||
// Validate checks configuration before logs or dependencies open.
|
||||
func (c Config) Validate() error {
|
||||
if err := c.Server.Validate(); err != nil {
|
||||
return err
|
||||
return config.AtField("server", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
16
examples/managedService/internal/config/loadGen.go
Normal file
16
examples/managedService/internal/config/loadGen.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Code generated by go-omni api. DO NOT EDIT.
|
||||
package config
|
||||
|
||||
import omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
|
||||
|
||||
// Load is maintained by the generator. Extend Config/Default/Validate in
|
||||
// config.go, or NewLoader in loader.go; do not duplicate the loading pipeline.
|
||||
func Load(path string) (Config, error) {
|
||||
loader, err := NewLoader()
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return omniConfig.LoadProject(path, Default(), omniConfig.EnvOptions{
|
||||
Prefix: "GO_OMNI_", Empty: "ignore", Aliases: omniConfig.ServerEnvAliases("GO_OMNI_", "server"),
|
||||
}, loader)
|
||||
}
|
||||
7
examples/managedService/internal/config/loader.go
Normal file
7
examples/managedService/internal/config/loader.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
import omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
|
||||
|
||||
// NewLoader is project-owned. JSON/YAML/XML work without changes. Register
|
||||
// additional format decoders here only when the project needs them.
|
||||
func NewLoader() (*omniConfig.Loader, error) { return omniConfig.NewLoader() }
|
||||
Reference in New Issue
Block a user