Skip to content
quarry Artifact [0f46f67195]
Login

Artifact [0f46f67195]

Artifact 0f46f671955defbca3eeda2939cc0adeb944375cfa97bd8af542cc5caaf610e2:


// 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)
	}
	sv := rv.Elem()
	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 {
			continue
		}
		if err := setField(sv.Field(i), vals); err != nil {
			return fmt.Errorf("field %q: %w", name, err)
		}
	}
	return nil
}

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