Files
lazyhandler/magic/form.go
2025-06-18 10:12:19 +08:00

207 lines
4.8 KiB
Go

package magic
import (
"fmt"
"net/http"
"reflect"
"regexp"
"strconv"
"strings"
)
var (
cachedStructFormFields = make(map[reflect.Type][]structFormFields)
cachedUnsupportedFormFields = make(map[reflect.Type]structFormFields)
)
type Form[T any] struct {
Data T
}
type structFormFields struct {
PathKey string
FieldKey string
Type reflect.Type
MinValues, MaxValues int
Match *regexp.Regexp
}
type UnsupportedFormType struct {
inner reflect.Type
}
func (err UnsupportedFormType) Error() string {
return "unsupported type for Form: " + err.inner.String()
}
type FormValueNotFitIn struct {
Count int
field structFormFields
}
func (err FormValueNotFitIn) Error() string {
return fmt.Sprintf("Form %v is expected to has %v~%v values, got %v", err.field.PathKey, err.field.MinValues, err.field.MaxValues, err.Count)
}
type InvalidFormValue struct {
Match *regexp.Regexp
Value string
}
func (err InvalidFormValue) Error() string {
return fmt.Sprintf("value not matched with regexp %v: %v", err.Match, err.Value)
}
func findReflectFormFields(v reflect.Value) ([]structFormFields, error) {
if cached, ok := cachedStructFormFields[v.Type()]; ok {
return cached, nil
}
if cached, ok := cachedUnsupportedFormFields[v.Type()]; ok {
return nil, UnsupportedPathValueType{inner: cached.Type}
}
var fields []structFormFields
if v.Kind() == reflect.Struct {
t := v.Type()
for idx := 0; idx < t.NumField(); idx++ {
field := t.Field(idx)
pathKey := field.Name
var match *regexp.Regexp
if tags, ok := field.Tag.Lookup("form"); ok {
parts := strings.Split(strings.TrimSpace(tags), ",")
if len(parts) == 0 {
// do nothing
} else {
if parts[0] != "" {
pathKey = parts[0]
}
for _, part := range parts[1:] {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "match=") {
part := strings.TrimPrefix(part, "match=")
if unquoted, err := strconv.Unquote(part); err != nil {
return nil, err
} else {
part = unquoted
}
exp, err := regexp.Compile(part)
if err != nil {
return nil, err
}
match = exp
}
}
}
}
fieldType := field.Type
maxValues, minValues := 1, 1
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
minValues = 0
} else if fieldType.Kind() == reflect.Array {
maxValues, minValues = fieldType.Len(), fieldType.Len()
fieldType = fieldType.Elem()
} else if fieldType.Kind() == reflect.Slice {
fieldType = fieldType.Elem()
maxValues, minValues = -1, 0
}
if pathValueConvertTable[fieldType.Kind()] == nil {
if _, ok := fieldType.MethodByName("FromString"); !ok {
cachedUnsupportedPathValueFields[t] = field
return nil, UnsupportedPathValueType{inner: field.Type}
}
}
fields = append(fields, structFormFields{
PathKey: pathKey,
FieldKey: field.Name,
Type: fieldType,
MaxValues: maxValues,
MinValues: minValues,
Match: match,
})
cachedStructFormFields[t] = fields
}
} else {
return nil, UnsupportedFormType{inner: v.Type()}
}
return fields, nil
}
func (form *Form[T]) FromRequest(r *http.Request) error {
v := reflect.ValueOf(form).Elem().FieldByName("Data")
fields, err := findReflectFormFields(v)
if err != nil {
return err
}
if err := r.ParseMultipartForm(32 << 20); err != nil {
return err
}
q := r.PostForm
for _, field := range fields {
values := q[field.PathKey]
if len(values) < field.MinValues || (field.MaxValues > 0 && len(values) > field.MaxValues) {
return FormValueNotFitIn{Count: len(values), field: field}
}
structField := v.FieldByName(field.FieldKey)
if structField.Kind() == reflect.Slice && structField.Len() < len(values) {
if structField.Cap() < len(values) {
structField.SetCap(len(values))
}
structField.SetLen(len(values))
}
for idx, value := range values {
if structField.Kind() != reflect.Array && structField.Kind() != reflect.Slice {
if idx != len(values)-1 {
// only the last takes effect
continue
}
}
// check match regexp
if field.Match != nil && !field.Match.MatchString(value) {
return InvalidFormValue{Match: field.Match, Value: value}
}
// convert to elem value
convert := pathValueConvertTable[field.Type.Kind()]
result, err := convert(value)
if err != nil {
return err
}
reflectValue := reflect.ValueOf(result)
if structField.Kind() != reflect.Array && structField.Kind() != reflect.Slice {
if field.MinValues == 0 {
newReflectValue := reflect.New(field.Type)
newReflectValue.Elem().Set(reflectValue)
reflectValue = newReflectValue
}
structField.Set(reflectValue)
} else {
structField.Index(idx).Set(reflectValue)
}
}
}
return nil
}
func (form Form[T]) TakeRequestBody() {}