From 7192d3790d30d921e82ac5e68074624f6050257a Mon Sep 17 00:00:00 2001 From: Tom Smith Date: Sun, 21 Jul 2019 13:39:02 +0100 Subject: [PATCH] Initial --- go.mod | 3 + message.go | 9 +++ rule.go | 15 +++++ validation.go | 79 +++++++++++++++++++++++++ validation_test.go | 142 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 248 insertions(+) create mode 100644 go.mod create mode 100644 message.go create mode 100644 rule.go create mode 100644 validation.go create mode 100644 validation_test.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cd04d5f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/gostalt/validate + +go 1.12 diff --git a/message.go b/message.go new file mode 100644 index 0000000..87a4590 --- /dev/null +++ b/message.go @@ -0,0 +1,9 @@ +package validate + +// Message represents a failed validation. It contains details +// of the param that failed, as well as the error message from +// the rule that caused it to fail. +type Message struct { + Error string `json:"error"` + Param string `json:"param"` +} diff --git a/rule.go b/rule.go new file mode 100644 index 0000000..97698a8 --- /dev/null +++ b/rule.go @@ -0,0 +1,15 @@ +package validate + +import ( + "net/http" +) + +// Rule represents a check to run on a request. +type Rule struct { + // Param is the field in the Request to check. + Param string + // Check is a callback that is ran on the request. The + // second argument is the param to check. If the check + // fails, an error should be returned with details why. + Check func(*http.Request, string) error +} diff --git a/validation.go b/validation.go new file mode 100644 index 0000000..d6d98c2 --- /dev/null +++ b/validation.go @@ -0,0 +1,79 @@ +package validate + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" +) + +// Validator is responsible for collecting an http.Request and +// a list of rules and checking that the rules are satisfied +// by the given request. +type Validator struct { + request *http.Request + Rules []Rule +} + +// Respond is a helper method that writes the errors to the given +// http.ResponseWriter. This also sets an appropriate HTTP header +// and sets the content-type to JSON. +func Respond(w http.ResponseWriter, m []Message) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnprocessableEntity) + + eb := make(map[string][]Message) + eb["errors"] = m + + d, _ := json.Marshal(eb) + w.Write(d) +} + +// Check is an all-in-one method of creating and running a new +// Validator. If there is no logic around adding rules, it is +// the easiest way to run a Validator. +func Check(r *http.Request, rule ...Rule) ([]Message, error) { + return Make(r, rule...).Run() +} + +// Make creates a new Validator based on the request and rules +// passed into it. The rules argument is optional. Rules can +// be added by calling `Add` on the returned Validator. +func Make(r *http.Request, rule ...Rule) *Validator { + return &Validator{ + request: r, + Rules: rule, + } +} + +// Run determines if the given rules are satisfied by the request. +// A "perfect" outcome is `nil, nil`. +func (v *Validator) Run() ([]Message, error) { + if len(v.Rules) == 0 { + return nil, fmt.Errorf("no rules defined on validator") + } + + // The number of messages can't exceed the number of rules, + // so define an upper limit here for speed. + vm := make([]Message, 0, len(v.Rules)) + + for _, rule := range v.Rules { + if err := rule.Check(v.request, rule.Param); err != nil { + vm = append(vm, Message{ + Error: err.Error(), + Param: rule.Param, + }) + } + } + + if len(vm) > 0 { + return vm, errors.New("validation failed") + } + + return nil, nil +} + +// Add adds an additional set of rules to the Validator. +func (v *Validator) Add(rules ...Rule) { + v.Rules = append(v.Rules, rules...) +} diff --git a/validation_test.go b/validation_test.go new file mode 100644 index 0000000..95439cc --- /dev/null +++ b/validation_test.go @@ -0,0 +1,142 @@ +package validate + +import ( + "errors" + "fmt" + "net/http" + "testing" +) + +func TestCheckCreatesValidatorAndRunsIt(t *testing.T) { + r, _ := http.NewRequest("GET", "localhost", nil) + + rule := Rule{ + Param: "forename", + Check: func(r *http.Request, param string) error { + return errors.New("fail") + }, + } + + rule2 := Rule{ + Param: "forename", + Check: func(r *http.Request, param string) error { + return errors.New("fail") + }, + } + + msgs, _ := Check(r, rule, rule2) + if len(msgs) == 0 { + fmt.Println("expected an error, didn't get one") + t.FailNow() + } +} + +func TestMakeReturnsErrorWithNoRules(t *testing.T) { + r, _ := http.NewRequest("GET", "localhost", nil) + + validator := Make(r) + + if _, err := validator.Run(); err == nil { + fmt.Println("no error returned from empty validator") + t.FailNow() + } +} + +func TestCanAddRules(t *testing.T) { + r, _ := http.NewRequest("GET", "localhost", nil) + + validator := Make(r) + + rule := Rule{} + + validator.Add(rule) + + if len(validator.Rules) == 0 { + fmt.Println("validator empty, expected 1 rule") + t.FailNow() + } +} + +func TestFailureReturnsValidationMessage(t *testing.T) { + r, _ := http.NewRequest("GET", "localhost", nil) + + validator := Make(r) + + rule := Rule{ + Param: "forename", + Check: func(r *http.Request, param string) error { + return errors.New("fail") + }, + } + + validator.Add(rule) + + messages, _ := validator.Run() + + if len(messages) == 0 { + fmt.Println("expected a validation message, got none") + t.FailNow() + } +} + +func TestValidatorRunsRuleCheck(t *testing.T) { + r, _ := http.NewRequest("GET", "localhost", nil) + + validator := Make(r) + + rule := Rule{ + Param: "forename", + Check: func(r *http.Request, param string) error { + return errors.New("forced failure") + }, + } + + validator.Add(rule) + + if _, err := validator.Run(); err == nil { + fmt.Println("expected validator to fail. It didn't.") + t.FailNow() + } +} + +func TestValidatorReturnsRuleCheckErrorMessage(t *testing.T) { + r, _ := http.NewRequest("GET", "localhost", nil) + + validator := Make(r) + + rule := Rule{ + Param: "forename", + Check: func(r *http.Request, param string) error { + return errors.New("forced failure") + }, + } + + validator.Add(rule) + + messages, _ := validator.Run() + if messages[0].Error != "forced failure" { + fmt.Println("expected the message to contain the rule error. It didn't.") + t.FailNow() + } +} + +func TestValidatorReturnsParamInError(t *testing.T) { + r, _ := http.NewRequest("GET", "localhost", nil) + + validator := Make(r) + + rule := Rule{ + Param: "forename", + Check: func(r *http.Request, param string) error { + return errors.New("forced failure") + }, + } + + validator.Add(rule) + + messages, _ := validator.Run() + if messages[0].Param != "forename" { + fmt.Println("expected the message to contain the param. It didn't.") + t.FailNow() + } +}