2024-10-31 09:09:58 -04:00
|
|
|
package forms
|
|
|
|
|
|
|
|
import (
|
2024-11-09 13:14:31 -05:00
|
|
|
"encoding/json"
|
2024-10-31 09:09:58 -04:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"reflect"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2024-11-13 09:24:11 -05:00
|
|
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
2024-10-31 09:09:58 -04:00
|
|
|
|
|
|
|
"github.com/araddon/dateparse"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2024-11-09 13:14:31 -05:00
|
|
|
ErrNotStruct = errors.New("destination is not a struct")
|
|
|
|
ErrNotPointer = errors.New("destination is not a pointer")
|
|
|
|
ErrContentType = errors.New("bad content type")
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
MaxMultipartMemory int64 = 1024 * 1024 // 1MB
|
2024-10-31 09:09:58 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
type options struct {
|
2024-11-09 13:14:31 -05:00
|
|
|
tagOverride *string
|
|
|
|
parseTimeIn *time.Location
|
|
|
|
parseLocal bool
|
|
|
|
acceptBlank bool
|
|
|
|
maxMultipartMemory int64
|
|
|
|
defaultOmitEmpty bool
|
2024-10-31 09:09:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
type Option func(*options)
|
|
|
|
|
|
|
|
func WithParseTimeInTZ(l *time.Location) Option {
|
|
|
|
return func(o *options) {
|
|
|
|
o.parseTimeIn = l
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithParseLocalTime() Option {
|
|
|
|
return func(o *options) {
|
|
|
|
o.parseLocal = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithAcceptBlank() Option {
|
|
|
|
return func(o *options) {
|
|
|
|
o.acceptBlank = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithTag(t string) Option {
|
|
|
|
return func(o *options) {
|
|
|
|
o.tagOverride = &t
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
func WithMaxMultipartSize(s int64) Option {
|
|
|
|
return func(o *options) {
|
|
|
|
o.maxMultipartMemory = s
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithOmitEmpty() Option {
|
|
|
|
return func(o *options) {
|
|
|
|
o.defaultOmitEmpty = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-31 09:09:58 -04:00
|
|
|
func (o *options) Tag() string {
|
|
|
|
if o.tagOverride != nil {
|
|
|
|
return *o.tagOverride
|
|
|
|
}
|
|
|
|
|
|
|
|
return "form"
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *options) parseTime(s string, dpo ...dateparse.ParserOption) (t time.Time, set bool, err error) {
|
|
|
|
if o.acceptBlank && s == "" {
|
|
|
|
set = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if iv, err := strconv.Atoi(s); err == nil {
|
|
|
|
return time.Unix(int64(iv), 0), true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case o.parseTimeIn != nil:
|
|
|
|
t, err = dateparse.ParseIn(s, o.parseTimeIn, dpo...)
|
|
|
|
case o.parseLocal:
|
|
|
|
t, err = dateparse.ParseLocal(s, dpo...)
|
|
|
|
default:
|
|
|
|
t, err = dateparse.ParseAny(s, dpo...)
|
|
|
|
}
|
|
|
|
|
|
|
|
set = true
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *options) parseBool(s string) (v bool, set bool, err error) {
|
|
|
|
if o.acceptBlank && s == "" {
|
|
|
|
set = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
set = true
|
|
|
|
|
|
|
|
v, err = strconv.ParseBool(s)
|
|
|
|
if err != nil {
|
|
|
|
return v, set, fmt.Errorf("parsebool('%s'): %w", s, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *options) parseInt(s string) (v int, set bool, err error) {
|
|
|
|
if o.acceptBlank && s == "" {
|
|
|
|
set = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
set = true
|
|
|
|
|
|
|
|
v, err = strconv.Atoi(s)
|
|
|
|
if err != nil {
|
|
|
|
return v, set, fmt.Errorf("atoi('%s'): %w", s, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *options) parseFloat64(s string) (v float64, set bool, err error) {
|
|
|
|
if o.acceptBlank && s == "" {
|
|
|
|
set = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
set = true
|
|
|
|
|
|
|
|
v, err = strconv.ParseFloat(s, 64)
|
|
|
|
if err != nil {
|
|
|
|
return v, set, fmt.Errorf("ParseFloat('%s'): %w", s, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *options) parseDuration(s string) (v time.Duration, set bool, err error) {
|
|
|
|
if o.acceptBlank && s == "" {
|
|
|
|
set = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
set = true
|
|
|
|
|
|
|
|
v, err = time.ParseDuration(s)
|
|
|
|
if err != nil {
|
|
|
|
return v, set, fmt.Errorf("ParseDuration('%s'): %w", s, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
var typeOfByteSlice = reflect.TypeOf([]byte(nil))
|
|
|
|
|
|
|
|
func (o *options) iterFields(r *http.Request, destStruct reflect.Value) error {
|
|
|
|
structType := destStruct.Type()
|
|
|
|
for i := 0; i < destStruct.NumField(); i++ {
|
|
|
|
destFieldVal := destStruct.Field(i)
|
|
|
|
fieldType := structType.Field(i)
|
|
|
|
if !fieldType.IsExported() && !fieldType.Anonymous {
|
2024-10-31 09:09:58 -04:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
if destFieldVal.Kind() == reflect.Struct && fieldType.Anonymous {
|
|
|
|
err := o.iterFields(r, destFieldVal)
|
2024-10-31 09:09:58 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var tAr []string
|
|
|
|
var formField string
|
2024-11-09 13:14:31 -05:00
|
|
|
var omitEmpty bool
|
|
|
|
if o.defaultOmitEmpty {
|
|
|
|
omitEmpty = true
|
|
|
|
}
|
|
|
|
|
|
|
|
formTag, has := structType.Field(i).Tag.Lookup(o.Tag())
|
2024-10-31 09:09:58 -04:00
|
|
|
if has {
|
|
|
|
tAr = strings.Split(formTag, ",")
|
|
|
|
formField = tAr[0]
|
2024-11-09 13:14:31 -05:00
|
|
|
for _, v := range tAr[1:] {
|
|
|
|
if v == "omitempty" {
|
|
|
|
omitEmpty = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2024-10-31 09:09:58 -04:00
|
|
|
}
|
2024-11-09 13:14:31 -05:00
|
|
|
|
2024-10-31 09:09:58 -04:00
|
|
|
if !has || formField == "-" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
destFieldIntf := destFieldVal.Interface()
|
2024-10-31 09:09:58 -04:00
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
if destFieldVal.Kind() == reflect.Slice && destFieldVal.Type() == typeOfByteSlice {
|
2024-10-31 09:09:58 -04:00
|
|
|
file, hdr, err := r.FormFile(formField)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("get form file: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
nameField, hasFilename := structType.Field(i).Tag.Lookup("filenameField")
|
2024-10-31 09:09:58 -04:00
|
|
|
if hasFilename {
|
2024-11-09 13:14:31 -05:00
|
|
|
fnf := destStruct.FieldByName(nameField)
|
2024-10-31 09:09:58 -04:00
|
|
|
if fnf == (reflect.Value{}) {
|
|
|
|
panic(fmt.Errorf("filenameField '%s' does not exist", nameField))
|
|
|
|
}
|
|
|
|
|
|
|
|
fnf.SetString(hdr.Filename)
|
|
|
|
}
|
|
|
|
audioBytes, err := io.ReadAll(file)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("file read: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
destFieldVal.SetBytes(audioBytes)
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if !r.Form.Has(formField) && omitEmpty {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
ff := r.Form.Get(formField)
|
|
|
|
|
|
|
|
switch v := destFieldIntf.(type) {
|
|
|
|
case string, *string:
|
|
|
|
setVal(destFieldVal, ff != "" || o.acceptBlank, ff)
|
|
|
|
case int, uint, *int, *uint:
|
|
|
|
val, set, err := o.parseInt(ff)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
setVal(destFieldVal, set, val)
|
|
|
|
case float64:
|
|
|
|
val, set, err := o.parseFloat64(ff)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
setVal(destFieldVal, set, val)
|
|
|
|
case bool, *bool:
|
|
|
|
val, set, err := o.parseBool(ff)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
setVal(destFieldVal, set, val)
|
2024-11-13 09:24:11 -05:00
|
|
|
case time.Time, *time.Time, jsontypes.Time, *jsontypes.Time:
|
2024-11-09 13:14:31 -05:00
|
|
|
t, set, err := o.parseTime(ff)
|
2024-10-31 09:09:58 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-11-09 13:14:31 -05:00
|
|
|
setVal(destFieldVal, set, t)
|
2024-11-13 09:24:11 -05:00
|
|
|
case time.Duration, *time.Duration, jsontypes.Duration, *jsontypes.Duration:
|
2024-11-09 13:14:31 -05:00
|
|
|
d, set, err := o.parseDuration(ff)
|
2024-10-31 09:09:58 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-11-09 13:14:31 -05:00
|
|
|
setVal(destFieldVal, set, d)
|
2024-10-31 09:09:58 -04:00
|
|
|
case []int:
|
2024-11-09 13:14:31 -05:00
|
|
|
val := strings.Trim(ff, "[]")
|
2024-10-31 09:09:58 -04:00
|
|
|
if val == "" && o.acceptBlank {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
vals := strings.Split(val, ",")
|
|
|
|
ar := make([]int, 0, len(vals))
|
|
|
|
for _, v := range vals {
|
|
|
|
i, err := strconv.Atoi(v)
|
|
|
|
if err == nil {
|
|
|
|
ar = append(ar, i)
|
|
|
|
}
|
|
|
|
}
|
2024-11-09 13:14:31 -05:00
|
|
|
destFieldVal.Set(reflect.ValueOf(ar))
|
2024-10-31 09:09:58 -04:00
|
|
|
default:
|
|
|
|
panic(fmt.Errorf("unsupported type %T", v))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
func setVal(destFieldVal reflect.Value, set bool, src any) {
|
2024-10-31 09:09:58 -04:00
|
|
|
if !set {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
destType := destFieldVal.Type()
|
|
|
|
srcVal := reflect.ValueOf(src)
|
2024-10-31 09:09:58 -04:00
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
if srcVal.Kind() == reflect.Ptr {
|
|
|
|
srcVal = srcVal.Elem()
|
2024-10-31 09:09:58 -04:00
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
if destType.Kind() == reflect.Ptr {
|
|
|
|
if !srcVal.CanAddr() {
|
|
|
|
if srcVal.CanConvert(destType.Elem()) {
|
|
|
|
srcVal = srcVal.Convert(destType.Elem())
|
|
|
|
}
|
|
|
|
copy := reflect.New(srcVal.Type())
|
|
|
|
copy.Elem().Set(srcVal)
|
|
|
|
srcVal = copy
|
|
|
|
}
|
|
|
|
} else if srcVal.CanConvert(destFieldVal.Type()) {
|
|
|
|
srcVal = srcVal.Convert(destFieldVal.Type())
|
2024-10-31 09:09:58 -04:00
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
destFieldVal.Set(srcVal)
|
2024-10-31 09:09:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func Unmarshal(r *http.Request, dest any, opt ...Option) error {
|
2024-11-09 13:14:31 -05:00
|
|
|
o := options{
|
|
|
|
maxMultipartMemory: MaxMultipartMemory,
|
|
|
|
}
|
|
|
|
|
2024-10-31 09:09:58 -04:00
|
|
|
for _, opt := range opt {
|
|
|
|
opt(&o)
|
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
contentType := strings.Split(r.Header.Get("Content-Type"), ";")[0]
|
2024-10-31 09:09:58 -04:00
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
switch contentType {
|
|
|
|
case "multipart/form-data":
|
|
|
|
err := r.ParseMultipartForm(o.maxMultipartMemory)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("ParseForm: %w", err)
|
|
|
|
}
|
2024-10-31 09:09:58 -04:00
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
return o.unmarshalForm(r, dest)
|
|
|
|
case "application/x-www-form-urlencoded":
|
2024-10-31 09:09:58 -04:00
|
|
|
err := r.ParseForm()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("ParseForm: %w", err)
|
|
|
|
}
|
2024-11-09 13:14:31 -05:00
|
|
|
return o.unmarshalForm(r, dest)
|
|
|
|
case "application/json":
|
|
|
|
return json.NewDecoder(r.Body).Decode(dest)
|
|
|
|
}
|
|
|
|
|
|
|
|
return ErrContentType
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *options) unmarshalForm(r *http.Request, dest any) error {
|
|
|
|
destVal := reflect.ValueOf(dest)
|
|
|
|
if k := destVal.Kind(); k == reflect.Ptr {
|
|
|
|
destVal = destVal.Elem()
|
|
|
|
} else {
|
|
|
|
return ErrNotPointer
|
|
|
|
}
|
|
|
|
|
|
|
|
if destVal.Kind() != reflect.Struct {
|
|
|
|
return ErrNotStruct
|
2024-10-31 09:09:58 -04:00
|
|
|
}
|
|
|
|
|
2024-11-09 13:14:31 -05:00
|
|
|
return o.iterFields(r, destVal)
|
2024-10-31 09:09:58 -04:00
|
|
|
}
|