Skip to content

Commit

Permalink
Initial
Browse files Browse the repository at this point in the history
  • Loading branch information
tmus committed Jul 21, 2019
0 parents commit 7192d37
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 0 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/gostalt/validate

go 1.12
9 changes: 9 additions & 0 deletions message.go
Original file line number Diff line number Diff line change
@@ -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"`
}
15 changes: 15 additions & 0 deletions rule.go
Original file line number Diff line number Diff line change
@@ -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
}
79 changes: 79 additions & 0 deletions validation.go
Original file line number Diff line number Diff line change
@@ -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...)
}
142 changes: 142 additions & 0 deletions validation_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit 7192d37

Please sign in to comment.