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 }