Break out form unmarshal
This commit is contained in:
parent
0d793cb31a
commit
80badd0f00
12 changed files with 634 additions and 138 deletions
|
@ -29,7 +29,7 @@ alerting:
|
||||||
lookbackDays: 7
|
lookbackDays: 7
|
||||||
halfLife: 30m
|
halfLife: 30m
|
||||||
recent: 2h
|
recent: 2h
|
||||||
alertThreshold: 0.5
|
alertThreshold: 0.3
|
||||||
renotify: 30m
|
renotify: 30m
|
||||||
notify:
|
notify:
|
||||||
- provider: slackwebhook
|
- provider: slackwebhook
|
||||||
|
|
8
go.mod
8
go.mod
|
@ -16,8 +16,10 @@ require (
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/hajimehoshi/oto v1.0.1
|
github.com/hajimehoshi/oto v1.0.1
|
||||||
github.com/jackc/pgx/v5 v5.6.0
|
github.com/jackc/pgx/v5 v5.6.0
|
||||||
|
github.com/nikoksr/notify v1.0.1
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
|
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.28.0
|
||||||
golang.org/x/sync v0.8.0
|
golang.org/x/sync v0.8.0
|
||||||
|
@ -29,11 +31,11 @@ require (
|
||||||
require (
|
require (
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
github.com/go-audio/audio v1.0.0 // indirect
|
github.com/go-audio/audio v1.0.0 // indirect
|
||||||
github.com/go-audio/riff v1.0.0 // indirect
|
github.com/go-audio/riff v1.0.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
@ -50,16 +52,14 @@ require (
|
||||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/nikoksr/notify v1.0.1 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/stretchr/testify v1.9.0 // indirect
|
|
||||||
go.uber.org/atomic v1.7.0 // indirect
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
golang.org/x/exp/shiny v0.0.0-20240719175910-8a7402abbf56 // indirect
|
golang.org/x/exp/shiny v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||||
golang.org/x/image v0.14.0 // indirect
|
golang.org/x/image v0.14.0 // indirect
|
||||||
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
|
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
|
||||||
golang.org/x/net v0.30.0 // indirect
|
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
golang.org/x/text v0.19.0 // indirect
|
golang.org/x/text v0.19.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -76,6 +76,8 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||||
|
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
|
306
internal/forms/forms.go
Normal file
306
internal/forms/forms.go
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
package forms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/jsontime"
|
||||||
|
|
||||||
|
"github.com/araddon/dateparse"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotStruct = errors.New("destination is not a struct")
|
||||||
|
ErrNotPointer = errors.New("destination is not a pointer")
|
||||||
|
)
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
tagOverride *string
|
||||||
|
parseTimeIn *time.Location
|
||||||
|
parseLocal bool
|
||||||
|
acceptBlank bool
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *options) iterFields(r *http.Request, rv reflect.Value) error {
|
||||||
|
rt := rv.Type()
|
||||||
|
for i := 0; i < rv.NumField(); i++ {
|
||||||
|
f := rv.Field(i)
|
||||||
|
tf := rt.Field(i)
|
||||||
|
if !tf.IsExported() && !tf.Anonymous {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Kind() == reflect.Struct && tf.Anonymous {
|
||||||
|
err := o.iterFields(r, f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tAr []string
|
||||||
|
var formField string
|
||||||
|
formTag, has := rt.Field(i).Tag.Lookup(o.Tag())
|
||||||
|
if has {
|
||||||
|
tAr = strings.Split(formTag, ",")
|
||||||
|
formField = tAr[0]
|
||||||
|
}
|
||||||
|
if !has || formField == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fi := f.Interface()
|
||||||
|
|
||||||
|
switch v := fi.(type) {
|
||||||
|
case string, *string:
|
||||||
|
s := r.Form.Get(formField)
|
||||||
|
setVal(f, s != "" || o.acceptBlank, v, s)
|
||||||
|
case int, uint, *int, *uint:
|
||||||
|
ff := r.Form.Get(formField)
|
||||||
|
val, set, err := o.parseInt(ff)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
setVal(f, set, v, val)
|
||||||
|
case float64:
|
||||||
|
ff := r.Form.Get(formField)
|
||||||
|
val, set, err := o.parseFloat64(ff)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
setVal(f, set, v, val)
|
||||||
|
case bool, *bool:
|
||||||
|
ff := r.Form.Get(formField)
|
||||||
|
val, set, err := o.parseBool(ff)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
setVal(f, set, v, val)
|
||||||
|
case []byte:
|
||||||
|
file, hdr, err := r.FormFile(formField)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get form file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nameField, hasFilename := rt.Field(i).Tag.Lookup("filenameField")
|
||||||
|
if hasFilename {
|
||||||
|
fnf := rv.FieldByName(nameField)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.SetBytes(audioBytes)
|
||||||
|
case time.Time, *time.Time, jsontime.Time, *jsontime.Time:
|
||||||
|
tval := r.Form.Get(formField)
|
||||||
|
t, set, err := o.parseTime(tval)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
setVal(f, set, v, t)
|
||||||
|
case time.Duration, *time.Duration, jsontime.Duration, *jsontime.Duration:
|
||||||
|
dval := r.Form.Get(formField)
|
||||||
|
d, set, err := o.parseDuration(dval)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
setVal(f, set, v, d)
|
||||||
|
case []int:
|
||||||
|
val := strings.Trim(r.Form.Get(formField), "[]")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Set(reflect.ValueOf(ar))
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unsupported type %T", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVal(setField reflect.Value, set bool, fv any, sv any) {
|
||||||
|
if !set {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := reflect.TypeOf(fv)
|
||||||
|
svo := reflect.ValueOf(sv)
|
||||||
|
|
||||||
|
if svo.CanConvert(rv) {
|
||||||
|
svo = svo.Convert(rv)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rv.Kind() == reflect.Ptr {
|
||||||
|
svo = svo.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
setField.Set(svo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unmarshal(r *http.Request, dest any, opt ...Option) error {
|
||||||
|
o := options{}
|
||||||
|
for _, opt := range opt {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := reflect.ValueOf(dest)
|
||||||
|
if k := rv.Kind(); k == reflect.Ptr {
|
||||||
|
rv = rv.Elem()
|
||||||
|
} else {
|
||||||
|
return ErrNotPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
if rv.Kind() != reflect.Struct {
|
||||||
|
return ErrNotStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if strings.HasPrefix(r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ParseForm: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.iterFields(r, rv)
|
||||||
|
}
|
216
internal/forms/forms_test.go
Normal file
216
internal/forms/forms_test.go
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
package forms_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/forms"
|
||||||
|
"dynatron.me/x/stillbox/internal/jsontime"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/pkg/gordio/alerting"
|
||||||
|
"dynatron.me/x/stillbox/pkg/gordio/config"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type callUploadRequest struct {
|
||||||
|
Audio []byte `form:"audio" filenameField:"AudioName"`
|
||||||
|
AudioName string
|
||||||
|
AudioType string `form:"audioType"`
|
||||||
|
DateTime time.Time `form:"dateTime"`
|
||||||
|
Frequencies []int `form:"frequencies"`
|
||||||
|
Frequency int `form:"frequency"`
|
||||||
|
Key string `form:"key"`
|
||||||
|
Patches []int `form:"patches"`
|
||||||
|
Source int `form:"source"`
|
||||||
|
Sources []int `form:"sources"`
|
||||||
|
System int `form:"system"`
|
||||||
|
SystemLabel string `form:"systemLabel"`
|
||||||
|
Talkgroup int `form:"talkgroup"`
|
||||||
|
TalkgroupGroup string `form:"talkgroupGroup"`
|
||||||
|
TalkgroupLabel string `form:"talkgroupLabel"`
|
||||||
|
TalkgroupTag string `form:"talkgroupTag"`
|
||||||
|
DontStore bool `form:"dontStore"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type urlEncTest struct {
|
||||||
|
LookbackDays int `form:"lookbackDays"`
|
||||||
|
HalfLife time.Duration `form:"halfLife"`
|
||||||
|
Recent string `form:"recent"`
|
||||||
|
ScoreStart time.Time `form:"scoreStart"`
|
||||||
|
ScoreEnd time.Time `form:"scoreEnd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type urlEncTestJT struct {
|
||||||
|
LookbackDays uint `json:"lookbackDays"`
|
||||||
|
HalfLife jsontime.Duration `json:"halfLife"`
|
||||||
|
Recent string `json:"recent"`
|
||||||
|
ScoreStart jsontime.Time `json:"scoreStart"`
|
||||||
|
ScoreEnd jsontime.Time `json:"scoreEnd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
UrlEncTest = urlEncTest{
|
||||||
|
LookbackDays: 7,
|
||||||
|
HalfLife: 30 * time.Minute,
|
||||||
|
Recent: "2h0m0s",
|
||||||
|
ScoreStart: time.Date(2024, time.October, 28, 9, 25, 0, 0, time.UTC),
|
||||||
|
}
|
||||||
|
|
||||||
|
UrlEncTestJT = urlEncTestJT{
|
||||||
|
LookbackDays: 7,
|
||||||
|
HalfLife: jsontime.Duration(30 * time.Minute),
|
||||||
|
Recent: "2h0m0s",
|
||||||
|
ScoreStart: jsontime.Time(time.Date(2024, time.October, 28, 9, 25, 0, 0, time.UTC)),
|
||||||
|
}
|
||||||
|
|
||||||
|
UrlEncTestJTLocal = urlEncTestJT{
|
||||||
|
LookbackDays: 7,
|
||||||
|
HalfLife: jsontime.Duration(30 * time.Minute),
|
||||||
|
Recent: "2h0m0s",
|
||||||
|
ScoreStart: jsontime.Time(time.Date(2024, time.October, 28, 9, 25, 0, 0, time.Local)),
|
||||||
|
}
|
||||||
|
|
||||||
|
realSim = &alerting.Simulation{
|
||||||
|
Alerting: config.Alerting{
|
||||||
|
LookbackDays: 7,
|
||||||
|
HalfLife: jsontime.Duration(30 * time.Minute),
|
||||||
|
Recent: jsontime.Duration(2 * time.Hour),
|
||||||
|
},
|
||||||
|
SimInterval: jsontime.Duration(5 * time.Minute),
|
||||||
|
ScoreStart: jsontime.Time(time.Date(2024, time.October, 22, 17, 49, 0, 0, time.Local)),
|
||||||
|
}
|
||||||
|
|
||||||
|
Call1 = callUploadRequest{
|
||||||
|
AudioName: "20241031_081939.285.mp3",
|
||||||
|
Audio: []byte("ID3dotdotdotLAME3.98.4a"),
|
||||||
|
DateTime: time.Unix(1730377177, 0),
|
||||||
|
Frequency: 852637500,
|
||||||
|
Key: "2b7c871b-abcd-defa-0123-456789abcdef",
|
||||||
|
Source: 3237,
|
||||||
|
System: 197,
|
||||||
|
SystemLabel: "RISCON",
|
||||||
|
Talkgroup: 2,
|
||||||
|
TalkgroupGroup: "Wide Area",
|
||||||
|
TalkgroupLabel: "Wide Area 1 FD/EMS Intercity",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeRequest(fixture string) *http.Request {
|
||||||
|
fixt, err := os.Open("testdata/" + fixture)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := http.ReadRequest(bufio.NewReader(fixt))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshal(t *testing.T) {
|
||||||
|
var str string
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
r *http.Request
|
||||||
|
dest any
|
||||||
|
compare any
|
||||||
|
expectErr error
|
||||||
|
opts []forms.Option
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "base case",
|
||||||
|
r: makeRequest("call1.http"),
|
||||||
|
dest: &callUploadRequest{},
|
||||||
|
compare: &Call1,
|
||||||
|
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base case no accept blank",
|
||||||
|
r: makeRequest("call1.http"),
|
||||||
|
dest: &callUploadRequest{},
|
||||||
|
compare: &Call1,
|
||||||
|
expectErr: errors.New(`parsebool(''): strconv.ParseBool: parsing "": invalid syntax`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not a pointer",
|
||||||
|
r: makeRequest("call1.http"),
|
||||||
|
dest: callUploadRequest{},
|
||||||
|
compare: callUploadRequest{},
|
||||||
|
expectErr: forms.ErrNotPointer,
|
||||||
|
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not a struct",
|
||||||
|
r: makeRequest("call1.http"),
|
||||||
|
dest: &str,
|
||||||
|
compare: callUploadRequest{},
|
||||||
|
expectErr: forms.ErrNotStruct,
|
||||||
|
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url encoded",
|
||||||
|
r: makeRequest("urlenc.http"),
|
||||||
|
dest: &urlEncTest{},
|
||||||
|
compare: &UrlEncTest,
|
||||||
|
expectErr: errors.New(`Could not find format for ""`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url encoded accept blank",
|
||||||
|
r: makeRequest("urlenc.http"),
|
||||||
|
dest: &urlEncTest{},
|
||||||
|
compare: &UrlEncTest,
|
||||||
|
opts: []forms.Option{forms.WithAcceptBlank()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url encoded jsontime",
|
||||||
|
r: makeRequest("urlenc.http"),
|
||||||
|
dest: &urlEncTestJT{},
|
||||||
|
compare: &UrlEncTestJT,
|
||||||
|
expectErr: errors.New(`Could not find format for ""`),
|
||||||
|
opts: []forms.Option{forms.WithTag("json")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url encoded jsontime with tz",
|
||||||
|
r: makeRequest("urlenc.http"),
|
||||||
|
dest: &urlEncTestJT{},
|
||||||
|
compare: &UrlEncTestJT,
|
||||||
|
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseTimeInTZ(time.UTC), forms.WithTag("json")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url encoded jsontime with local",
|
||||||
|
r: makeRequest("urlenc.http"),
|
||||||
|
dest: &urlEncTestJT{},
|
||||||
|
compare: &UrlEncTestJTLocal,
|
||||||
|
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseLocalTime(), forms.WithTag("json")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sim real data",
|
||||||
|
r: makeRequest("urlenc2.http"),
|
||||||
|
dest: &alerting.Simulation{},
|
||||||
|
compare: realSim,
|
||||||
|
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseLocalTime()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := forms.Unmarshal(tc.r, tc.dest, tc.opts...)
|
||||||
|
if tc.expectErr != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, tc.expectErr.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.compare, tc.dest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
54
internal/forms/testdata/call1.http
vendored
Normal file
54
internal/forms/testdata/call1.http
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
POST /api/call-upload HTTP/1.1
|
||||||
|
Connection: Upgrade, HTTP2-Settings
|
||||||
|
Content-Length: 5372
|
||||||
|
Host: xenon:3050
|
||||||
|
HTTP2-Settings: AAEAAEAAAAIAAAAAAAMAAAAAAAQBAAAAAAUAAEAAAAYABgAA
|
||||||
|
Upgrade: h2c
|
||||||
|
Content-Type: multipart/form-data; boundary=--sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
User-Agent: sdrtrunk
|
||||||
|
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; name="key"
|
||||||
|
|
||||||
|
2b7c871b-abcd-defa-0123-456789abcdef
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; name="system"
|
||||||
|
|
||||||
|
197
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; name="dateTime"
|
||||||
|
|
||||||
|
1730377177
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; name="talkgroup"
|
||||||
|
|
||||||
|
2
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; name="source"
|
||||||
|
|
||||||
|
3237
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; name="frequency"
|
||||||
|
|
||||||
|
852637500
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; name="talkgroupLabel"
|
||||||
|
|
||||||
|
Wide Area 1 FD/EMS Intercity
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; name="talkgroupGroup"
|
||||||
|
|
||||||
|
Wide Area
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; name="systemLabel"
|
||||||
|
|
||||||
|
RISCON
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; name="patches"
|
||||||
|
|
||||||
|
[]
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||||
|
Content-Disposition: form-data; filename="20241031_081939.285.mp3"; name="audio"
|
||||||
|
|
||||||
|
ID3dotdotdotLAME3.98.4a
|
||||||
|
----sdrtrunk-sdrtrunk-sdrtrunk--
|
17
internal/forms/testdata/urlenc.http
vendored
Normal file
17
internal/forms/testdata/urlenc.http
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
POST /tgstats HTTP/1.1
|
||||||
|
Host: xenon:3050
|
||||||
|
Connection: keep-alive
|
||||||
|
Content-Length: 98
|
||||||
|
Cache-Control: max-age=0
|
||||||
|
Origin: http://xenon:3050
|
||||||
|
DNT: 1
|
||||||
|
Upgrade-Insecure-Requests: 1
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
|
||||||
|
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
|
||||||
|
Referer: http://xenon:3050/tgstats
|
||||||
|
Accept-Encoding: gzip, deflate
|
||||||
|
Accept-Language: en-US,en;q=0.9,fr;q=0.8
|
||||||
|
sec-gpc: 1
|
||||||
|
|
||||||
|
lookbackDays=7&halfLife=30m0s&recent=2h0m0s&simInterval=5m&scoreStart=2024-10-28T09%3A25&scoreEnd=
|
17
internal/forms/testdata/urlenc2.http
vendored
Normal file
17
internal/forms/testdata/urlenc2.http
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
POST /tgstats HTTP/1.1
|
||||||
|
Host: xenon:3050
|
||||||
|
Connection: keep-alive
|
||||||
|
Content-Length: 98
|
||||||
|
Cache-Control: max-age=0
|
||||||
|
Origin: http://xenon:3050
|
||||||
|
DNT: 1
|
||||||
|
Upgrade-Insecure-Requests: 1
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
|
||||||
|
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
|
||||||
|
Referer: http://xenon:3050/tgstats
|
||||||
|
Accept-Encoding: gzip, deflate
|
||||||
|
Accept-Language: en-US,en;q=0.9,fr;q=0.8
|
||||||
|
sec-gpc: 1
|
||||||
|
|
||||||
|
lookbackDays=7&halfLife=30m0s&recent=2h0m0s&simInterval=5m0s&scoreStart=2024-10-22T17%3A49&scoreEnd=2024-10-22T19%3A50
|
|
@ -101,7 +101,7 @@ func ParseAny(s string, opt ...dateparse.ParserOption) (Time, error) {
|
||||||
return Time(t), err
|
return Time(t), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseInLocal(s string, opt ...dateparse.ParserOption) (Time, error) {
|
func ParseLocal(s string, opt ...dateparse.ParserOption) (Time, error) {
|
||||||
t, err := dateparse.ParseIn(s, time.Now().Location(), opt...)
|
t, err := dateparse.ParseLocal(s, opt...)
|
||||||
return Time(t), err
|
return Time(t), err
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/forms"
|
||||||
"dynatron.me/x/stillbox/internal/jsontime"
|
"dynatron.me/x/stillbox/internal/jsontime"
|
||||||
"dynatron.me/x/stillbox/internal/trending"
|
"dynatron.me/x/stillbox/internal/trending"
|
||||||
cl "dynatron.me/x/stillbox/pkg/calls"
|
cl "dynatron.me/x/stillbox/pkg/calls"
|
||||||
|
@ -24,12 +24,12 @@ type Simulation struct {
|
||||||
config.Alerting
|
config.Alerting
|
||||||
|
|
||||||
// ScoreStart is the time when scoring begins
|
// ScoreStart is the time when scoring begins
|
||||||
ScoreStart jsontime.Time `json:"scoreStart"`
|
ScoreStart jsontime.Time `json:"scoreStart" yaml:"scoreStart" form:"scoreStart"`
|
||||||
// ScoreEnd is the time when the score simulator ends. Left blank, it defaults to time.Now()
|
// ScoreEnd is the time when the score simulator ends. Left blank, it defaults to time.Now()
|
||||||
ScoreEnd jsontime.Time `json:"scoreEnd"`
|
ScoreEnd jsontime.Time `json:"scoreEnd" yaml:"scoreEnd" form:"scoreEnd"`
|
||||||
|
|
||||||
// SimInterval is the interval at which the scorer will be called
|
// SimInterval is the interval at which the scorer will be called
|
||||||
SimInterval jsontime.Duration `json:"simInterval"`
|
SimInterval jsontime.Duration `json:"simInterval" yaml:"simInterval" form:"simInterval"`
|
||||||
|
|
||||||
clock offsetClock `json:"-"`
|
clock offsetClock `json:"-"`
|
||||||
*alerter `json:"-"`
|
*alerter `json:"-"`
|
||||||
|
@ -115,48 +115,12 @@ func (as *alerter) simulateHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
err := r.ParseForm()
|
err := forms.Unmarshal(r, s, forms.WithAcceptBlank(), forms.WithParseLocalTime())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("simulate form parse: %w", err)
|
err = fmt.Errorf("simulate unmarshal: %w", err)
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lbd, err := strconv.Atoi(r.Form["lookbackDays"][0])
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("lookbackDays parse: %w", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.LookbackDays = uint(lbd)
|
|
||||||
s.HalfLife, err = jsontime.ParseDuration(r.Form["halfLife"][0])
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("halfLife parse: %w", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.Recent, err = jsontime.ParseDuration(r.Form["recent"][0])
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("recent parse: %w", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.SimInterval, err = jsontime.ParseDuration(r.Form["simInterval"][0])
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("simInterval parse: %w", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.ScoreStart, err = jsontime.ParseInLocal(r.Form["scoreStart"][0])
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("scoreStart parse: %w", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.ScoreEnd, err = jsontime.ParseInLocal(r.Form["scoreEnd"][0])
|
|
||||||
if err != nil {
|
|
||||||
s.ScoreEnd = jsontime.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.verify()
|
err := s.verify()
|
||||||
|
|
|
@ -54,12 +54,12 @@ type RateLimit struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Alerting struct {
|
type Alerting struct {
|
||||||
Enable bool `yaml:"enable"`
|
Enable bool `yaml:"enable" form:"enable"`
|
||||||
LookbackDays uint `yaml:"lookbackDays"`
|
LookbackDays uint `yaml:"lookbackDays" form:"lookbackDays"`
|
||||||
HalfLife jsontime.Duration `yaml:"halfLife"`
|
HalfLife jsontime.Duration `yaml:"halfLife" form:"halfLife"`
|
||||||
Recent jsontime.Duration `yaml:"recent"`
|
Recent jsontime.Duration `yaml:"recent" form:"recent"`
|
||||||
AlertThreshold float64 `yaml:"alertThreshold"`
|
AlertThreshold float64 `yaml:"alertThreshold" form:"alertThreshold"`
|
||||||
Renotify *jsontime.Duration `yaml:"renotify,omitempty"`
|
Renotify *jsontime.Duration `yaml:"renotify,omitempty" form:"renotify,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notify []NotifyService
|
type Notify []NotifyService
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
package sources
|
package sources
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/common"
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
|
"dynatron.me/x/stillbox/internal/forms"
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
"dynatron.me/x/stillbox/pkg/gordio/auth"
|
"dynatron.me/x/stillbox/pkg/gordio/auth"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
@ -40,7 +37,7 @@ func (h *RdioHTTP) InstallPublicRoutes(r chi.Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type callUploadRequest struct {
|
type callUploadRequest struct {
|
||||||
Audio []byte `form:"audio"`
|
Audio []byte `form:"audio" filenameField:"AudioName"`
|
||||||
AudioName string
|
AudioName string
|
||||||
AudioType string `form:"audioType"`
|
AudioType string `form:"audioType"`
|
||||||
DateTime time.Time `form:"dateTime"`
|
DateTime time.Time `form:"dateTime"`
|
||||||
|
@ -115,7 +112,7 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cur := new(callUploadRequest)
|
cur := new(callUploadRequest)
|
||||||
err = cur.fill(r)
|
err = forms.Unmarshal(r, cur, forms.WithAcceptBlank())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "cannot bind upload "+err.Error(), http.StatusExpectationFailed)
|
http.Error(w, "cannot bind upload "+err.Error(), http.StatusExpectationFailed)
|
||||||
return
|
return
|
||||||
|
@ -141,80 +138,3 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Error().Err(err).Int("written", written).Msg("upload response failed")
|
log.Error().Err(err).Int("written", written).Msg("upload response failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (car *callUploadRequest) fill(r *http.Request) error {
|
|
||||||
rv := reflect.ValueOf(car).Elem()
|
|
||||||
rt := rv.Type()
|
|
||||||
|
|
||||||
for i := 0; i < rv.NumField(); i++ {
|
|
||||||
f := rv.Field(i)
|
|
||||||
fi := f.Interface()
|
|
||||||
formField, has := rt.Field(i).Tag.Lookup("form")
|
|
||||||
if !has {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch v := fi.(type) {
|
|
||||||
case []byte:
|
|
||||||
file, hdr, err := r.FormFile(formField)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get form file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
car.AudioName = hdr.Filename
|
|
||||||
audioBytes, err := io.ReadAll(file)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("file read: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.SetBytes(audioBytes)
|
|
||||||
case time.Time:
|
|
||||||
tval := r.Form.Get(formField)
|
|
||||||
if iv, err := strconv.Atoi(tval); err == nil {
|
|
||||||
f.Set(reflect.ValueOf(time.Unix(int64(iv), 0)))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
t, err := time.Parse(time.RFC3339, tval)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parse time: %w", err)
|
|
||||||
}
|
|
||||||
f.Set(reflect.ValueOf(t))
|
|
||||||
case []int:
|
|
||||||
val := strings.Trim(r.Form.Get(formField), "[]")
|
|
||||||
if val == "" {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.Set(reflect.ValueOf(ar))
|
|
||||||
case int:
|
|
||||||
ff := r.Form.Get(formField)
|
|
||||||
val, err := strconv.Atoi(ff)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("atoi('%s'): %w", ff, err)
|
|
||||||
}
|
|
||||||
f.SetInt(int64(val))
|
|
||||||
case string:
|
|
||||||
f.SetString(r.Form.Get(formField))
|
|
||||||
case bool:
|
|
||||||
ff := r.Form.Get(formField)
|
|
||||||
if ff == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val, err := strconv.ParseBool(ff)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsebool('%s'): %w", ff, err)
|
|
||||||
}
|
|
||||||
f.SetBool(val)
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("unsupported type %T", v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue