156 lines
3.9 KiB
Go
156 lines
3.9 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding"
|
|
"errors"
|
|
"math"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// LoadProject clones defaults, resolves only explicit file references, binds the
|
|
// provided fields and validates. Empty path uses only defaults. No env tags,
|
|
// prefixes, aliases or implicit environment overlays are applied.
|
|
func LoadProject[T any](path string, defaults T, loaders ...*Loader) (T, error) {
|
|
var zero T
|
|
loader, err := selectLoader(loaders)
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
if err = rejectEnvironmentTags(reflect.TypeOf(defaults), map[reflect.Type]bool{}); err != nil {
|
|
return zero, err
|
|
}
|
|
doc := Document{Value: map[string]any{}}
|
|
if path != "" {
|
|
doc, err = loader.read(path)
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
}
|
|
cfg, err := bindDocument(doc, defaults)
|
|
if err != nil {
|
|
return zero, locateError(err, path, doc)
|
|
}
|
|
if valid, ok := any(&cfg).(interface{ Validate() error }); ok {
|
|
if err = valid.Validate(); err != nil {
|
|
return zero, locateError(err, path, doc)
|
|
}
|
|
} else if valid, ok := any(cfg).(interface{ Validate() error }); ok {
|
|
if err = valid.Validate(); err != nil {
|
|
return zero, locateError(err, path, doc)
|
|
}
|
|
}
|
|
return cfg, nil
|
|
}
|
|
func locateError(err error, path string, doc Document) *Error {
|
|
d := validationError(err)
|
|
d.File = path
|
|
if d.Line == 0 {
|
|
p := doc.Positions[d.Field]
|
|
d.Line = p.Line
|
|
d.Column = p.Column
|
|
}
|
|
for field := d.Field; field != ""; {
|
|
if names := doc.environments[field]; len(names) > 0 {
|
|
d.Env = strings.Join(names, ", ")
|
|
break
|
|
}
|
|
i := strings.LastIndexAny(field, ".[")
|
|
if i < 0 {
|
|
break
|
|
}
|
|
field = field[:i]
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Reject removed declarations rather than silently ignoring deployment rules.
|
|
func rejectEnvironmentTags(t reflect.Type, seen map[reflect.Type]bool) error {
|
|
if t == nil || seen[t] {
|
|
return nil
|
|
}
|
|
seen[t] = true
|
|
switch t.Kind() {
|
|
case reflect.Pointer, reflect.Slice, reflect.Array, reflect.Map:
|
|
return rejectEnvironmentTags(t.Elem(), seen)
|
|
case reflect.Struct:
|
|
for i := 0; i < t.NumField(); i++ {
|
|
f := t.Field(i)
|
|
if f.PkgPath != "" {
|
|
continue
|
|
}
|
|
_, env := f.Tag.Lookup("env")
|
|
_, def := f.Tag.Lookup("envDefault")
|
|
if env || def {
|
|
return &Error{Field: f.Name, Reason: "env/envDefault tags were removed; use Default() and explicit file references; see CONFIG.md"}
|
|
}
|
|
if f.Tag.Get("json") != "-" {
|
|
if err := rejectEnvironmentTags(f.Type, seen); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var textType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
|
var durationType = reflect.TypeOf(time.Duration(0))
|
|
|
|
func parseTextValue(v reflect.Value, raw string) (err error) {
|
|
defer func() {
|
|
if recover() != nil {
|
|
err = errors.New("invalid custom value")
|
|
}
|
|
}()
|
|
if v.Kind() == reflect.Pointer {
|
|
v.Set(reflect.New(v.Type().Elem()))
|
|
return parseTextValue(v.Elem(), raw)
|
|
}
|
|
if v.CanAddr() && v.Addr().Type().Implements(textType) {
|
|
return v.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(raw))
|
|
}
|
|
if v.Type() == durationType {
|
|
d, err := time.ParseDuration(raw)
|
|
if err == nil {
|
|
v.SetInt(int64(d))
|
|
}
|
|
return err
|
|
}
|
|
switch v.Kind() {
|
|
case reflect.String:
|
|
v.SetString(raw)
|
|
return nil
|
|
case reflect.Bool:
|
|
x, err := strconv.ParseBool(raw)
|
|
if err == nil {
|
|
v.SetBool(x)
|
|
}
|
|
return err
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
x, err := strconv.ParseInt(raw, 10, v.Type().Bits())
|
|
if err == nil {
|
|
v.SetInt(x)
|
|
}
|
|
return err
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
x, err := strconv.ParseUint(raw, 10, v.Type().Bits())
|
|
if err == nil {
|
|
v.SetUint(x)
|
|
}
|
|
return err
|
|
case reflect.Float32, reflect.Float64:
|
|
x, err := strconv.ParseFloat(raw, v.Type().Bits())
|
|
if err == nil && (math.IsNaN(x) || math.IsInf(x, 0)) {
|
|
return errors.New("nonfinite value")
|
|
}
|
|
if err == nil {
|
|
v.SetFloat(x)
|
|
}
|
|
return err
|
|
}
|
|
return errors.New("expected scalar text")
|
|
}
|