207 lines
4.8 KiB
Go
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() {}
|