mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +08:00
feat(core): resolve declared configuration with env and file precedence
按 显式 env > autoEnable > 配置文件 > default 的优先级链解析每个已声明 key,支持标量 env 填充切片、duration 与结构体切片解码,非法 env 值不再 静默回退而是报 ErrConfigType。
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package extpoints
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// convertValue coerces a raw value coming from the configuration file or an
|
||||
// environment variable into the declared Go type.
|
||||
func convertValue(raw any, typ reflect.Type) (any, error) {
|
||||
if typ == durationType {
|
||||
return convertDuration(raw)
|
||||
}
|
||||
|
||||
switch typ.Kind() {
|
||||
case reflect.Bool:
|
||||
return convertBool(raw)
|
||||
case reflect.String:
|
||||
return convertString(raw)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return convertNumeric(raw, typ, signedNumbers)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return convertNumeric(raw, typ, unsignedNumbers)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return convertNumeric(raw, typ, floatingNumbers)
|
||||
case reflect.Slice:
|
||||
return convertSlice(raw, typ)
|
||||
case reflect.Struct:
|
||||
return convertStruct(raw, typ)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s is not a supported configuration type", ErrConfigType, typ)
|
||||
}
|
||||
}
|
||||
|
||||
func convertBool(raw any) (any, error) {
|
||||
switch v := raw.(type) {
|
||||
case bool:
|
||||
return v, nil
|
||||
case string:
|
||||
parsed, err := strconv.ParseBool(strings.TrimSpace(v))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %q is not a boolean", ErrConfigType, v)
|
||||
}
|
||||
return parsed, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %v is not a boolean", ErrConfigType, raw)
|
||||
}
|
||||
}
|
||||
|
||||
func convertString(raw any) (any, error) {
|
||||
switch v := raw.(type) {
|
||||
case string:
|
||||
return v, nil
|
||||
case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return fmt.Sprint(v), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %v is not a string", ErrConfigType, raw)
|
||||
}
|
||||
}
|
||||
|
||||
// numericString extracts the textual form of a value so environment overrides,
|
||||
// which always arrive as strings, share one parsing path with file values.
|
||||
func numericString(raw any) (string, bool) {
|
||||
switch v := raw.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(v), true
|
||||
case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return fmt.Sprint(v), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// numericKind selects which strconv family converts a raw value.
|
||||
type numericKind int
|
||||
|
||||
const (
|
||||
signedNumbers numericKind = iota
|
||||
unsignedNumbers
|
||||
floatingNumbers
|
||||
)
|
||||
|
||||
// convertNumeric parses a raw value into the numeric type declared by typ. The three
|
||||
// numeric families share one implementation because they differ only in the strconv
|
||||
// call and the reflect setter.
|
||||
func convertNumeric(raw any, typ reflect.Type, family numericKind) (any, error) {
|
||||
text, ok := numericString(raw)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %v is not a %s", ErrConfigType, raw, typ)
|
||||
}
|
||||
|
||||
out := reflect.New(typ).Elem()
|
||||
var err error
|
||||
|
||||
switch family {
|
||||
case signedNumbers:
|
||||
parsed, parseErr := strconv.ParseInt(text, 10, typ.Bits())
|
||||
out.SetInt(parsed)
|
||||
err = parseErr
|
||||
case unsignedNumbers:
|
||||
parsed, parseErr := strconv.ParseUint(text, 10, typ.Bits())
|
||||
out.SetUint(parsed)
|
||||
err = parseErr
|
||||
default:
|
||||
parsed, parseErr := strconv.ParseFloat(text, typ.Bits())
|
||||
out.SetFloat(parsed)
|
||||
err = parseErr
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %q is not a valid %s", ErrConfigType, text, typ)
|
||||
}
|
||||
return out.Interface(), nil
|
||||
}
|
||||
|
||||
// convertDuration accepts both Go duration strings such as "200ms" and integer
|
||||
// nanoseconds, mirroring what the previous viper based decoding supported.
|
||||
func convertDuration(raw any) (any, error) {
|
||||
if v, ok := raw.(time.Duration); ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
text, ok := numericString(raw)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %v is not a duration", ErrConfigType, raw)
|
||||
}
|
||||
|
||||
if parsed, err := time.ParseDuration(text); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
nanos, err := strconv.ParseInt(text, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %q is not a valid duration", ErrConfigType, text)
|
||||
}
|
||||
return time.Duration(nanos), nil
|
||||
}
|
||||
|
||||
// convertSlice promotes a scalar into a single-element slice so that a value such as
|
||||
// REDIS_ADDR=redis:6379 can populate the redis.addrs list.
|
||||
func convertSlice(raw any, typ reflect.Type) (any, error) {
|
||||
items, ok := sliceItems(raw)
|
||||
if !ok {
|
||||
items = []any{raw}
|
||||
}
|
||||
|
||||
out := reflect.MakeSlice(typ, 0, len(items))
|
||||
for _, item := range items {
|
||||
converted, err := convertValue(item, typ.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = reflect.Append(out, reflect.ValueOf(converted))
|
||||
}
|
||||
return out.Interface(), nil
|
||||
}
|
||||
|
||||
// sliceItems normalises the several slice shapes a loader may produce.
|
||||
func sliceItems(raw any) ([]any, bool) {
|
||||
switch v := raw.(type) {
|
||||
case []any:
|
||||
return v, true
|
||||
case []string:
|
||||
items := make([]any, len(v))
|
||||
for i, s := range v {
|
||||
items[i] = s
|
||||
}
|
||||
return items, true
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(raw)
|
||||
if rv.IsValid() && rv.Kind() == reflect.Slice {
|
||||
items := make([]any, rv.Len())
|
||||
for i := range items {
|
||||
items[i] = rv.Index(i).Interface()
|
||||
}
|
||||
return items, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func convertStruct(raw any, typ reflect.Type) (any, error) {
|
||||
table, ok := asStringMap(raw)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %v is not a mapping, cannot decode into %s", ErrConfigType, raw, typ)
|
||||
}
|
||||
|
||||
fields, err := walkConfigFields(typ, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := reflect.New(typ).Elem()
|
||||
for _, f := range fields {
|
||||
item, present := table[f.path]
|
||||
if !present || item == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
converted, err := convertValue(item, f.typ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s.%s: %w", ErrConfigType, typ.Name(), f.key, err)
|
||||
}
|
||||
out.FieldByName(fieldNameForPath(typ, f.path)).Set(reflect.ValueOf(converted))
|
||||
}
|
||||
return out.Interface(), nil
|
||||
}
|
||||
|
||||
// asStringMap normalises the two map shapes produced by YAML decoders.
|
||||
func asStringMap(raw any) (map[string]any, bool) {
|
||||
switch v := raw.(type) {
|
||||
case map[string]any:
|
||||
return v, true
|
||||
case map[any]any:
|
||||
out := make(map[string]any, len(v))
|
||||
for key, val := range v {
|
||||
name, ok := key.(string)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
out[name] = val
|
||||
}
|
||||
return out, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// fieldNameForPath maps a declared config path back to the Go struct field carrying it.
|
||||
func fieldNameForPath(t reflect.Type, path string) string {
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if t.Field(i).Tag.Get("config") == path {
|
||||
return t.Field(i).Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user