package forms import ( "fmt" "net/http" "net/url" "reflect" "strconv" "strings" ) 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 } 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 }