Skip to content
quarry Artifact [89c284d509]
Login

Artifact [89c284d509]

Artifact 89c284d50953799e33d5001fe5c0ea557f8dce20ec80ab3f323c93f695bbc332:


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

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

func setField(fv reflect.Value, vals []string) error {
	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
}