diff --git a/.github/workflows/scripts/golangci.yaml b/.github/workflows/scripts/golangci.yaml index a0bd1e6..dfa19b8 100644 --- a/.github/workflows/scripts/golangci.yaml +++ b/.github/workflows/scripts/golangci.yaml @@ -25,6 +25,7 @@ linters: - musttag - nilnil - noctx + - paralleltest - perfsprint - prealloc - predeclared diff --git a/forms.go b/forms.go index b55450e..fd60efa 100644 --- a/forms.go +++ b/forms.go @@ -22,7 +22,21 @@ var ( ErrParseFailure = errors.New("could not parse value") ) -func Parse(data url.Values, schema Schema) error { +// Parse uses the given Schema to parse the HTTP form values in the given HTTP +// Request. If the values of the form do not match the schema, or required values +// are missing, an error is returned. +func Parse(r *http.Request, schema Schema) error { + if err := r.ParseForm(); err != nil { + return err + } + + return ParseValues(r.Form, schema) +} + +// ParseValues uses the given Schema to parse the values in the given url.Values. +// If the values do not match the schema, or required values are missing, an +// error is returned. +func ParseValues(data url.Values, schema Schema) error { for name, parser := range schema { values := data[name] if err := parser.Parse(values); err != nil { @@ -32,14 +46,6 @@ func Parse(data url.Values, schema Schema) error { return nil } -func ParseForm(r *http.Request, schema Schema) error { - if err := r.ParseForm(); err != nil { - return err - } - - return Parse(r.Form, schema) -} - // A Schema describes how a set of url.Values should be parsed. // Typically these are coming from an http.Request.Form from inside an // http.Handler responding to an inbound request. @@ -54,31 +60,18 @@ type Parser interface { Parse([]string) error } -// String is used to extract a form data value into a Go string. If the value -// is not a string or is missing then an error is returned during parsing. -func String(s *string) Parser { - return &stringParser{ - required: true, - destination: s, - } +// StringType represents any type compatible with the Go string built-in type, +// to be used as a destination for writing the value of an environment variable. +type StringType interface { + ~string } -// StringOr is used to extract a form data value into a Go string. If the value -// is missing, then the alt value is used instead. -func StringOr(s *string, alt string) Parser { - *s = alt - return &stringParser{ - required: false, - destination: s, - } -} - -type stringParser struct { +type stringParser[T StringType] struct { required bool - destination *string + destination *T } -func (p *stringParser) Parse(values []string) error { +func (p *stringParser[T]) Parse(values []string) error { switch { case len(values) > 1: return ErrMulitpleValues @@ -87,11 +80,30 @@ func (p *stringParser) Parse(values []string) error { case len(values) == 0: return nil default: - *p.destination = values[0] + *p.destination = T(values[0]) } return nil } +// String is used to extract a form data value into a Go string. If the value +// is not a string or is missing then an error is returned during parsing. +func String[T StringType](s *T) Parser { + return &stringParser[T]{ + required: true, + destination: s, + } +} + +// StringOr is used to extract a form data value into a Go string. If the value +// is missing, then the alt value is used instead. +func StringOr[T StringType](s *T, alt T) Parser { + *s = alt + return &stringParser[T]{ + required: false, + destination: s, + } +} + // Secret is used to extract a form data value into a Go conceal.Text. If the // value is missing then an error is returned during parsing. func Secret(s **conceal.Text) Parser { @@ -121,31 +133,19 @@ func (p *secretParser) Parse(values []string) error { return nil } -type intParser struct { - required bool - destination *int -} - -// Int is used to extract a form data value into a Go int. If the value is not -// an int or is missing then an error is returned during parsing. -func Int(i *int) Parser { - return &intParser{ - required: true, - destination: i, - } +// IntType represents any type compatible with the Go integer built-in types, +// to be used as a destination for writing the value of a form value. +type IntType interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 } -// IntOr is used to extract a form data value into a Go int. If the value is -// missing, then the alt value is used instead. -func IntOr(i *int, alt int) Parser { - *i = alt - return &intParser{ - required: false, - destination: i, - } +type intParser[T IntType] struct { + required bool + destination *T } -func (p *intParser) Parse(values []string) error { +func (p *intParser[T]) Parse(values []string) error { switch { case len(values) > 1: return ErrMulitpleValues @@ -160,10 +160,29 @@ func (p *intParser) Parse(values []string) error { return err } - *p.destination = i + *p.destination = T(i) return nil } +// Int is used to extract a form data value into a Go int. If the value is not +// an int or is missing then an error is returned during parsing. +func Int[T IntType](i *T) Parser { + return &intParser[T]{ + required: true, + destination: i, + } +} + +// IntOr is used to extract a form data value into a Go int. If the value is +// missing, then the alt value is used instead. +func IntOr[T IntType](i *T, alt T) Parser { + *i = alt + return &intParser[T]{ + required: false, + destination: i, + } +} + type floatParser struct { required bool destination *float64 diff --git a/forms_test.go b/forms_test.go index 5acc7ed..d105ae3 100644 --- a/forms_test.go +++ b/forms_test.go @@ -14,6 +14,8 @@ import ( ) func Test_Parse_singles(t *testing.T) { + t.Parallel() + data := url.Values{ "one": []string{"1"}, "two": []string{"2"}, @@ -30,7 +32,7 @@ func Test_Parse_singles(t *testing.T) { five *conceal.Text ) - err := Parse(data, Schema{ + err := ParseValues(data, Schema{ "one": String(&one), "two": Int(&two), "three": Float(&three), @@ -46,6 +48,8 @@ func Test_Parse_singles(t *testing.T) { } func Test_Parse_singles_Or(t *testing.T) { + t.Parallel() + data := url.Values{ "string1": []string{"hi"}, "string2": nil, @@ -64,7 +68,7 @@ func Test_Parse_singles_Or(t *testing.T) { b1, b2 bool ) - err := Parse(data, Schema{ + err := ParseValues(data, Schema{ "string1": StringOr(&s1, "X"), "string2": StringOr(&s2, "X"), "int1": IntOr(&i1, 3), @@ -87,6 +91,8 @@ func Test_Parse_singles_Or(t *testing.T) { } func Test_Parse_HTMLForm(t *testing.T) { + t.Parallel() + ctx := context.Background() request, err := http.NewRequestWithContext(ctx, http.MethodPost, "/", nil) must.NoError(t, err) @@ -104,7 +110,7 @@ func Test_Parse_HTMLForm(t *testing.T) { four bool ) - err2 := ParseForm(request, Schema{ + err2 := Parse(request, Schema{ "one": String(&one), "two": Int(&two), "three": Float(&three), @@ -118,6 +124,8 @@ func Test_Parse_HTMLForm(t *testing.T) { } func Test_Parse_HTMLForm_optional(t *testing.T) { + t.Parallel() + ctx := context.Background() request, err := http.NewRequestWithContext(ctx, http.MethodPost, "/", nil) must.NoError(t, err) @@ -130,7 +138,7 @@ func Test_Parse_HTMLForm_optional(t *testing.T) { two string ) - err2 := ParseForm(request, Schema{ + err2 := Parse(request, Schema{ "one": String(&one), "two": StringOr(&two, "alternate"), }) @@ -140,6 +148,8 @@ func Test_Parse_HTMLForm_optional(t *testing.T) { } func Test_Parse_HTMLForm_not_ready(t *testing.T) { + t.Parallel() + ctx := context.Background() request, err := http.NewRequestWithContext(ctx, http.MethodPost, "/", nil) must.NoError(t, err) @@ -147,104 +157,194 @@ func Test_Parse_HTMLForm_not_ready(t *testing.T) { var one string // not yet a valid form, never had the FormValues field set - err2 := ParseForm(request, Schema{ + err2 := Parse(request, Schema{ "one": String(&one), }) must.Error(t, err2) } func Test_Parse_key_missing(t *testing.T) { + t.Parallel() + data := url.Values{ "one": []string{"1"}, } var two int - err := Parse(data, Schema{ + err := ParseValues(data, Schema{ "two": Int(&two), }) must.Error(t, err) } func Test_Parse_string_value_missing(t *testing.T) { + t.Parallel() + data := url.Values{ "one": []string{}, } var one string - err := Parse(data, Schema{ + err := ParseValues(data, Schema{ "one": String(&one), }) must.Error(t, err) } func Test_Parse_int_value_missing(t *testing.T) { + t.Parallel() + data := url.Values{ "two": []string{}, } var two int - err := Parse(data, Schema{ + err := ParseValues(data, Schema{ "two": Int(&two), }) must.Error(t, err) } func Test_Parse_int_malformed(t *testing.T) { + t.Parallel() + data := url.Values{ "two": []string{"not an int"}, } var two int - err := Parse(data, Schema{ + err := ParseValues(data, Schema{ "two": Int(&two), }) must.Error(t, err) } func Test_Parse_float_value_missing(t *testing.T) { + t.Parallel() + data := url.Values{ "three": []string{}, } var three float64 - err := Parse(data, Schema{ + err := ParseValues(data, Schema{ "three": Float(&three), }) must.Error(t, err) } func Test_Parse_float_malformed(t *testing.T) { + t.Parallel() + data := url.Values{ "three": []string{"not a float"}, } var three float64 - err := Parse(data, Schema{ + err := ParseValues(data, Schema{ "three": Float(&three), }) must.Error(t, err) } func Test_Parse_bool_value_missing(t *testing.T) { + t.Parallel() + data := url.Values{ "four": []string{}, } var four bool - err := Parse(data, Schema{ + err := ParseValues(data, Schema{ "four": Bool(&four), }) must.Error(t, err) } func Test_Parse_bool_malformed(t *testing.T) { + t.Parallel() + data := url.Values{ "four": []string{"not a bool"}, } var four bool - err := Parse(data, Schema{ + err := ParseValues(data, Schema{ "four": Bool(&four), }) must.Error(t, err) } + +func Test_Parse_StringType_String(t *testing.T) { + t.Parallel() + + data := url.Values{ + "user": []string{"bob"}, + } + + type username string + + var user username + + err := ParseValues(data, Schema{ + "user": String(&user), + }) + must.NoError(t, err) + must.Eq(t, "bob", user) +} + +func Test_Parse_StringType_StringOr(t *testing.T) { + t.Parallel() + + data := url.Values{ + "foo": []string{"bar"}, + } + + type username string + + var user username + var fallback username = "alice" + + err := ParseValues(data, Schema{ + "user": StringOr(&user, fallback), + }) + must.NoError(t, err) + must.Eq(t, "alice", user) +} + +func Test_Parse_IntType_Int(t *testing.T) { + t.Parallel() + + data := url.Values{ + "age": []string{"34"}, + } + + type years int + + var age years + + err := ParseValues(data, Schema{ + "age": Int(&age), + }) + must.NoError(t, err) + must.Eq(t, 34, age) +} + +func Test_Parse_IntType_IntOr(t *testing.T) { + t.Parallel() + + data := url.Values{ + "foo": []string{"bar"}, + } + + type years int + + var age years + var fallback years = 100 + + err := ParseValues(data, Schema{ + "age": IntOr(&age, fallback), + }) + must.NoError(t, err) + must.Eq(t, 100, age) +}