Skip to content
This repository has been archived by the owner on May 11, 2022. It is now read-only.

Commit

Permalink
issue #64: prepare validation to move it to subpackage of domain
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilsk committed Jan 30, 2018
1 parent 22669dd commit 1b9aa4c
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 113 deletions.
73 changes: 73 additions & 0 deletions domain/error.go
@@ -0,0 +1,73 @@
package domain

import "fmt"

// AccumulatedError represents an error related to invalid input values.
type AccumulatedError interface {
error

// InputWithErrors returns map of form inputs and their errors.
InputWithErrors() map[Input][]error
}

type validationError struct {
single bool
position int
value string
message string
}

func (err validationError) Error() string {
if err.single {
if err.value != "" {
return fmt.Sprintf("value %q at position %d is invalid: %s", err.value, err.position, err.message)
}
return fmt.Sprintf("value at position %d is invalid: %s", err.position, err.message)
}
return err.message
}

type dataValidationError struct {
dataValidationResult
}

func (dataValidationError) Error() string {
return "validation error"
}

func (err dataValidationError) InputWithErrors() map[Input][]error {
m := make(map[Input][]error, len(err.results))
for _, r := range err.results {
m[r.input] = r.errors
}
return m
}

type dataValidationResult struct {
results []inputValidationResult
}

// AsError converts the result into error if it contains at least one input validation error.
func (r dataValidationResult) AsError() AccumulatedError {
for _, sub := range r.results {
if sub.HasError() {
return dataValidationError{r}
}
}
return nil
}

type inputValidationResult struct {
input Input
errors []error
}

// HasError returns true if the result contains at least one, not nil error.
func (r inputValidationResult) HasError() bool {
for _, err := range r.errors {
if err != nil {
return true
}
}
return false
}
9 changes: 9 additions & 0 deletions domain/input.go
@@ -1,5 +1,14 @@
package domain

const (
// EmailType specifies `<input type="email">`.
EmailType = "email"
// HiddenType specifies `<input type="hidden">`
HiddenType = "hidden"
// TextType specifies `<input type="text">`.
TextType = "text"
)

