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() {}