Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http/req: flatten #82

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading