// codec.go — the form/URL-values half of the two-codec contract (0001 §9:
// In structs must be expressible as both JSON and form values). Field names
// come from the `json` tag so both codecs share one vocabulary; there is no
// separate `form` tag to drift.
package ops
import (
"fmt"
"net/url"
"reflect"
"strconv"
"strings"
)
// DecodeValues fills dst (pointer to struct) from form/query values. Missing
// keys leave the zero value; unknown keys are ignored (HTML forms carry CSRF
// tokens and submit buttons). Repeated keys populate slice fields.
func DecodeValues(values url.Values, dst any) error {
rv := reflect.ValueOf(dst)
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Struct {
return fmt.Errorf("ops: DecodeValues dst must be *struct, got %T", dst)
}
return decodeValues(rv.Elem(), values, true)
}
// OverlayValues sets only the fields these values name, leaving every
// other field exactly as it was.
//
// This exists because the router decodes twice: once from the query or
// body, then again from the routed path so the path is authoritative.
// The second pass used DecodeValues, whose contract is to apply a
// field's declared default when its key is ABSENT — and every field is
// absent from a path that names one wildcard. So a decoded ?limit=200
// was silently reset to its default of 50 on the way through, on every
// op that has both a wildcard route and a defaulted field. Only fields
// carrying a default were affected, which is why ?y=all worked and
// ?limit did not, and why it survived this long.
func OverlayValues(values url.Values, dst any) error {
rv := reflect.ValueOf(dst)
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Struct {
return fmt.Errorf("ops: OverlayValues dst must be *struct, got %T", dst)
}
return decodeValues(rv.Elem(), values, false)
}
func decodeValues(sv reflect.Value, values url.Values, defaults bool) error {
st := sv.Type()
for i := 0; i < st.NumField(); i++ {
f := st.Field(i)
name := fieldName(f)
if name == "" || !f.IsExported() {
continue
}
// A map[string]string field takes the DOTTED keys under its
// name: fields.assignee=alice. It is the one exception to the
// flat-struct rule, and it earns it — a tracker's schema is the
// repository's, so the set of keys is not knowable at compile
// time and cannot be a struct field.
if f.Type.Kind() == reflect.Map {
if err := setMap(sv.Field(i), name, values); err != nil {
return fmt.Errorf("field %q: %w", name, err)
}
continue
}
vals, ok := values[name]
if !ok || len(vals) == 0 {
if defaults {
// Absent keys take the field's declared default (if any) —
// which is how an explicit 0 stays distinguishable from
// "not provided" (ticket 080).
applyDefault(sv.Field(i), f)
}
continue
}
if err := setField(sv.Field(i), vals); err != nil {
return fmt.Errorf("field %q: %w", name, err)
}
}
return nil
}
func applyDefault(fv reflect.Value, f reflect.StructField) {
r, err := parseRules(f.Tag.Get("validate"))
if err != nil || !r.hasDef {
return
}
switch fv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fv.SetInt(int64(r.def))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fv.SetUint(uint64(r.def))
case reflect.Float32, reflect.Float64:
fv.SetFloat(r.def)
case reflect.Bool:
// `default=1` on a bool used to parse, publish itself in the op
// documentation, and then do nothing — so a field documented as
// defaulting to true arrived false, and the caller was told the
// opposite of what happened. A non-zero default means true.
fv.SetBool(r.def != 0)
}
}
// fieldName returns the form/JSON key for a struct field: the json tag's
// name segment, the field name if untagged, "" if the field is json:"-".
func fieldName(f reflect.StructField) string {
tag, ok := f.Tag.Lookup("json")
if !ok {
return f.Name
}
name, _, _ := strings.Cut(tag, ",")
if name == "-" {
return ""
}
if name == "" {
return f.Name
}
return name
}
// setMap fills a map[string]string field from every "<name>.<key>"
// entry in values. A key with no name after the dot is skipped rather
// than stored under "": a form that posted "fields." asked for nothing,
// and the op's own validation would have to invent a message for it.
func setMap(fv reflect.Value, name string, values url.Values) error {
if fv.Type().Key().Kind() != reflect.String || fv.Type().Elem().Kind() != reflect.String {
return fmt.Errorf("unsupported map type %s (only map[string]string)", fv.Type())
}
prefix := name + "."
var out reflect.Value
for k, vals := range values {
if !strings.HasPrefix(k, prefix) || len(vals) == 0 {
continue
}
key := k[len(prefix):]
if key == "" {
continue
}
if !out.IsValid() {
out = reflect.MakeMap(fv.Type())
}
out.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(vals[0]))
}
if out.IsValid() {
fv.Set(out)
}
return nil
}
func setField(fv reflect.Value, vals []string) error {
// A POINTER field is a tri-state: nil says the key was absent, which
// is the only way a form or a JSON body can distinguish "not sent"
// from "sent as the zero value". Allocate on first sight and fill
// the pointee; a field left alone stays nil.
if fv.Kind() == reflect.Pointer {
if fv.IsNil() {
fv.Set(reflect.New(fv.Type().Elem()))
}
return setField(fv.Elem(), vals)
}
if fv.Kind() == reflect.Slice {
elem := fv.Type().Elem()
out := reflect.MakeSlice(fv.Type(), len(vals), len(vals))
for i, s := range vals {
if err := setScalar(out.Index(i), elem.Kind(), s); err != nil {
return err
}
}
fv.Set(out)
return nil
}
return setScalar(fv, fv.Kind(), vals[0])
}
// setScalar parses one value. Parse failures return client-oriented
// messages — these cross the HTTP boundary in 400 bodies, so no stdlib
// error text leaks through.
func setScalar(fv reflect.Value, kind reflect.Kind, s string) error {
switch kind {
case reflect.String:
fv.SetString(s)
case reflect.Bool:
b, err := strconv.ParseBool(s)
if err != nil {
return fmt.Errorf("must be a boolean, got %q", s)
}
fv.SetBool(b)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n, err := strconv.ParseInt(s, 10, fv.Type().Bits())
if err != nil {
return fmt.Errorf("must be an integer, got %q", s)
}
fv.SetInt(n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
n, err := strconv.ParseUint(s, 10, fv.Type().Bits())
if err != nil {
return fmt.Errorf("must be a non-negative integer, got %q", s)
}
fv.SetUint(n)
case reflect.Float32, reflect.Float64:
n, err := strconv.ParseFloat(s, fv.Type().Bits())
if err != nil {
return fmt.Errorf("must be a number, got %q", s)
}
fv.SetFloat(n)
default:
return fmt.Errorf("unsupported kind %s (In/Out structs must stay flat)", kind)
}
return nil
}
// Wildcards returns the {name} segments of a ServeMux pattern in order,
// stripping a trailing "..." — the keys the router copies from
// Request.PathValue into the url.Values handed to DecodeValues.
func Wildcards(pattern string) []string {
var names []string
for _, seg := range strings.Split(pattern, "/") {
if len(seg) > 1 && seg[0] == '{' && seg[len(seg)-1] == '}' {
names = append(names, strings.TrimSuffix(seg[1:len(seg)-1], "..."))
}
}
return names
}