Skip to content

Commit

Permalink
feat: implements decoder and payload validator (#8)
Browse files Browse the repository at this point in the history
* feat(helpers): adds Filter helper

* feat(parser): implements new struct methods

- `GetTag(reflect.StructField, string) map[string]string`
- `SetValuesFromMap(any, map[string]any)`
- `SetValuesFromBytes(any, []byte)`
- `RemoveValuesFromTag(string, []string, reflect.StructField) string`

* feat(structs): implements Decode() function

- adds `Decode([]byte, any, DecoderOptions) map[string][]string`

* feat(validators): implements ValidatePayload() function

- adds `ValidatePayload([]byte, any, ValidationOptions) map[string][]string`
  • Loading branch information
oleoneto authored Nov 9, 2022
1 parent 701f828 commit a449f29
Show file tree
Hide file tree
Showing 10 changed files with 1,059 additions and 161 deletions.
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,13 @@ go 1.18

require (
github.com/google/uuid v1.3.0
github.com/invopop/jsonschema v0.7.0
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/text v0.4.0
)

require (
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
)
18 changes: 18 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk=
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy770So=
github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 h1:Ko2LQMrRU+Oy/+EDBwX7eZ2jp3C47eDBB8EIhKTun+I=
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
162 changes: 162 additions & 0 deletions structs/decoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package structs

import (
"regexp"
"strings"

"github.com/invopop/jsonschema"
"github.com/xeipuuv/gojsonschema"
)

type SchemaValidationRule string

type DecoderOptions struct {
// Set of rules that should be checked when validation the provided data against the Go struct.
Rules []SchemaValidationRule

// A function that runs before the decoder starts processing the data.
// This could be used for setting/unsetting values in the provided bytes array.
BeforeHook func(data []byte, model any) []byte

// A function that runs after the decoder is done processing the data.
// This could be used for ignoring certain errors or providing custom error messages.
AfterHook func(validations map[string][]string) map[string][]string
}

const (
ADDITIONAL_PROPERTY SchemaValidationRule = "additional_property_not_allowed"
REQUIRED_ATTRIBUTE SchemaValidationRule = "required"
INVALID_TYPE SchemaValidationRule = "invalid_type"
)

var DecodingErrors = map[string]string{
"required": "REQUIRED_ATTRIBUTE_MISSING",
"invalid_payload": "INVALID_PAYLOAD",
"invalid_type": "INVALID_TYPE",
"additional_property_not_allowed": "ADDITIONAL_PROPERTY",
}

// Replacement for the standard `json.Unmarshal` implementation.
// It deserializes a JSON object into a Go struct. This function does not
// Panic when the value for a JSON field is incompatible with the type set in the struct.
//
// You're allowed to pass a list of `SchemaValidationType` to use while deserializing the JSON payload
// into your struct. They are:
// - `ATTRIBUTE_MUST_BE_PRESENT`:
// checks if a required field is absent from the JSON payload.
// - `ADDITIONAL_PROPERTY`:
// checks if an unknown field was passed in the JSON payload.
// - `INVALID_TYPE`:
// checks if the type of a JSON attribute in the payload is compatible with the underlying type of the Go struct field.
//
//
// Usage:
//
// type User struct {
// Id int `json:"id"`
// Name string `json:"name" validate:"is_present"`
// Emails []string `json:"emails,omitempty"`
// }
//
// payload := []byte(`{"name": 42, "emails": ["[email protected]", 0]}`)
// parsedValues, errs := Decode(payload, User{}, options)
// /*
// expected errors:
// [
// "id - REQUIRED_ATTRIBUTE_MISSING",
// "name - INVALID_DATA_TYPE",
// "emails[1] - INVALID_DATA_TYPE"
// ]
// */
func Decode(data []byte, model any, options DecoderOptions) map[string][]string {
validations := make(map[string][]string, 0)

if options.BeforeHook != nil {
data = options.BeforeHook(data, model)
}

SetValuesFromBytes(model, data)

afterFunc := func(validations map[string][]string) map[string][]string {
return validations
}

if options.AfterHook != nil {
afterFunc = options.AfterHook
}

if len(data) == 0 || len(options.Rules) == 0 {
return afterFunc(validations)
}

reflector := new(jsonschema.Reflector)
reflector.RequiredFromJSONSchemaTags = true
reflector.AllowAdditionalProperties = !Contains(options.Rules, ADDITIONAL_PROPERTY)

schema := reflector.Reflect(model)
decoded, _ := schema.MarshalJSON()

result, verr := gojsonschema.Validate(
gojsonschema.NewBytesLoader(decoded),
gojsonschema.NewBytesLoader(data),
)

if verr != nil {
validations["_"] = []string{DecodingErrors["invalid_payload"]}
return afterFunc(validations)
}

res := Filter(result.Errors(), func(index int, err gojsonschema.ResultError) bool {
return Contains(options.Rules, SchemaValidationRule(err.Type()))
})

for _, err := range res {
name := jsonAttributeName(err.String())
normalizedName := regexp.MustCompile(`\[\d+\]`).ReplaceAllString(name, "")
validations[normalizedName] = []string{DecodingErrors[err.Type()]}
}

return afterFunc(validations)
}

func jsonAttributeName(str string) string {
pattern := regexp.MustCompile(`\.([0-9]+)`)
scope := strings.Split(str, ": ")[0]
scope = pattern.ReplaceAllString(scope, "[$1]")

if scope == "(root)" {
scope = ""
}

if strings.Contains(str, "Additional property") {
/*
format:
- (root): Additional property extra is not allowed
*/
p := regexp.MustCompile(`Additional property (.*) is not allowed`)
name := p.FindStringSubmatch(str)[1]
return name
}

if strings.Contains(str, "required") {
/*
format:
- (root): field_name is required
- parent.0: field_name is required
*/
str = pattern.ReplaceAllString(str, "[$1]")
name := strings.Split(strings.Trim(strings.SplitAfter(str, ":")[1], " "), " ")
return strings.TrimPrefix(strings.Join([]string{scope, name[0]}, "."), ".")
}

if strings.Contains(str, "Invalid type") {
/*
for format:
- field_name. Expected: typeA, given: typeB
- field_name.0. Expected: typeA, given: typeB
*/
return scope
}

return str
}
Loading

0 comments on commit a449f29

Please sign in to comment.