// Input represents an element of an HTML form.
type Input struct {
ID string `json:"id,omitempty" yaml:"id,omitempty" xml:"id,attr,omitempty"`
Expand Down
35 changes: 7 additions & 28 deletions domain/schema.go
Expand Up @@ -17,7 +17,8 @@ type Schema struct {
}

// Apply uses filtration, normalization, and validation for input values.
func (s *Schema) Apply(data map[string][]string) (map[string][]string, ValidationError) {
// It can raise the panic if the input type is unsupported.
func (s *Schema) Apply(data map[string][]string) (map[string][]string, AccumulatedError) {
data, err := s.Validate(s.Normalize(s.Filter(data)))
for i, input := range s.Inputs {
if values, found := data[input.Name]; found && len(values) > 0 {
Expand All @@ -38,7 +39,7 @@ func (s Schema) Filter(data map[string][]string) map[string][]string {
}
filtered := make(map[string][]string)
for name, values := range data {
if _, ok := index[name]; ok {
if _, found := index[name]; found {
filtered[name] = values
}
}
Expand Down Expand Up @@ -78,34 +79,12 @@ func (s Schema) Normalize(data map[string][]string) map[string][]string {

// Validate checks input values for errors.
// It can raise the panic if the input type is unsupported.
func (s Schema) Validate(data map[string][]string) (map[string][]string, ValidationError) {
func (s Schema) Validate(data map[string][]string) (map[string][]string, AccumulatedError) {
if len(s.Inputs) == 0 || len(data) == 0 {
return data, nil
}
rules, index := makeRules(s.Inputs)
validation := dataValidationResult{data: data}
for name, values := range data {
i, found := index[name]
if !found {
continue
}
inputValidation := inputValidationResult{input: s.Inputs[i]}
validators := rules[name]
for _, validator := range validators {
if err := validator.Validate(values); err != nil {
inputValidation.errors = append(inputValidation.errors, err)
}
}
validation.results = append(validation.results, inputValidation)
}
return data, validation.AsError()
}

func makeRules(inputs []Input) (map[string][]Validator, map[string]int) {
index := make(map[string]int, len(inputs))
rules := make(map[string][]Validator, len(inputs))
for i, input := range inputs {
index[input.Name] = i
rules := make(map[string][]Validator, len(s.Inputs))
for _, input := range s.Inputs {
validators := make([]Validator, 0, 3)
validators = append(validators, TypeValidator(input.Type, input.Strict))
if input.MinLength != 0 || input.MaxLength != 0 {
Expand All @@ -116,5 +95,5 @@ func makeRules(inputs []Input) (map[string][]Validator, map[string]int) {
}
rules[input.Name] = validators
}
return rules, index
return data, Run(s.Inputs, rules, data)
}
6 changes: 3 additions & 3 deletions domain/schema_test.go
Expand Up @@ -32,7 +32,7 @@ func TestSchema_Apply(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
var (
obtained map[string][]string
err domain.ValidationError
err domain.AccumulatedError
)
action := func() { obtained, err = tc.schema.Apply(tc.values) }
if tc.expected.panic {
Expand Down Expand Up @@ -206,15 +206,15 @@ func TestSchema_Validate(t *testing.T) {
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
var err domain.ValidationError
var err domain.AccumulatedError
action := func() { _, err = tc.schema.Validate(tc.values) }
if tc.expected.panic {
assert.Panics(t, action)
} else {
assert.NotPanics(t, action)
}
if tc.expected.error {
assert.EqualError(t, err, "input data has error")
assert.EqualError(t, err, "validation error")
obtained := err.InputWithErrors()
for input, errors := range obtained {
expected := tc.expected.data[input]
Expand Down
Empty file removed domain/validation/.gitkeep
Empty file.
104 changes: 26 additions & 78 deletions domain/validator.go → domain/validators.go
Expand Up @@ -5,20 +5,27 @@ import (
"strings"
)

const (
// EmailType specifies `<input type="email">`.
EmailType = "email"
// HiddenType specifies `<input type="hidden">`
HiddenType = "hidden"
// TextType specifies `<input type="text">`.
TextType = "text"
)

// ValidationError represents an error related to invalid input values.
type ValidationError interface {
error
// InputWithErrors returns map of form inputs and their errors.
InputWithErrors() map[Input][]error
func Run(inputs []Input, rules map[string][]Validator, data map[string][]string) AccumulatedError {
index := make(map[string]int, len(inputs))
for i, input := range inputs {
index[input.Name] = i
}
validation := dataValidationResult{}
for name, values := range data {
i, found := index[name]
if !found {
continue
}
inputValidation := inputValidationResult{input: inputs[i]}
validators := rules[name]
for _, validator := range validators {
if err := validator.Validate(values); err != nil {
inputValidation.errors = append(inputValidation.errors, err)
}
}
validation.results = append(validation.results, inputValidation)
}
return validation.AsError()
}

// Validator defines basic behavior of input validators.
Expand All @@ -35,6 +42,7 @@ func (fn ValidatorFunc) Validate(values []string) error {
return fn(values)
}

// LengthValidator ...
func LengthValidator(min, max int) ValidatorFunc {
return func(values []string) error {
for i, value := range values {
Expand All @@ -49,6 +57,7 @@ func LengthValidator(min, max int) ValidatorFunc {
}
}

// RequireValidator ...
func RequireValidator() ValidatorFunc {
return func(values []string) error {
if len(values) == 0 {
Expand All @@ -63,6 +72,8 @@ func RequireValidator() ValidatorFunc {
}
}

// TypeValidator ...
// It can raise the panic if the input type is unsupported.
func TypeValidator(inputType string, strict bool) ValidatorFunc {
return func(values []string) error {
switch inputType {
Expand All @@ -85,71 +96,8 @@ func TypeValidator(inputType string, strict bool) ValidatorFunc {
case HiddenType, TextType:
// nothing special
default:
panic(fmt.Sprintf("not supported input type %q", inputType))
panic(fmt.Sprintf("input type %q is not supported", inputType))
}
return nil
}
}

type validationError struct {
single bool
position int
value string
message string
}

func (err validationError) Error() string {
if err.single {
if err.value != "" {
return fmt.Sprintf("value %q at position %d is invalid: %s", err.value, err.position, err.message)
}
return fmt.Sprintf("value at position %d is invalid: %s", err.position, err.message)
}
return err.message
}

type dataValidationError struct {
dataValidationResult
}

func (dataValidationError) Error() string {
return "input data has error"
}

func (err dataValidationError) InputWithErrors() map[Input][]error {
m := make(map[Input][]error, len(err.results))
for _, r := range err.results {
m[r.input] = r.errors
}
return m
}

type dataValidationResult struct {
data map[string][]string
results []inputValidationResult
}

// AsError converts the result into error if it contains at least one input validation error.
func (r dataValidationResult) AsError() ValidationError {
for _, sub := range r.results {
if sub.HasError() {
return dataValidationError{r}
}
}
return nil
}

type inputValidationResult struct {
input Input
errors []error
}

// HasError returns true if the result contains at least one, not nil error.
func (r inputValidationResult) HasError() bool {
for _, err := range r.errors {
if err != nil {
return true
}
}
return false
}
2 changes: 1 addition & 1 deletion server/server.go
Expand Up @@ -109,7 +109,7 @@ func (s *Server) PostV1(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusBadRequest)
s.templates.errorTpl.Execute(rw, static.ErrorPageContext{
Schema: response.Schema,
Error: err.Cause().(domain.ValidationError),
Error: err.Cause().(domain.AccumulatedError),
Delay: 5 * time.Second,
Redirect: redirect,
})
Expand Down
2 changes: 1 addition & 1 deletion static/fixtures/email_subscription.html.golden
Expand Up @@ -13,7 +13,7 @@
</style>
</head>
<body class="text-center"><form lang="en" title="Email subscription" action="https://kamil.samigullin.info/" method="post" enctype="application/x-www-form-urlencoded"><div class="form-group row"><label for="email" class="col-sm-2 col-form-label col-form-label-lg">Email</label>
<div class="col-sm-10"><input id="email" class="form-control form-control-lg" name="email" type="email" title="Email" placeholder="Your email..." value="invalid" maxlength="64" required></div></div><input name="_redirect" type="hidden" value="https://kamil.samigullin.info/">
<div class="col-sm-10"><input id="email" class="form-control form-control-lg" name="email" type="email" title="Email" placeholder="Your email..." value="is invalid" maxlength="64" required></div></div><input name="_redirect" type="hidden" value="https://kamil.samigullin.info/">
<input class="btn btn-danger" type="submit">
</form><script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
Expand Down
2 changes: 1 addition & 1 deletion static/get.go
Expand Up @@ -12,7 +12,7 @@ import (
// ErrorPageContext contains data for `error.html` template.
type ErrorPageContext struct {
Schema domain.Schema
Error domain.ValidationError
Error domain.AccumulatedError
Delay time.Duration
Redirect string
}
Expand Down
2 changes: 1 addition & 1 deletion static/render_test.go
Expand Up @@ -52,7 +52,7 @@ func TestErrorTemplate(t *testing.T) {
},
},
}
_, err := schema.Apply(map[string][]string{"email": {"invalid"}})
_, err := schema.Apply(map[string][]string{"email": {"is invalid"}})
return static.ErrorPageContext{Schema: schema, Error: err, Delay: time.Hour, Redirect: schema.Action}
}, "./fixtures/email_subscription.html.golden"},
}
Expand Down

0 comments on commit 1b9aa4c

Please sign in to comment.