197 lines
4.4 KiB
Go
197 lines
4.4 KiB
Go
package forms
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type Formatter func(string) string
|
|
|
|
var Formatters = map[string]Formatter{
|
|
"trim": strings.TrimSpace,
|
|
"lower": strings.ToLower,
|
|
"upper": strings.ToUpper,
|
|
"capitalize": capitalize,
|
|
}
|
|
|
|
type Validator func(fieldName string, value any, param string) error
|
|
|
|
var Validators = map[string]Validator{
|
|
"nonzero": nonzero,
|
|
"minlen": minlen,
|
|
"email": email,
|
|
}
|
|
|
|
func FormToStruct[T any](r *http.Request) (T, error) {
|
|
var target T
|
|
if err := r.ParseForm(); err != nil {
|
|
return target, fmt.Errorf("error parsing form: %v", err)
|
|
}
|
|
err := UrlValuesToStruct(r.Form, &target)
|
|
return target, err
|
|
}
|
|
|
|
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
|
|
}
|
|
parts := strings.Split(tag, ",")
|
|
var fns []func(string) string
|
|
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
|
|
}
|