Skip to content

Commit

Permalink
http/req: moves parsing/validating functionality into ParseBody and P…
Browse files Browse the repository at this point in the history
…arseQueryParams
  • Loading branch information
DavidLarsKetch committed Jun 25, 2024
1 parent 44f0b07 commit 64ce86a
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 53 deletions.
29 changes: 6 additions & 23 deletions http/req/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,18 @@ import (
"github.com/xy-planning-network/trails"
)

type queryParamDecoder struct {
decoder *schema.Decoder
}

func newQueryParamDecoder() queryParamDecoder {
func newQueryParamDecoder() *schema.Decoder {
dec := schema.NewDecoder()
dec.IgnoreUnknownKeys(true)

return queryParamDecoder{dec}
return dec
}

// Decode translates src into dst.
// Upon success, dst holds all values in src that match to fields in dst.
//
// On failure, Decode can return a host of errors.
// Some errors are issues with calling code;
// translateDecoderError converts an error returned by *schema.Decoder into standardized errors.
// Some *schema.Decoder errors are issues with calling code;
// some errors are unexpected issues;
// still some are issues with src's keys or values not matching dst.
// In the last case,
func (q queryParamDecoder) decode(dst any, src map[string][]string) error {
err := q.decoder.Decode(dst, src)
if err == nil {
return nil
}

if err.Error() == "schema: interface must be a pointer to struct" {
return fmt.Errorf("%w: called with non-pointer: %s", trails.ErrBadAny, err)
}

// still some are issues with mismatches between a request's query params and the expected shape.
func translateDecoderError(err error) error {
var pkgErrs schema.MultiError
// NOTE(dlk): In testing the schema package, outside other errors handled above,
// the package appears to always use MultiError to wrap errors up.
Expand Down
55 changes: 46 additions & 9 deletions http/req/req.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ import (
"io"
"net/url"

v10 "github.com/go-playground/validator/v10"
"github.com/gorilla/schema"
"github.com/xy-planning-network/trails"
)

const (
failedValidationTmpl = "trails/http/req: %T failed validation: %w"
)

type Parser struct {
queryParamDecoder queryParamDecoder
validator
queryParamDecoder *schema.Decoder
validator *v10.Validate
}

func NewParser() *Parser {
Expand All @@ -29,18 +35,28 @@ func NewParser() *Parser {
// ParseBody reads the entire r.Body and can't be read from again.
// Use a [io.TeeReader] if r.Body needs to be reused after calling ParseBody.
func (p *Parser) ParseBody(body io.Reader, structPtr any) error {
var ourFault *json.InvalidUnmarshalError
err := json.NewDecoder(body).Decode(structPtr)

var ourFault *json.InvalidUnmarshalError
if errors.As(err, &ourFault) {
return fmt.Errorf("trails/http/req: %w: ParseBody called with non-pointer: %s", trails.ErrBadAny, err)
return fmt.Errorf("trails/http/req: %w: ParseBody called with non-pointer", trails.ErrBadAny)
}

if err != nil {
return fmt.Errorf("trails/http/req: %w: failed decoding request body: %s", trails.ErrBadFormat, err)
}

if err := p.validate(structPtr); err != nil {
return fmt.Errorf("trails/http/req: %T failed validation: %w", structPtr, err)
err = p.validator.Struct(structPtr)

var verrs v10.ValidationErrors
if errors.As(err, &verrs) {
err = translateValidationErrors(verrs)
return fmt.Errorf(failedValidationTmpl, structPtr, err)
}

if err != nil {
// NOTE(dlk): return raw v10.Validator errors until use cases show up for parsing these differently.
return err
}

return nil
Expand All @@ -50,12 +66,33 @@ func (p *Parser) ParseBody(body io.Reader, structPtr any) error {
// If successful, ParseQueryParams runs validation against the contents,
// returning an ErrNotValid if the data fails validation rules.
func (p *Parser) ParseQueryParams(params url.Values, structPtr any) error {
if err := p.queryParamDecoder.decode(structPtr, params); err != nil {
err := p.queryParamDecoder.Decode(structPtr, params)
if err != nil {
if err.Error() == "schema: interface must be a pointer to struct" {
err = fmt.Errorf("trails/http/req: %w: ParseQueryParams called with non-pointer", trails.ErrBadAny)

return err
}

err = translateDecoderError(err)
if errors.Is(err, trails.ErrNotValid) {
return fmt.Errorf(failedValidationTmpl, structPtr, err)
}

return fmt.Errorf("trails/http/req: failed decoding request query params: %w", err)
}

if err := p.validate(structPtr); err != nil {
return fmt.Errorf("trails/http/req: %T failed validation: %w", structPtr, err)
err = p.validator.Struct(structPtr)

var verrs v10.ValidationErrors
if errors.As(err, &verrs) {
err = translateValidationErrors(verrs)
return fmt.Errorf(failedValidationTmpl, structPtr, err)
}

if err != nil {
// NOTE(dlk): return raw v10.Validator errors until use cases show up for parsing these differently.
return err
}

return nil
Expand Down
25 changes: 4 additions & 21 deletions http/req/validate.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
package req

import (
"errors"
"reflect"
"strings"

v10 "github.com/go-playground/validator/v10"
"github.com/xy-planning-network/trails"
)

type validator struct {
valid *v10.Validate
}

// newValidator constructs a validator, which applies default configuration.
func newValidator() validator {
func newValidator() *v10.Validate {
v := v10.New()
v.RegisterValidation("enum", validateEnumerable)
v.RegisterTagNameFunc(func(field reflect.StructField) string {
Expand All @@ -34,24 +29,12 @@ func newValidator() validator {
return name
})

return validator{v}
return v
}

// validate checks the fields on structPtr match the rules set by "validate" struct tags.
// On success, validate returns no error.
// On failure, validate translates each issue to a ValidationError,
// translateValidationErrors converts each issue into a ValidationError,
// returning them all as ValidationErrors.
func (v validator) validate(structPtr any) error {
err := v.valid.Struct(structPtr)
if err == nil {
return nil
}

var errs v10.ValidationErrors
if !errors.As(err, &errs) {
return err
}

func translateValidationErrors(errs v10.ValidationErrors) error {
var validateErrs ValidationErrors
for _, ve := range errs {
field := ve.Namespace()
Expand Down

0 comments on commit 64ce86a

Please sign in to comment.