Files
OpenFlare/backend/core/extpoints/config_resolve.go
T
ryan 4653afc554 feat(core): resolve declared configuration with env and file precedence
按 显式 env > autoEnable > 配置文件 > default 的优先级链解析每个已声明
key,支持标量 env 填充切片、duration 与结构体切片解码,非法 env 值不再
静默回退而是报 ErrConfigType。
2026-08-29 09:38:39 +08:00

167 lines
4.2 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package extpoints
import (
"errors"
"fmt"
"reflect"
"sort"
)
// Resolve computes the effective value of every declared key. Priority is, in order:
// an explicit environment override, an auto-enable trigger, the configuration file,
// then the declared default. Resolution is idempotent; later declarations resolve lazily.
func (r *ConfigRegistry) Resolve() error {
r.mu.Lock()
defer r.mu.Unlock()
if r.src == nil {
return ErrConfigNoSource
}
var errs []error
for _, key := range r.order {
if _, done := r.values[key]; done {
continue
}
if err := r.resolveLocked(key); err != nil {
errs = append(errs, err)
}
}
r.resolved = true
return errors.Join(errs...)
}
// Resolved reports whether Resolve has already run.
func (r *ConfigRegistry) Resolved() bool {
r.mu.RLock()
defer r.mu.RUnlock()
return r.resolved
}
// resolveLocked computes the effective value of one key. The caller must hold r.mu.
func (r *ConfigRegistry) resolveLocked(key string) error {
d, ok := r.decls[key]
if !ok {
return fmt.Errorf("%w: %s", ErrConfigUnknownKey, key)
}
if d.env != "" {
if raw, found := r.src.LookupEnv(d.env); found {
value, err := convertValue(raw, d.typ)
if err != nil {
return fmt.Errorf("%w: key %q from environment %s: %w", ErrConfigType, key, d.env, err)
}
r.values[key], r.origins[key] = value, OriginEnv
return nil
}
}
if d.autoEnable != "" && d.typ.Kind() == reflect.Bool {
if _, found := r.src.LookupEnv(d.autoEnable); found {
r.values[key], r.origins[key] = true, OriginAutoEnable
return nil
}
}
if raw, found := r.src.Lookup(key); found {
value, err := convertValue(raw, d.typ)
if err != nil {
return fmt.Errorf("%w: key %q from %s: %w", ErrConfigType, key, r.src.Describe(), err)
}
r.values[key], r.origins[key] = value, OriginFile
return nil
}
if d.def != "" {
value, err := convertValue(d.def, d.typ)
if err != nil {
return fmt.Errorf("%w: default %q for key %q: %w", ErrConfigType, d.def, key, err)
}
r.values[key], r.origins[key] = value, OriginDefault
return nil
}
r.values[key] = reflect.New(d.typ).Elem().Interface()
r.origins[key] = ""
return nil
}
// Bind resolves the tagged fields of target and assigns them in place. Prefixes that
// were never declared self-register, so only gates need DeclareConfig.
func (r *ConfigRegistry) Bind(prefix string, target any) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.src == nil {
return ErrConfigNoSource
}
if !r.resolved {
return fmt.Errorf("%w: Bind(%q, %T) ran before App.Prepare", ErrConfigNotResolved, prefix, target)
}
elem, err := bindingStruct(target, prefix)
if err != nil {
return err
}
fields, err := walkConfigFields(elem.Type(), prefix)
if err != nil {
return err
}
for _, f := range fields {
if _, declared := r.decls[f.key]; !declared {
if err := r.addDecl("bind:"+prefix, f); err != nil {
return err
}
}
if _, done := r.values[f.key]; !done {
if err := r.resolveLocked(f.key); err != nil {
return err
}
}
}
return assignFields(elem, fields, r.values)
}
// assignFields writes resolved values into a freshly walked target struct.
func assignFields(elem reflect.Value, fields []configField, values map[string]any) error {
for _, f := range fields {
field := elem.FieldByName(fieldNameForPath(elem.Type(), f.path))
if !field.IsValid() || !field.CanSet() {
return fmt.Errorf("%w: field for key %q is not settable", ErrConfigTarget, f.key)
}
value := reflect.ValueOf(values[f.key])
if !value.Type().AssignableTo(field.Type()) {
return fmt.Errorf("%w: key %q resolves to %s, field expects %s",
ErrConfigType, f.key, value.Type(), field.Type())
}
field.Set(value)
}
return nil
}
// Entries returns the effective configuration as redacted, key-sorted entries.
func (r *ConfigRegistry) Entries() []ConfigEntry {
r.mu.RLock()
defer r.mu.RUnlock()
keys := append([]string(nil), r.order...)
sort.Strings(keys)
out := make([]ConfigEntry, 0, len(keys))
for _, key := range keys {
d := r.decls[key]
out = append(out, ConfigEntry{
Key: d.key, PluginID: d.pluginID, Env: d.env,
Origin: r.origins[key], Value: "pending",
})
}
return out
}