mirror of
https://codeberg.org/readeck/readeck.git
synced 2026-06-18 11:04:36 +00:00
252 lines
6.3 KiB
Go
252 lines
6.3 KiB
Go
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package forms
|
|
|
|
import (
|
|
"encoding"
|
|
"errors"
|
|
"iter"
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ValuesUnmarshaler is an interface implemented by types with a custom
|
|
// [url.Values] item decoder.
|
|
type ValuesUnmarshaler interface {
|
|
UnmarshalValues([]string) error
|
|
}
|
|
|
|
// URLValuesUnmarshaler is an interface implemented by types with a
|
|
// custom [url.Values] decoder.
|
|
// This can be implemented by a [Form] instance for special use
|
|
// cases.
|
|
type URLValuesUnmarshaler interface {
|
|
UnmarshalValues(url.Values) error
|
|
}
|
|
|
|
// UnmarshalURLValues decode [url.Values] into v.
|
|
// v must be a pointer to a struct. Nested values are supported
|
|
// with a "." separator.
|
|
// For example, the following struct:
|
|
//
|
|
// type nested {
|
|
// Name string `json:"string"`
|
|
// Meta struct {
|
|
// Address string `json:"address"`
|
|
// } `json:"meta"`
|
|
// }
|
|
//
|
|
// can be decoded with "?name=someone&meta.address=somewhere"
|
|
//
|
|
// The output struct's fields support the "json" tag.
|
|
//
|
|
// Each value name matching a struct field is decoded using [UnmarshalValues]
|
|
// while values with a prefix are sent again to this function with the prefix
|
|
// removed.
|
|
func UnmarshalURLValues(values url.Values, v any) (err error) {
|
|
pv := reflect.ValueOf(v)
|
|
cv := reflect.Indirect(pv)
|
|
if pv.Kind() != reflect.Pointer || cv.Kind() != reflect.Struct {
|
|
return errors.New("value must be a pointer to a struct")
|
|
}
|
|
|
|
if v, ok := v.(URLValuesUnmarshaler); ok {
|
|
return v.UnmarshalValues(values)
|
|
}
|
|
|
|
for fi := range recurseStructFields(cv, "") {
|
|
if fi.field.Tag.Get("json") == "-" {
|
|
continue
|
|
}
|
|
|
|
v := fi.value
|
|
if v.Kind() != reflect.Pointer {
|
|
v = v.Addr()
|
|
}
|
|
|
|
// Direct value
|
|
if values.Has(fi.name) {
|
|
if err = UnmarshalValues(values[fi.name], v.Interface()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Prefixed values
|
|
prefixedValues := url.Values{}
|
|
for k, val := range values {
|
|
if p, s, _ := strings.Cut(k, "."); p == fi.name && s != "" {
|
|
prefixedValues[s] = val
|
|
}
|
|
}
|
|
if len(prefixedValues) > 0 {
|
|
if err = UnmarshalURLValues(prefixedValues, v.Interface()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalValues decodes a list of string into v.
|
|
// When v implements [ValuesUnmarshaler], [encoding.TextUnmarshaler]
|
|
// or [encoding.BinaryUnmarshaler], their respective unmarshal methods
|
|
// have priority (in that order).
|
|
//
|
|
// Otherwise, scalar values are decoded (bool, string, float, signed and unsigned integer).
|
|
// With [encoding.TextUnmarshaler], [encoding.BinaryUnmarshaler] or scalar
|
|
// values, only the first item from values is decoded.
|
|
//
|
|
// When v is a slice, [UnmarshalValues] is called on each item in values.
|
|
func UnmarshalValues(values []string, v any) (err error) {
|
|
if v, ok := v.(ValuesUnmarshaler); ok {
|
|
return v.UnmarshalValues(values)
|
|
}
|
|
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if v, ok := v.(encoding.TextUnmarshaler); ok {
|
|
return v.UnmarshalText([]byte(values[0]))
|
|
}
|
|
|
|
if v, ok := v.(encoding.BinaryUnmarshaler); ok {
|
|
return v.UnmarshalBinary([]byte(values[0]))
|
|
}
|
|
|
|
pv := reflect.ValueOf(v)
|
|
if pv.Kind() == reflect.Pointer {
|
|
pv = reflect.Indirect(pv)
|
|
}
|
|
|
|
switch pv.Type().Kind() {
|
|
case reflect.String:
|
|
pv.SetString(values[0])
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
n, err := strconv.ParseInt(values[0], 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pv.SetInt(n)
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
n, err := strconv.ParseUint(values[0], 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pv.SetUint(n)
|
|
case reflect.Float32:
|
|
n, err := strconv.ParseFloat(values[0], 32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pv.SetFloat(n)
|
|
case reflect.Float64:
|
|
n, err := strconv.ParseFloat(values[0], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pv.SetFloat(n)
|
|
case reflect.Bool:
|
|
ok, err := strconv.ParseBool(values[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pv.SetBool(ok)
|
|
case reflect.Slice:
|
|
pv.Set(reflect.MakeSlice(pv.Type(), len(values), len(values)))
|
|
for i, x := range values {
|
|
d := reflect.New(pv.Type().Elem())
|
|
if err := UnmarshalValues([]string{x}, d.Interface()); err != nil {
|
|
pv.Set(reflect.MakeSlice(pv.Type(), 0, 0))
|
|
return err
|
|
}
|
|
pv.Index(i).Set(reflect.Indirect(d))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// recurseStructFields iterates recursively over a struc fields.
|
|
// It yields a name (prefixed when nested) and a [reflect.Value] of
|
|
// anything that is exported.
|
|
func recurseStructFields(src reflect.Value, prefix string) iter.Seq[structFieldInfo] {
|
|
return func(yield func(structFieldInfo) bool) {
|
|
if !pushRecurseStructFields(src, prefix, yield) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// pushRecurseStructFields is the recursive iteration pusher.
|
|
// It can handle Anonymous struct fields (allowing for form composition)
|
|
// and nested structs.
|
|
func pushRecurseStructFields(src reflect.Value, prefix string, yield func(structFieldInfo) bool) bool {
|
|
for info := range iterStructFields(src) {
|
|
info.name = prefix + info.name
|
|
|
|
if info.field.Type.Kind() == reflect.Struct {
|
|
if !pushRecurseStructFields(info.value, info.name+".", yield) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if !yield(info) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// iterStructFields iterates over a struc fields. If a field is anonymous
|
|
// it yields its inner values at the same level.
|
|
func iterStructFields(src reflect.Value) iter.Seq[structFieldInfo] {
|
|
return func(yield func(structFieldInfo) bool) {
|
|
if !pushIterStructFields(src, yield) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// pushIterStructFields is the iteration pusher.
|
|
// It can handle Anonymous struct fields (allowing for form composition).
|
|
func pushIterStructFields(src reflect.Value, yield func(structFieldInfo) bool) bool {
|
|
for field, value := range src.Fields() {
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
|
|
// An anonymous field stays at the same prefix level
|
|
// and we iterate over its own fields.
|
|
if field.Anonymous {
|
|
if field.Type.Kind() == reflect.Struct {
|
|
if !pushIterStructFields(value, yield) {
|
|
return false
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
|
|
if name == "" {
|
|
name = field.Name
|
|
}
|
|
|
|
if !yield(structFieldInfo{name: name, field: field, value: value}) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
type structFieldInfo struct {
|
|
name string
|
|
field reflect.StructField
|
|
value reflect.Value
|
|
}
|