Skip to content
quarry Artifact [7c904541f4]
Login

Artifact [7c904541f4]

Artifact 7c904541f4a8638899d60f324548fbc320db25b619f293b061ed334e9dd1f818:


// 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
}

// ApplyDefaults sets each field's declared default on every field the
// caller did not name.
//
// It exists because applyDefault was reachable only from DecodeValues,
// which serves query strings and forms. A JSON body went straight into
// the struct through encoding/json and never met a validate tag, so a
// field documented as defaulting to 1 arrived as 0 — and Descriptor
// publishes that default from the same tag regardless of transport, so
// the caller read one behaviour and got the other (9e71cc15).
//
// present holds the keys the caller actually wrote, LOWERCASED.
// Absence has to be learned from the body rather than from the decoded
// struct, because a zero value is not evidence of absence: a caller who
// writes {"listed": false} means false and must not have a default put
// back over it. That distinction is what DecodeValues gets for free
// from url.Values, and it is the whole reason this takes an argument
// instead of inspecting dst.
//
// Lowercased because encoding/json matches field names case-
// insensitively: {"Listed": false} sets the same field as
// {"listed": false}, and a case-sensitive absence test would decide
// the caller had said nothing and overwrite what they sent.
func ApplyDefaults(dst any, present map[string]bool) error {
	rv := reflect.ValueOf(dst)
	if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Struct {
		return fmt.Errorf("ops: ApplyDefaults dst must be *struct, got %T", dst)
	}
	sv := rv.Elem()
	st := sv.Type()
	for i := 0; i < st.NumField(); i++ {
		f := st.Field(i)
		name := fieldName(f)
		if name == "" || !f.IsExported() || present[strings.ToLower(name)] {
			continue
		}
		// A POINTER field is left alone on purpose. Its nil IS the
		// "caller did not mention it" signal — the shape team.member
		// adopted so an upsert could tell "set this false" from
		// silence — and filling it in would destroy the very
		// distinction it was declared to carry.
		applyDefault(sv.Field(i), f)
	}
	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
}