Artifact 49dcf9b3d2fb45b552604985493cbda0ab898e73948dd85996524fd395670dc4:
- File
internal/ops/codec.go
— part of check-in
[839e3e756a]
at
2026-08-15 16:13:52
on branch trunk
— search fixes: folded ticket docs, mark snippets, honest limit (050/060/080/130)
search_doc gains label; tickets one folded doc per id; ⟦⟧ markers become <mark> post-escape; default= validate rule fills absent keys. (user: dmestas, size: 4221) [annotate] [blame] [check-ins using]
// 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 { // 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 }