mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
gin was in go.mod for two reflection helpers and zero routing: pkg/http/binding/query.go ginbinding.MapFormWithTag pkg/valuer/valuer.go ginbinding.BindUnmarshaler BindUnmarshaler was one method; the Valuer contract now states it directly. MapFormWithTag is reimplemented in pkg/http/binding/values.go, which is where it belonged: a binding package should not import a web framework to reflect over form values. bindValues is the whole mapper. It carries exactly the semantics 20 bound structs across this repo actually depend on, enumerated field by field: string · bool · int/8/16/32/64 · uint/8/16/32/64 · float32/64 slices · pointers (nil when the key is absent, allocated when it is not) anonymous embedded structs, flattened into the parent namespace `,default=` (4 live sites: format, limit, order, orderBy) `-` to skip, empty tag falls back to the field name Unmarshaler — the one extension point, which is how every valuer enum binds Dropped as dead weight, none of it reachable from any bound struct: map targets, multipart, time_format/time_utc/time_location, collection_format, the `parser` tag, arrays, and the JSON fallback for unrecognised structs. An unsupported kind is now a named error instead of a silent JSON decode. `required:"..."` is openapi metadata — MapFormWithTag never read it and neither does this; a test pins that so nobody re-derives it. Verified against the code being deleted, not asserted: a differential test ran gin's MapFormWithTag and bindValues over all 20 real query structs plus an every-kind fixture, 3520 input cases, comparing decoded structs and error-ness — identical throughout. Two deliberate mutations (the empty-value default rule, the pointer-allocation rule) turned it red, so it bites. That harness is removed here with gin; 82 table cases stand in its place. go mod tidy also drops goccy/go-yaml, ugorji/go/codec, pelletier/go-toml and mongo-driver, which were gin codec plugins and nothing else. Route table unchanged at 367. Build and full suite green. Co-authored-by: Hanzo Dev <[email protected]>
222 lines
5.6 KiB
Go
222 lines
5.6 KiB
Go
package binding
|
|
|
|
import (
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/o11y/pkg/errors"
|
|
)
|
|
|
|
var (
|
|
ErrCodeUnsupportedBindingKind = errors.MustNewCode("unsupported_binding_kind")
|
|
ErrCodeInvalidBindingTarget = errors.MustNewCode("invalid_binding_target")
|
|
)
|
|
|
|
// Unmarshaler is implemented by types that decode themselves from a single raw
|
|
// parameter. It is the one extension point of bindValues: every enum in the
|
|
// codebase reaches it through valuer, so a named type never needs a case here.
|
|
type Unmarshaler interface {
|
|
// UnmarshalParam decodes and assigns a value from a form or query param.
|
|
UnmarshalParam(param string) error
|
|
}
|
|
|
|
var unmarshalerType = reflect.TypeFor[Unmarshaler]()
|
|
|
|
// bindValues maps values onto the fields of ptr, a non-nil pointer to a struct,
|
|
// selecting each field by its `tag` struct tag. A field is left untouched when
|
|
// its key is absent and it declares no `,default=` option, which is what makes
|
|
// a nil pointer field distinguishable from a supplied zero.
|
|
func bindValues(ptr any, values map[string][]string, tag string) error {
|
|
value := reflect.ValueOf(ptr)
|
|
if value.Kind() != reflect.Pointer || value.IsNil() {
|
|
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidBindingTarget, "binding target must be a non-nil pointer, got %T", ptr)
|
|
}
|
|
|
|
_, err := bindValue(value.Elem(), reflect.StructField{}, values, tag)
|
|
return err
|
|
}
|
|
|
|
// bindValue binds one value, reporting whether anything was set. Pointers are
|
|
// allocated only if their element ends up set; anonymous structs are flattened
|
|
// into the parent namespace; every other struct is offered to bindField first
|
|
// so that an Unmarshaler wins over its own fields.
|
|
func bindValue(value reflect.Value, field reflect.StructField, values map[string][]string, tag string) (bool, error) {
|
|
if field.Tag.Get(tag) == "-" {
|
|
return false, nil
|
|
}
|
|
|
|
if value.Kind() == reflect.Pointer {
|
|
pointer, isNew := value, value.IsNil()
|
|
if isNew {
|
|
pointer = reflect.New(value.Type().Elem())
|
|
}
|
|
|
|
set, err := bindValue(pointer.Elem(), field, values, tag)
|
|
if err != nil || !set {
|
|
return false, err
|
|
}
|
|
|
|
if isNew {
|
|
value.Set(pointer)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
if value.Kind() != reflect.Struct || !field.Anonymous {
|
|
set, err := bindField(value, field, values, tag)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if set {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
if value.Kind() == reflect.Struct {
|
|
typ, set := value.Type(), false
|
|
for i := range typ.NumField() {
|
|
structField := typ.Field(i)
|
|
if structField.PkgPath != "" && !structField.Anonymous {
|
|
continue
|
|
}
|
|
|
|
ok, err := bindValue(value.Field(i), structField, values, tag)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
set = set || ok
|
|
}
|
|
|
|
return set, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// bindField resolves the key and default for a single struct field and sets it.
|
|
func bindField(value reflect.Value, field reflect.StructField, values map[string][]string, tag string) (bool, error) {
|
|
key, opts, _ := strings.Cut(field.Tag.Get(tag), ",")
|
|
if key == "" {
|
|
key = field.Name
|
|
}
|
|
if key == "" {
|
|
return false, nil
|
|
}
|
|
|
|
defaultValue, hasDefault := defaultOption(opts)
|
|
|
|
vs, ok := values[key]
|
|
if !ok && !hasDefault {
|
|
return false, nil
|
|
}
|
|
|
|
if value.Kind() == reflect.Slice && !implementsUnmarshaler(value) {
|
|
if len(vs) == 0 {
|
|
if !hasDefault {
|
|
return false, nil
|
|
}
|
|
vs = []string{defaultValue}
|
|
}
|
|
|
|
slice := reflect.MakeSlice(value.Type(), len(vs), len(vs))
|
|
for i, v := range vs {
|
|
if err := setValue(v, slice.Index(i)); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
value.Set(slice)
|
|
|
|
return true, nil
|
|
}
|
|
|
|
raw := defaultValue
|
|
if len(vs) > 0 && vs[0] != "" {
|
|
raw = vs[0]
|
|
}
|
|
|
|
return true, setValue(raw, value)
|
|
}
|
|
|
|
// defaultOption reports the `default=` option of a tag, if any.
|
|
func defaultOption(opts string) (string, bool) {
|
|
for opts != "" {
|
|
var opt string
|
|
opt, opts, _ = strings.Cut(opts, ",")
|
|
|
|
if key, value, _ := strings.Cut(opt, "="); key == "default" {
|
|
return value, true
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// setValue parses raw into value. An empty raw is the zero of the kind rather
|
|
// than a parse error, so `?limit=` behaves like an omitted limit.
|
|
func setValue(raw string, value reflect.Value) error {
|
|
if implementsUnmarshaler(value) {
|
|
return value.Addr().Interface().(Unmarshaler).UnmarshalParam(raw)
|
|
}
|
|
|
|
if value.Kind() != reflect.String {
|
|
raw = strings.TrimSpace(raw)
|
|
}
|
|
|
|
switch value.Kind() {
|
|
case reflect.String:
|
|
value.SetString(raw)
|
|
case reflect.Bool:
|
|
if raw == "" {
|
|
raw = "false"
|
|
}
|
|
parsed, err := strconv.ParseBool(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value.SetBool(parsed)
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
if raw == "" {
|
|
raw = "0"
|
|
}
|
|
parsed, err := strconv.ParseInt(raw, 10, value.Type().Bits())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value.SetInt(parsed)
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
if raw == "" {
|
|
raw = "0"
|
|
}
|
|
parsed, err := strconv.ParseUint(raw, 10, value.Type().Bits())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value.SetUint(parsed)
|
|
case reflect.Float32, reflect.Float64:
|
|
if raw == "" {
|
|
raw = "0"
|
|
}
|
|
parsed, err := strconv.ParseFloat(raw, value.Type().Bits())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value.SetFloat(parsed)
|
|
case reflect.Pointer:
|
|
if value.IsNil() {
|
|
value.Set(reflect.New(value.Type().Elem()))
|
|
}
|
|
return setValue(raw, value.Elem())
|
|
default:
|
|
return errors.Newf(errors.TypeInvalidInput, ErrCodeUnsupportedBindingKind, "cannot bind %q into %s", raw, value.Type())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func implementsUnmarshaler(value reflect.Value) bool {
|
|
return value.CanAddr() && value.Addr().Type().Implements(unmarshalerType)
|
|
}
|