- LoadWithSources: reflection snapshots at layer boundaries (default -> env-file per file -> process-env incl. DSN/CORS/KB normalization); diff yields exact per-field source - no manual key registry, no drift - Load unchanged signature/behavior (wrapper); ApplyCLIOverlay records the cli layer on a cloned config - 'config explain' upgraded to field table (59 fields; source + set flag only, values never printed per KTD17) - replaces the reporting-only key registry from PR #12 - tests: layer attribution (file/process override), cli overlay, Load regression
103 lines
2.5 KiB
Go
103 lines
2.5 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// FieldSource 是单个配置字段的来源解释(KTD17:只报告来源/状态,不打印值)。
|
|
type FieldSource struct {
|
|
Field string // json 标签名(无标签时为小写字段名)
|
|
Layer string // default | env-file:<path> | process-env | cli
|
|
Set bool // 是否被显式设置(非默认值即视为被设置)
|
|
}
|
|
|
|
// snapshotFields 用反射导出 Config 的全部字段值(格式化字符串)。
|
|
// 字段均为基本类型/时间片,无指针与嵌套结构;新增字段自动纳入。
|
|
func snapshotFields(c *Config) map[string]string {
|
|
out := map[string]string{}
|
|
v := reflect.ValueOf(*c)
|
|
t := v.Type()
|
|
for i := 0; i < t.NumField(); i++ {
|
|
f := t.Field(i)
|
|
if f.PkgPath != "" {
|
|
continue // 未导出
|
|
}
|
|
name := f.Tag.Get("json")
|
|
if idx := len(name); idx > 0 {
|
|
// 去 omitempty 等选项
|
|
for j := 0; j < len(name); j++ {
|
|
if name[j] == ',' {
|
|
name = name[:j]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if name == "" {
|
|
name = lowerFirst(f.Name)
|
|
}
|
|
fv := v.Field(i)
|
|
var s string
|
|
if fv.Type() == reflect.TypeOf(time.Duration(0)) {
|
|
s = fv.Interface().(time.Duration).String()
|
|
} else {
|
|
s = fmt.Sprintf("%v", fv.Interface())
|
|
}
|
|
out[name] = s
|
|
}
|
|
return out
|
|
}
|
|
|
|
func lowerFirst(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
b := []byte(s)
|
|
if b[0] >= 'A' && b[0] <= 'Z' {
|
|
b[0] += 'a' - 'A'
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// diffLayers 依序比较各阶段快照,返回每个字段的最后变更层(即生效来源)。
|
|
// phases 按应用顺序给出(层名, 快照);字段未出现于任何快照时归为 default。
|
|
func diffLayers(phases []struct {
|
|
Layer string
|
|
Snap map[string]string
|
|
}) map[string]FieldSource {
|
|
src := map[string]FieldSource{}
|
|
var prev map[string]string
|
|
for _, ph := range phases {
|
|
for field, val := range ph.Snap {
|
|
fs := src[field]
|
|
fs.Field = field
|
|
if prev == nil || prev[field] != val {
|
|
fs.Layer = ph.Layer
|
|
fs.Set = prev == nil || prev[field] != val && ph.Layer != phases[0].Layer
|
|
}
|
|
src[field] = fs
|
|
}
|
|
prev = ph.Snap
|
|
}
|
|
// default 层不算"显式设置"
|
|
for field, fs := range src {
|
|
if fs.Layer == phases[0].Layer {
|
|
fs.Set = false
|
|
src[field] = fs
|
|
}
|
|
}
|
|
return src
|
|
}
|
|
|
|
// SortedFieldSources 排序输出(稳定渲染用)。
|
|
func SortedFieldSources(m map[string]FieldSource) []FieldSource {
|
|
out := make([]FieldSource, 0, len(m))
|
|
for _, fs := range m {
|
|
out = append(out, fs)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Field < out[j].Field })
|
|
return out
|
|
}
|