337 lines
9.7 KiB
Go
337 lines
9.7 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
var jsonUnmarshalerType = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()
|
|
|
|
type documentBinder struct {
|
|
doc Document
|
|
nodes int
|
|
}
|
|
|
|
func (b *documentBinder) step(path string, depth int) error {
|
|
b.nodes++
|
|
if depth > maxConfigDepth || b.nodes > maxConfigNodes {
|
|
return b.doc.diagnostic(path, "configuration nesting or node limit exceeded", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func bindDocument[T any](doc Document, defaults T) (T, error) {
|
|
var zero T
|
|
base := reflect.ValueOf(&defaults).Elem()
|
|
if base.Kind() != reflect.Struct && (base.Kind() != reflect.Pointer || base.Type().Elem().Kind() != reflect.Struct) {
|
|
return zero, &Error{Reason: "project configuration must be a struct"}
|
|
}
|
|
result, err := cloneConfiguration(base, 0)
|
|
if err != nil {
|
|
return zero, &Error{Reason: "cannot clone configuration defaults", cause: err}
|
|
}
|
|
b := documentBinder{doc: doc}
|
|
if err = b.bind(doc.Value, result, "", 0, false); err != nil {
|
|
return zero, err
|
|
}
|
|
return result.Interface().(T), nil
|
|
}
|
|
|
|
func (b *documentBinder) decode(v any, dst reflect.Value, path string) (err error) {
|
|
defer func() {
|
|
if recover() != nil {
|
|
err = b.doc.diagnostic(path, "custom value decoder panicked", nil)
|
|
}
|
|
}()
|
|
raw, err := json.Marshal(v)
|
|
if err != nil {
|
|
return b.doc.diagnostic(path, "unsupported configuration value", err)
|
|
}
|
|
if err = json.Unmarshal(raw, dst.Addr().Interface()); err != nil {
|
|
return b.doc.diagnostic(path, "expected a valid "+dst.Type().String(), err)
|
|
}
|
|
return nil
|
|
}
|
|
func (b *documentBinder) bind(v any, dst reflect.Value, path string, depth int, quoted bool) error {
|
|
if err := b.step(path, depth); err != nil {
|
|
return err
|
|
}
|
|
if text, ok := v.(resolvedText); ok {
|
|
if dst.Kind() != reflect.Pointer && dst.Type() != durationType && dst.CanAddr() && !dst.Addr().Type().Implements(jsonUnmarshalerType) && !dst.Addr().Type().Implements(textType) {
|
|
switch dst.Kind() {
|
|
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
|
|
if err := parseTextValue(dst, string(text)); err != nil {
|
|
return b.doc.diagnostic(path, "expected a valid "+dst.Type().String(), err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
if dst.Kind() != reflect.Pointer {
|
|
v = string(text)
|
|
}
|
|
}
|
|
if v == nil {
|
|
return b.decode(nil, dst, path)
|
|
} // Preserve encoding/json null semantics.
|
|
if dst.Kind() == reflect.Pointer {
|
|
if dst.IsNil() {
|
|
dst.Set(reflect.New(dst.Type().Elem()))
|
|
}
|
|
return b.bind(v, dst.Elem(), path, depth+1, quoted)
|
|
}
|
|
if dst.Type() == durationType {
|
|
var raw string
|
|
switch x := v.(type) {
|
|
case string:
|
|
raw = x
|
|
case Text:
|
|
raw = string(x)
|
|
default:
|
|
return b.decode(v, dst, path)
|
|
}
|
|
if err := parseTextValue(dst, strings.TrimSpace(raw)); err != nil {
|
|
return b.doc.diagnostic(path, "expected a duration such as 5s", err)
|
|
}
|
|
return nil
|
|
}
|
|
if dst.CanAddr() && dst.Addr().Type().Implements(jsonUnmarshalerType) {
|
|
normalized, err := b.dynamic(v, path, depth+1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.decode(normalized, dst, path)
|
|
}
|
|
if dst.CanAddr() && dst.Addr().Type().Implements(textType) {
|
|
if text, ok := v.(Text); ok {
|
|
v = string(text)
|
|
}
|
|
return b.decode(v, dst, path)
|
|
}
|
|
if text, ok := v.(Text); ok {
|
|
if dst.Kind() == reflect.String || dst.Kind() == reflect.Interface || (dst.Kind() == reflect.Slice && dst.Type().Elem().Kind() == reflect.Uint8) {
|
|
return b.decode(string(text), dst, path)
|
|
}
|
|
switch dst.Kind() {
|
|
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
|
|
if err := parseTextValue(dst, strings.TrimSpace(string(text))); err != nil {
|
|
return b.doc.diagnostic(path, "expected a valid "+dst.Type().String(), err)
|
|
}
|
|
return nil
|
|
default:
|
|
return b.doc.diagnostic(path, "expected an object or array, not scalar text", nil)
|
|
}
|
|
}
|
|
if quoted {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return b.doc.diagnostic(path, "expected a JSON-encoded string", nil)
|
|
}
|
|
if err := json.Unmarshal([]byte(s), dst.Addr().Interface()); err != nil {
|
|
return b.doc.diagnostic(path, "invalid string-encoded value", err)
|
|
}
|
|
return nil
|
|
}
|
|
switch dst.Kind() {
|
|
case reflect.Struct:
|
|
m, ok := v.(map[string]any)
|
|
if !ok {
|
|
return b.doc.diagnostic(path, "expected an object", nil)
|
|
}
|
|
fields := configurationFields(dst.Type())
|
|
used := map[string]bool{}
|
|
for _, name := range sortedKeys(m) {
|
|
child := joinField(path, name)
|
|
f, ok := fields[name]
|
|
canonical := name
|
|
if !ok {
|
|
for n, field := range fields {
|
|
if strings.EqualFold(n, name) {
|
|
if ok {
|
|
return b.doc.diagnostic(child, "ambiguous field name", nil)
|
|
}
|
|
f, ok, canonical = field, true, n
|
|
}
|
|
}
|
|
}
|
|
if !ok {
|
|
return b.doc.diagnostic(child, unknownFieldReason(name, fields), nil)
|
|
}
|
|
if used[canonical] {
|
|
return b.doc.diagnostic(child, "duplicate field binding", nil)
|
|
}
|
|
used[canonical] = true
|
|
target := dst
|
|
for _, index := range f.Index {
|
|
if target.Kind() == reflect.Pointer {
|
|
if target.IsNil() {
|
|
if !target.CanSet() {
|
|
return b.doc.diagnostic(child, "field cannot be assigned", nil)
|
|
}
|
|
target.Set(reflect.New(target.Type().Elem()))
|
|
}
|
|
target = target.Elem()
|
|
}
|
|
target = target.Field(index)
|
|
}
|
|
if !target.CanSet() {
|
|
return b.doc.diagnostic(child, "field cannot be assigned", nil)
|
|
}
|
|
if err := b.bind(m[name], target, child, depth+1, strings.Contains(","+f.Tag.Get("json")+",", ",string,")); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
case reflect.Map:
|
|
m, ok := v.(map[string]any)
|
|
if !ok || dst.Type().Key().Kind() != reflect.String {
|
|
return b.doc.diagnostic(path, "expected an object with string keys", nil)
|
|
}
|
|
if dst.IsNil() {
|
|
dst.Set(reflect.MakeMap(dst.Type()))
|
|
}
|
|
for _, key := range sortedKeys(m) {
|
|
value := reflect.New(dst.Type().Elem()).Elem()
|
|
if err := b.bind(m[key], value, joinField(path, key), depth+1, false); err != nil {
|
|
return err
|
|
}
|
|
k := reflect.New(dst.Type().Key()).Elem()
|
|
k.SetString(key)
|
|
dst.SetMapIndex(k, value)
|
|
}
|
|
return nil
|
|
case reflect.Slice, reflect.Array:
|
|
if _, ok := v.(string); ok && dst.Kind() == reflect.Slice && dst.Type().Elem().Kind() == reflect.Uint8 {
|
|
return b.decode(v, dst, path)
|
|
}
|
|
items, ok := v.([]any)
|
|
if !ok {
|
|
return b.doc.diagnostic(path, "expected an array", nil)
|
|
}
|
|
if dst.Kind() == reflect.Array && len(items) != dst.Len() {
|
|
return b.doc.diagnostic(path, "array length does not match its declared type", nil)
|
|
}
|
|
if dst.Kind() == reflect.Slice {
|
|
dst.Set(reflect.MakeSlice(dst.Type(), len(items), len(items)))
|
|
}
|
|
for i, value := range items {
|
|
if err := b.bind(value, dst.Index(i), fmt.Sprintf("%s[%d]", path, i), depth+1, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
case reflect.Interface:
|
|
v, err := b.dynamic(v, path, depth+1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if dst.Type().NumMethod() == 0 {
|
|
dst.Set(reflect.ValueOf(v))
|
|
return nil
|
|
}
|
|
return b.decode(v, dst, path)
|
|
default:
|
|
return b.decode(v, dst, path)
|
|
}
|
|
}
|
|
func sortedKeys(m map[string]any) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
func (b *documentBinder) dynamic(v any, path string, depth int) (any, error) {
|
|
if err := b.step(path, depth); err != nil {
|
|
return nil, err
|
|
}
|
|
switch x := v.(type) {
|
|
case Text:
|
|
return string(x), nil
|
|
case resolvedText:
|
|
return string(x), nil
|
|
case map[string]any:
|
|
out := make(map[string]any, len(x))
|
|
for _, k := range sortedKeys(x) {
|
|
c, err := b.dynamic(x[k], joinField(path, k), depth+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out[k] = c
|
|
}
|
|
return out, nil
|
|
case []any:
|
|
out := make([]any, len(x))
|
|
for i, v := range x {
|
|
c, err := b.dynamic(v, fmt.Sprintf("%s[%d]", path, i), depth+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out[i] = c
|
|
}
|
|
return out, nil
|
|
default:
|
|
return v, nil
|
|
}
|
|
}
|
|
|
|
// Match encoding/json's field promotion: the shallowest declaration wins;
|
|
// explicit tags win ties, and ambiguous fields remain unavailable.
|
|
func configurationFields(t reflect.Type) map[string]reflect.StructField {
|
|
type candidate struct {
|
|
field reflect.StructField
|
|
tagged bool
|
|
}
|
|
candidates := map[string][]candidate{}
|
|
var visit func(reflect.Type, []int, map[reflect.Type]bool)
|
|
visit = func(current reflect.Type, prefix []int, visiting map[reflect.Type]bool) {
|
|
if visiting[current] {
|
|
return
|
|
}
|
|
visiting[current] = true
|
|
defer delete(visiting, current)
|
|
for i := 0; i < current.NumField(); i++ {
|
|
f := current.Field(i)
|
|
name := strings.Split(f.Tag.Get("json"), ",")[0]
|
|
if name == "-" {
|
|
continue
|
|
}
|
|
child := f.Type
|
|
if child.Kind() == reflect.Pointer {
|
|
child = child.Elem()
|
|
}
|
|
if f.PkgPath != "" && (!f.Anonymous || child.Kind() != reflect.Struct) {
|
|
continue
|
|
}
|
|
f.Index = append(append([]int(nil), prefix...), i)
|
|
if name == "" && f.Anonymous && child.Kind() == reflect.Struct {
|
|
visit(child, f.Index, visiting)
|
|
continue
|
|
}
|
|
tagged := name != ""
|
|
if name == "" {
|
|
name = f.Name
|
|
}
|
|
candidates[name] = append(candidates[name], candidate{f, tagged})
|
|
}
|
|
}
|
|
visit(t, nil, map[reflect.Type]bool{})
|
|
fields := map[string]reflect.StructField{}
|
|
for name, list := range candidates {
|
|
sort.Slice(list, func(i, j int) bool {
|
|
if len(list[i].field.Index) != len(list[j].field.Index) {
|
|
return len(list[i].field.Index) < len(list[j].field.Index)
|
|
}
|
|
return list[i].tagged && !list[j].tagged
|
|
})
|
|
if len(list) > 1 && len(list[0].field.Index) == len(list[1].field.Index) && list[0].tagged == list[1].tagged {
|
|
continue
|
|
}
|
|
fields[name] = list[0].field
|
|
}
|
|
return fields
|
|
}
|