axiom/forms/forms.go
2025-07-02 11:16:51 -06:00

197 lines
4.2 KiB
Go

package forms
import (
"fmt"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
)
func FormToStruct[T any](r *http.Request) (T, error) {
var target T
ct := r.Header.Get("Content-Type")
if strings.HasPrefix(ct, "multipart/form-data") {
if err := r.ParseMultipartForm(32 << 20); err != nil {
return target, fmt.Errorf("error parsing multipart form: %v", err)
}
} else {
if err := r.ParseForm(); err != nil {
return target, fmt.Errorf("error parsing form: %v", err)
}
}
return target, UrlValuesToStruct(r.Form, &target)
}
func UrlValuesToStruct(form url.Values, dst any) error {
v := reflect.ValueOf(dst)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
return fmt.Errorf("dst must be a pointer to a struct")
}
v = v.Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldValue := v.Field(i)
if !fieldValue.CanSet() {
continue
}
key := field.Tag.Get("form")
required := field.Tag.Get("req") == "1"
formatters := parseFormatters(field.Tag.Get("fmt"))
validateTags := parseValidators(field.Tag.Get("validate"))
values, ok := form[key]
if !ok || len(values) == 0 {
if required {
return fmt.Errorf("missing required form field: %s", key)
}
continue
}
for i := range values {
for _, fmtFunc := range formatters {
values[i] = fmtFunc(values[i])
}
}
fieldKind := fieldValue.Kind()
if fieldKind == reflect.Slice {
elemKind := field.Type.Elem().Kind()
castedSlice, err := castStringSliceToType(values, elemKind)
if err != nil {
return fmt.Errorf("field '%s': %v", field.Name, err)
}
sliceValue := reflect.MakeSlice(field.Type, len(castedSlice), len(castedSlice))
for i, val := range castedSlice {
sliceValue.Index(i).Set(reflect.ValueOf(val).Convert(field.Type.Elem()))
}
fieldValue.Set(sliceValue)
} else {
if len(values) != 1 {
return fmt.Errorf("field '%s' expects a single value", field.Name)
}
castedVal, err := castStringToType(values[0], fieldKind)
if err != nil {
return fmt.Errorf("field '%s': %v", field.Name, err)
}
fieldValue.Set(reflect.ValueOf(castedVal).Convert(field.Type))
}
var finalValue any
if fieldKind == reflect.Slice {
finalValue = fieldValue.Interface()
} else {
finalValue = fieldValue.Interface()
}
for _, validator := range validateTags {
if fn, ok := Validators[validator.Name]; ok {
if err := fn(field.Name, finalValue, validator.Param); err != nil {
return err
}
}
}
}
return nil
}
func castStringSliceToType(input []string, kind reflect.Kind) ([]any, error) {
var output []any
for _, value := range input {
cast, err := castStringToType(value, kind)
if err != nil {
return nil, err
}
output = append(output, cast)
}
return output, nil
}
func castStringToType(value string, kind reflect.Kind) (any, error) {
switch kind {
case reflect.String:
return value, nil
case reflect.Int, reflect.Int64:
new, err := strconv.Atoi(value)
if err != nil {
return nil, fmt.Errorf("failed to cast string to integer: %v", err)
}
return new, nil
case reflect.Float32, reflect.Float64:
new, err := strconv.ParseFloat(value, 64)
if err != nil {
return nil, fmt.Errorf("failed to cast string to float64: %v", err)
}
return new, nil
case reflect.Bool:
new, err := strconv.ParseBool(value)
if err != nil {
return nil, fmt.Errorf("failed to cast string to boolean: %v", err)
}
return new, nil
default:
return nil, fmt.Errorf("unsupported kind: %s", kind)
}
}
func parseFormatters(tag string) []func(string) string {
if tag == "" {
return nil
}
var fns []func(string) string
parts := strings.SplitSeq(tag, ",")
for p := range parts {
if fn, ok := Formatters[strings.TrimSpace(p)]; ok {
fns = append(fns, fn)
}
}
return fns
}
func parseValidators(tag string) []struct {
Name string
Param string
} {
if tag == "" {
return nil
}
var result []struct {
Name string
Param string
}
parts := strings.SplitSeq(tag, ",")
for part := range parts {
pair := strings.SplitN(part, ":", 2)
if len(pair) == 2 {
result = append(result, struct{ Name, Param string }{pair[0], pair[1]})
} else {
result = append(result, struct{ Name, Param string }{pair[0], ""})
}
}
return result
}