-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implements decoder and payload validator (#8)
* 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
Showing
10 changed files
with
1,059 additions
and
161 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.