mirror of
https://github.com/romdo/go-validate.git
synced 2026-02-18 23:56:41 +00:00
feat(validate): initial implementation
This is a bare-bones implementation of a basic validation package based
around a very simply Validatable interface:
type Validatable interface {
Validate() error
}
The goal is to keep things as simple as possible, while also giving as
much control as possible over how validation logic is performed.
This commit is contained in:
67
error.go
Normal file
67
error.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
// Error represents validation errors, and implements Go's error type. Field
|
||||
// indicates the struct field the validation error is relevant to, which is the
|
||||
// full nested path relative to the top-level object being validated.
|
||||
type Error struct {
|
||||
Field string
|
||||
Msg string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (s *Error) Error() string {
|
||||
msg := s.Msg
|
||||
if msg == "" && s.Err != nil {
|
||||
msg = s.Err.Error()
|
||||
}
|
||||
|
||||
if msg == "" {
|
||||
msg = "unknown error"
|
||||
}
|
||||
|
||||
if s.Field == "" {
|
||||
return msg
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s: %s", s.Field, msg)
|
||||
}
|
||||
|
||||
func (s *Error) Is(target error) bool {
|
||||
return errors.Is(s.Err, target)
|
||||
}
|
||||
|
||||
func (s *Error) Unwrap() error {
|
||||
return s.Err
|
||||
}
|
||||
|
||||
// Append combines two errors together into a single new error which internally
|
||||
// keeps track of multiple errors via go.uber.org/multierr. If either error is a
|
||||
// previously combined multierr, the returned error will be a flattened list of
|
||||
// all errors.
|
||||
func Append(errs error, err error) error {
|
||||
return multierr.Append(errs, err)
|
||||
}
|
||||
|
||||
// AppendError appends a new *Error type to errs with the Msg field populated
|
||||
// with the provided msg.
|
||||
func AppendError(errs error, msg string) error {
|
||||
return multierr.Append(errs, &Error{Msg: msg})
|
||||
}
|
||||
|
||||
// AppendFieldError appends a new *Error type to errs with Field and Msg
|
||||
// populated with given field and msg values.
|
||||
func AppendFieldError(errs error, field, msg string) error {
|
||||
return multierr.Append(errs, &Error{Field: field, Msg: msg})
|
||||
}
|
||||
|
||||
// Errors returns a slice of all errors appended into the given error.
|
||||
func Errors(err error) []error {
|
||||
return multierr.Errors(err)
|
||||
}
|
||||
456
error_test.go
Normal file
456
error_test.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
func TestError_Error(t *testing.T) {
|
||||
type fields struct {
|
||||
Field string
|
||||
Msg string
|
||||
Err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{},
|
||||
want: "unknown error",
|
||||
},
|
||||
{
|
||||
name: "field only",
|
||||
fields: fields{
|
||||
Field: "spec.images.0.name",
|
||||
},
|
||||
want: "spec.images.0.name: unknown error",
|
||||
},
|
||||
{
|
||||
name: "msg only",
|
||||
fields: fields{
|
||||
Msg: "flux capacitor is missing",
|
||||
},
|
||||
want: "flux capacitor is missing",
|
||||
},
|
||||
{
|
||||
name: "err only",
|
||||
fields: fields{
|
||||
Err: errors.New("flux capacitor is king"),
|
||||
},
|
||||
want: "flux capacitor is king",
|
||||
},
|
||||
{
|
||||
name: "field and msg",
|
||||
fields: fields{
|
||||
Field: "spec.images.0.name",
|
||||
Msg: "is required",
|
||||
},
|
||||
want: "spec.images.0.name: is required",
|
||||
},
|
||||
{
|
||||
name: "field and err",
|
||||
fields: fields{
|
||||
Field: "spec",
|
||||
Err: errors.New("something is wrong"),
|
||||
},
|
||||
want: "spec: something is wrong",
|
||||
},
|
||||
{
|
||||
name: "msg and err",
|
||||
fields: fields{
|
||||
Msg: "flux capacitor is missing",
|
||||
Err: errors.New("flux capacitor is king"),
|
||||
},
|
||||
want: "flux capacitor is missing",
|
||||
},
|
||||
{
|
||||
name: "field, msg, and err",
|
||||
fields: fields{
|
||||
Field: "spec.images.0.name",
|
||||
Msg: "is required",
|
||||
Err: errors.New("something is wrong"),
|
||||
},
|
||||
want: "spec.images.0.name: is required",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &Error{
|
||||
Field: tt.fields.Field,
|
||||
Msg: tt.fields.Msg,
|
||||
Err: tt.fields.Err,
|
||||
}
|
||||
|
||||
got := err.Error()
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_Is(t *testing.T) {
|
||||
errTest1 := errors.New("errtest1")
|
||||
errTest2 := errors.New("errtest2")
|
||||
|
||||
type fields struct {
|
||||
Err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
target error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{},
|
||||
target: errTest1,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Err and target match",
|
||||
fields: fields{Err: errTest1},
|
||||
target: errTest1,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Err and target do not match",
|
||||
fields: fields{Err: errTest2},
|
||||
target: errTest1,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &Error{Err: tt.fields.Err}
|
||||
|
||||
got := errors.Is(err, tt.target)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_Unwrap(t *testing.T) {
|
||||
errTest1 := errors.New("errtest1")
|
||||
errTest2 := errors.New("errtest2")
|
||||
|
||||
type fields struct {
|
||||
Err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "Err test1",
|
||||
fields: fields{Err: errTest1},
|
||||
want: errTest1,
|
||||
},
|
||||
{
|
||||
name: "Err test2",
|
||||
fields: fields{Err: errTest2},
|
||||
want: errTest2,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &Error{Err: tt.fields.Err}
|
||||
|
||||
got := err.Unwrap()
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
type args struct {
|
||||
errs error
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []error
|
||||
}{
|
||||
{
|
||||
name: "append nil to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
err: nil,
|
||||
},
|
||||
want: []error{},
|
||||
},
|
||||
{
|
||||
name: "append nil to err",
|
||||
args: args{
|
||||
errs: errors.New("foo"),
|
||||
err: nil,
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append nil to multi err",
|
||||
args: args{
|
||||
errs: multierr.Combine(errors.New("foo"), errors.New("bar")),
|
||||
err: nil,
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append err to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
err: errors.New("foo"),
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append err to err",
|
||||
args: args{
|
||||
errs: errors.New("foo"),
|
||||
err: errors.New("bar"),
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append err to multi err",
|
||||
args: args{
|
||||
errs: multierr.Combine(errors.New("foo"), errors.New("bar")),
|
||||
err: errors.New("baz"),
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
errors.New("baz"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := Append(tt.args.errs, tt.args.err)
|
||||
|
||||
if len(tt.want) == 0 {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
got := multierr.Errors(err)
|
||||
|
||||
assert.ElementsMatch(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendError(t *testing.T) {
|
||||
type args struct {
|
||||
errs error
|
||||
msg string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []error
|
||||
}{
|
||||
{
|
||||
name: "append empty msg to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
msg: "",
|
||||
},
|
||||
want: []error{
|
||||
&Error{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
msg: "foo",
|
||||
},
|
||||
want: []error{
|
||||
&Error{Msg: "foo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to err",
|
||||
args: args{
|
||||
errs: errors.New("foo"),
|
||||
msg: "bar",
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
&Error{Msg: "bar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to multi err",
|
||||
args: args{
|
||||
errs: multierr.Combine(errors.New("foo"), errors.New("bar")),
|
||||
msg: "baz",
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
&Error{Msg: "baz"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := AppendError(tt.args.errs, tt.args.msg)
|
||||
|
||||
if len(tt.want) == 0 {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
got := multierr.Errors(err)
|
||||
|
||||
assert.ElementsMatch(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendFieldError(t *testing.T) {
|
||||
type args struct {
|
||||
errs error
|
||||
field string
|
||||
msg string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []error
|
||||
}{
|
||||
{
|
||||
name: "append empty field and msg to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
field: "",
|
||||
msg: "",
|
||||
},
|
||||
want: []error{
|
||||
&Error{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
field: "Type",
|
||||
msg: "foo",
|
||||
},
|
||||
want: []error{
|
||||
&Error{Field: "Type", Msg: "foo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to err",
|
||||
args: args{
|
||||
errs: errors.New("foo"),
|
||||
field: "Type",
|
||||
msg: "bar",
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
&Error{Field: "Type", Msg: "bar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to multi err",
|
||||
args: args{
|
||||
errs: multierr.Combine(errors.New("foo"), errors.New("bar")),
|
||||
field: "Type",
|
||||
msg: "baz",
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
&Error{Field: "Type", Msg: "baz"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := AppendFieldError(tt.args.errs, tt.args.field, tt.args.msg)
|
||||
|
||||
if len(tt.want) == 0 {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
got := multierr.Errors(err)
|
||||
|
||||
assert.ElementsMatch(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
type args struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{err: nil},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single error",
|
||||
args: args{err: errors.New("foo")},
|
||||
want: []error{errors.New("foo")},
|
||||
},
|
||||
{
|
||||
name: "multi error with one error",
|
||||
args: args{
|
||||
err: multierr.Combine(errors.New("foo")),
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multi error with two errors",
|
||||
args: args{
|
||||
err: multierr.Combine(errors.New("foo"), errors.New("bar")),
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Errors(tt.args.err)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
9
go.mod
Normal file
9
go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module github.com/romdo/go-validate
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.7.0
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.7.0
|
||||
)
|
||||
19
go.sum
Normal file
19
go.sum
Normal file
@@ -0,0 +1,19 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
36
helpers.go
Normal file
36
helpers.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// RequireField returns a Error type for the given field if provided value is
|
||||
// empty/zero.
|
||||
func RequireField(field string, value interface{}) error {
|
||||
err := &Error{Field: field, Msg: "is required"}
|
||||
v := reflect.ValueOf(value)
|
||||
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return err
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if !v.IsValid() {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v.Kind() { //nolint:exhaustive
|
||||
case reflect.Map, reflect.Slice:
|
||||
if v.Len() == 0 {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if v.IsZero() {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
406
helpers_test.go
Normal file
406
helpers_test.go
Normal file
@@ -0,0 +1,406 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestRequireField(t *testing.T) {
|
||||
var nilMapString map[string]string
|
||||
emptyMapString := map[string]string{}
|
||||
mapString := map[string]string{"foo": "bar"}
|
||||
type testStruct struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type args struct {
|
||||
field string
|
||||
value interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: nil,
|
||||
},
|
||||
want: &Error{Field: "Title", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "nil pointer",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: &nilMapString,
|
||||
},
|
||||
want: &Error{Field: "Title", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "true boolean",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: true,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "false boolean",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: false,
|
||||
},
|
||||
want: &Error{Field: "Book", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "int",
|
||||
args: args{
|
||||
field: "Count",
|
||||
value: int(834),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero int",
|
||||
args: args{
|
||||
field: "Count",
|
||||
value: int(0),
|
||||
},
|
||||
want: &Error{Field: "Count", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "int8",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int8(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero int8",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int8(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "int16",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int16(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero int16",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int16(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "int32",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int32(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero int32",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int32(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "int64",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int64(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero int64",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int64(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "zero uint",
|
||||
args: args{
|
||||
field: "Count",
|
||||
value: uint(0),
|
||||
},
|
||||
want: &Error{Field: "Count", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "uint8",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint8(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero uint8",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint8(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "uint16",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint16(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero uint16",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint16(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "uint32",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint32(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero uint32",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint32(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "uint64",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint64(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero uint64",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint64(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "complex64",
|
||||
args: args{
|
||||
field: "Offset",
|
||||
value: complex64(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero complex64",
|
||||
args: args{
|
||||
field: "Offset",
|
||||
value: complex64(0),
|
||||
},
|
||||
want: &Error{Field: "Offset", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "complex128",
|
||||
args: args{
|
||||
field: "Offset",
|
||||
value: complex128(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero complex128",
|
||||
args: args{
|
||||
field: "Offset",
|
||||
value: complex128(0),
|
||||
},
|
||||
want: &Error{Field: "Offset", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "array",
|
||||
args: args{
|
||||
field: "List",
|
||||
value: [3]string{"foo", "bar", "baz"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty array",
|
||||
args: args{
|
||||
field: "List",
|
||||
value: [3]string{},
|
||||
},
|
||||
want: &Error{Field: "List", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "chan",
|
||||
args: args{
|
||||
field: "Comms",
|
||||
value: make(chan int),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "func",
|
||||
args: args{
|
||||
field: "Callback",
|
||||
value: func() error { return nil },
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "map",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: map[string]string{"foo": "bar"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "map pointer",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: &mapString,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: map[string]string{},
|
||||
},
|
||||
want: &Error{Field: "Lookup", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "empty map pointer",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: &emptyMapString,
|
||||
},
|
||||
want: &Error{Field: "Lookup", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "nil map",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: nilMapString,
|
||||
},
|
||||
want: &Error{Field: "Lookup", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "slice",
|
||||
args: args{
|
||||
field: "List",
|
||||
value: []string{"foo", "bar", "baz"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty slice",
|
||||
args: args{
|
||||
field: "List",
|
||||
value: []string{},
|
||||
},
|
||||
want: &Error{Field: "List", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: "foo",
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "string pointer",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: stringPtr("foo"),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: "",
|
||||
},
|
||||
want: &Error{Field: "Book", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "empty string pointer",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: stringPtr(""),
|
||||
},
|
||||
want: &Error{Field: "Book", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "struct",
|
||||
args: args{
|
||||
field: "Thing",
|
||||
value: testStruct{Name: "hi"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "struct pointer",
|
||||
args: args{
|
||||
field: "Thing",
|
||||
value: &testStruct{Name: "hi"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty struct",
|
||||
args: args{
|
||||
field: "Thing",
|
||||
value: testStruct{},
|
||||
},
|
||||
want: &Error{Field: "Thing", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "empty struct pointer",
|
||||
args: args{
|
||||
field: "Thing",
|
||||
value: &testStruct{},
|
||||
},
|
||||
want: &Error{Field: "Thing", Msg: "is required"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := RequireField(tt.args.field, tt.args.value)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
162
validate.go
Normal file
162
validate.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Package validate is yet another Go struct/object validation package, with a
|
||||
// focus on simplicity, flexibility, and full control over validation logic.
|
||||
//
|
||||
// Interface
|
||||
//
|
||||
// To add validation to any type, simply implement the Validatable interface:
|
||||
//
|
||||
// type Validatable interface {
|
||||
// Validate() error
|
||||
// }
|
||||
//
|
||||
// To mark a object as failing validation, the Validate method simply needs to
|
||||
// return a error.
|
||||
//
|
||||
// When validating array, slice, map, and struct types each item and/or field
|
||||
// that implements Validatable will be validated, meaning deeply nested structs
|
||||
// can be fully validated, and the nested path to each object is tracked and
|
||||
// reported back any validation errors.
|
||||
//
|
||||
// Multiple Errors
|
||||
//
|
||||
// Multiple errors can be reported from the Validate method using one of the
|
||||
// available Append helper functions which append errors together. Under the
|
||||
// hood the go.uber.org/multierr package is used to represent multiple errors as
|
||||
// a single error return type, and you can in fact just directly use multierr in
|
||||
// the a type's Validate method.
|
||||
//
|
||||
// Structs and Field-specific Errors
|
||||
//
|
||||
// When validating a struct, you are likely to have multiple errors for multiple
|
||||
// fields. To specify which field on the struct the error relates to, you have
|
||||
// to return a *validate.Error instead of a normal Go error type. For example:
|
||||
//
|
||||
// type Book struct {
|
||||
// Title string
|
||||
// Author string
|
||||
// }
|
||||
//
|
||||
// func (s *Book) Validate() error {
|
||||
// var errs error
|
||||
//
|
||||
// if s.Title == "" {
|
||||
// errs = validate.Append(errs, &validate.Error{
|
||||
// Field: "Title", Msg: "is required",
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// if s.Author == "" {
|
||||
// // Yields the same result as the Title field check above.
|
||||
// errs = validate.AppendFieldError(errs, "Author", "is required")
|
||||
// }
|
||||
//
|
||||
// return errs
|
||||
// }
|
||||
//
|
||||
// With the above example, if you validate a empty *Book:
|
||||
//
|
||||
// err := validate.Validate(&Book{})
|
||||
// for _, e := range validate.Errors(err) {
|
||||
// fmt.Println(e.Error())
|
||||
// }
|
||||
//
|
||||
// The following errors would be printed:
|
||||
//
|
||||
// Title: is required
|
||||
// Kind: is required
|
||||
//
|
||||
// Error type
|
||||
//
|
||||
// All errors will be wrapped in a *Error before being returned, which is used
|
||||
// to keep track of the path and field the error relates to. There are various
|
||||
// helpers available to create Error instances.
|
||||
//
|
||||
// Handling Validation Errors
|
||||
//
|
||||
// As mentioned above, multiple errors are wrapped up into a single error return
|
||||
// value using go.uber.org/multierr. You can access all errors individually with
|
||||
// Errors(), which accepts a single error, and returns []error. The Errors()
|
||||
// function is just wrapper around multierr.Errors(), so you could use that
|
||||
// instead if you prefer.
|
||||
//
|
||||
// Struct Field Tags
|
||||
//
|
||||
// Fields on a struct which customize the name via a json, yaml, or form field
|
||||
// tag, will automatically have the field name converted to the name in the tag
|
||||
// in returned *Error types with a non-empty Field value.
|
||||
//
|
||||
// You can customize the field name conversion logic by creating a custom
|
||||
// Validator instance, and calling FieldNameFunc() on it.
|
||||
//
|
||||
// Nested Validatable Objects
|
||||
//
|
||||
// All items/fields on any structs, maps, slices or arrays which are encountered
|
||||
// will be validated if they implement the Validatable interface. While
|
||||
// traversing nested data structures, a path list tracks the location of the
|
||||
// current object being validation in relation to the top-level object being
|
||||
// validated. This path is used within the field in the final output errors.
|
||||
//
|
||||
// By default path components are joined with a dot, but this can be customized
|
||||
// when using a custom Validator instance and calling FieldJoinFunc() passing in
|
||||
// a custom function to handle path joining.
|
||||
//
|
||||
// As an example, if our Book struct from above is nested within the following
|
||||
// structs:
|
||||
//
|
||||
// type Order struct {
|
||||
// Items []*Item `json:"items"`
|
||||
// }
|
||||
//
|
||||
// type Item struct {
|
||||
// Book *Book `json:"book"`
|
||||
// }
|
||||
//
|
||||
// And we have a Order where the book in the second Item has a empty Author
|
||||
// field:
|
||||
//
|
||||
// err := validate.Validate(&Order{
|
||||
// Items: []*Item{
|
||||
// {Book: &Book{Title: "The Traveler", Author: "John Twelve Hawks"}},
|
||||
// {Book: &Book{Title: "The Firm"}},
|
||||
// },
|
||||
// })
|
||||
// for _, e := range validate.Errors(err) {
|
||||
// fmt.Println(e.Error())
|
||||
// }
|
||||
//
|
||||
// Then we would get the following error:
|
||||
//
|
||||
// items.1.book.Author: is required
|
||||
//
|
||||
// Note how both "items" and "book" are lower cased thanks to the json tags on
|
||||
// the struct fields, while our Book struct does not have a json tag for the
|
||||
// Author field.
|
||||
//
|
||||
// Also note that the error message does not start with "Order". The field path
|
||||
// is relative to the object being validated, hence the top-level object is not
|
||||
// part of the returned field path.
|
||||
package validate
|
||||
|
||||
// global is a private instance of Validator to enable the package root-level
|
||||
// Validate() function.
|
||||
var global = New()
|
||||
|
||||
// Validate will validate the given object. Structs, maps, slices, and arrays
|
||||
// will have each of their fields/items validated, effectively performing a
|
||||
// deep-validation.
|
||||
func Validate(v interface{}) error {
|
||||
return global.Validate(v)
|
||||
}
|
||||
|
||||
// Validatable is the primary interface that a object needs to implement to be
|
||||
// validatable with Validator.
|
||||
//
|
||||
// Validation errors are reported by returning a error from the Validate
|
||||
// method. Multiple errors can be combined into a single error to return with
|
||||
// Append() and related functions, or via go.uber.org/multierr.
|
||||
//
|
||||
// For validatable structs, the field the validation error relates to can be
|
||||
// specified by returning a *Error type with the Field value specified.
|
||||
type Validatable interface {
|
||||
Validate() error
|
||||
}
|
||||
907
validate_test.go
Normal file
907
validate_test.go
Normal file
@@ -0,0 +1,907 @@
|
||||
package validate_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/romdo/go-validate"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//
|
||||
// Test helper types
|
||||
//
|
||||
|
||||
type validatableString string
|
||||
|
||||
func (s validatableString) Validate() error {
|
||||
if strings.Contains(string(s), " ") {
|
||||
return &validate.Error{Msg: "must not contain space"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type validatableStruct struct {
|
||||
Foo string
|
||||
Bar string `json:"bar"`
|
||||
Foz string `yaml:"foz"`
|
||||
Baz string `form:"baz"`
|
||||
|
||||
f func() error
|
||||
}
|
||||
|
||||
func (s *validatableStruct) Validate() error {
|
||||
if s.f == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.f()
|
||||
}
|
||||
|
||||
type nestedStruct struct {
|
||||
OtherField *validatableStruct
|
||||
OtherFieldJSON *validatableStruct `json:"other_field,omitempty"`
|
||||
OtherFieldYAML *validatableStruct `yaml:"otherField,omitempty"`
|
||||
OtherFieldFORM *validatableStruct `form:"other-field,omitempty"`
|
||||
|
||||
skippedField *validatableStruct
|
||||
SkippedFieldJSON *validatableStruct `json:"-,omitempty"`
|
||||
SkippedFieldYAML *validatableStruct `yaml:"-,omitempty"`
|
||||
SkippedFieldFORM *validatableStruct `form:"-,omitempty"`
|
||||
|
||||
OtherArray [5]*validatableStruct
|
||||
OtherArrayJSON [5]*validatableStruct `json:"other_array,omitempty"`
|
||||
OtherArrayYAML [5]*validatableStruct `yaml:"otherArray,omitempty"`
|
||||
OtherArrayFORM [5]*validatableStruct `form:"other-array,omitempty"`
|
||||
|
||||
OtherSlice []*validatableStruct
|
||||
OtherSliceJSON []*validatableStruct `json:"other_slice,omitempty"`
|
||||
OtherSliceYAML []*validatableStruct `yaml:"otherSlice,omitempty"`
|
||||
OtherSliceFORM []*validatableStruct `form:"other-slice,omitempty"`
|
||||
|
||||
OtherStringMap map[string]*validatableStruct
|
||||
OtherStringMapJSON map[string]*validatableStruct `json:"other_string_map,omitempty"`
|
||||
OtherStringMapYAML map[string]*validatableStruct `yaml:"otherStringMap,omitempty"`
|
||||
OtherStringMapFORM map[string]*validatableStruct `form:"other-string-map,omitempty"`
|
||||
|
||||
OtherIntMap map[int]*validatableStruct
|
||||
OtherIntMapJSON map[int]*validatableStruct `json:"other_int_map,omitempty"`
|
||||
OtherIntMapYAML map[int]*validatableStruct `yaml:"otherIntMap,omitempty"`
|
||||
OtherIntMapFORM map[int]*validatableStruct `form:"other-int-map,omitempty"`
|
||||
|
||||
OtherStruct *nestedStruct
|
||||
OtherStructJSON *nestedStruct `json:"other_struct,omitempty"`
|
||||
OtherStructYAML *nestedStruct `yaml:"otherStruct,omitempty"`
|
||||
OtherStructFORM *nestedStruct `form:"other-struct,omitempty"`
|
||||
}
|
||||
|
||||
//
|
||||
// Tests
|
||||
//
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
mixedValidationErrors := &validatableStruct{
|
||||
f: func() error {
|
||||
var errs error
|
||||
errs = validate.Append(errs, &validate.Error{
|
||||
Field: "Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
})
|
||||
errs = validate.Append(errs, errors.New("bar: is missing"))
|
||||
|
||||
return errs
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
obj interface{}
|
||||
wantErrs []error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
obj: nil,
|
||||
wantErrs: []error{},
|
||||
},
|
||||
{
|
||||
name: "no error",
|
||||
obj: &validatableStruct{},
|
||||
wantErrs: nil,
|
||||
},
|
||||
{
|
||||
name: "valid validatable string type",
|
||||
obj: validatableString("hello-world"),
|
||||
wantErrs: []error{},
|
||||
},
|
||||
{
|
||||
name: "invalid validatable string type",
|
||||
obj: validatableString("hello world"),
|
||||
wantErrs: []error{
|
||||
&validate.Error{Msg: "must not contain space"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single Go error",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return errors.New("foo: is required")
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Err: errors.New("foo: is required")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single *validate.Error",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return &validate.Error{
|
||||
Field: "foo", Msg: "is required", Err: errors.New("oops"),
|
||||
}
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "foo", Msg: "is required", Err: errors.New("oops"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple Go errors",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
var errs error
|
||||
errs = validate.Append(errs, errors.New("foo: is required"))
|
||||
errs = validate.Append(errs, errors.New("bar: is missing"))
|
||||
|
||||
return errs
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Err: errors.New("foo: is required")},
|
||||
&validate.Error{Err: errors.New("bar: is missing")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple *validate.Error",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
var errs error
|
||||
errs = validate.Append(errs, &validate.Error{
|
||||
Field: "foo", Msg: "is required", Err: errors.New("oops"),
|
||||
})
|
||||
errs = validate.Append(errs, &validate.Error{
|
||||
Field: "bar", Msg: "is required", Err: errors.New("whoops"),
|
||||
})
|
||||
|
||||
return errs
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "foo", Msg: "is required", Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "bar", Msg: "is required", Err: errors.New("whoops"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mix of Go error and *validate.Error",
|
||||
obj: mixedValidationErrors,
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "Foo", Msg: "is required", Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{Err: errors.New("bar: is missing")},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Field name conversion
|
||||
//
|
||||
{
|
||||
name: "no json, yaml or form field tag",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return &validate.Error{Field: "Foo", Msg: "is required"}
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Field: "Foo", Msg: "is required"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "converts field name via json field tag",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return &validate.Error{Field: "Bar", Msg: "is required"}
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Field: "bar", Msg: "is required"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "converts field name via yaml field tag",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return &validate.Error{Field: "Foz", Msg: "is required"}
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Field: "foz", Msg: "is required"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "converts field name via form field tag",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return &validate.Error{Field: "Baz", Msg: "is required"}
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Field: "baz", Msg: "is required"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested with no validation errors",
|
||||
obj: &nestedStruct{
|
||||
OtherField: &validatableStruct{},
|
||||
OtherArray: [5]*validatableStruct{{}, {}, {}, {}},
|
||||
OtherSlice: []*validatableStruct{{}},
|
||||
OtherStringMap: map[string]*validatableStruct{
|
||||
"hi": {},
|
||||
"bye": {},
|
||||
},
|
||||
OtherIntMap: map[int]*validatableStruct{42: {}, 64: {}},
|
||||
OtherStruct: &nestedStruct{
|
||||
OtherField: &validatableStruct{},
|
||||
},
|
||||
},
|
||||
wantErrs: []error{},
|
||||
},
|
||||
//
|
||||
// Nested in a struct field.
|
||||
//
|
||||
{
|
||||
name: "nested in a struct field",
|
||||
obj: &nestedStruct{
|
||||
OtherField: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherField.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherField", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherFieldJSON: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_field.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_field", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherFieldYAML: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherField.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherField", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherFieldFORM: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-field.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-field", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Nested in a unexposed/ignored fields.
|
||||
//
|
||||
{
|
||||
name: "nested in a unexposed field",
|
||||
obj: &nestedStruct{
|
||||
skippedField: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field skipped by json tag",
|
||||
obj: &nestedStruct{
|
||||
SkippedFieldJSON: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field skipped by yaml tag",
|
||||
obj: &nestedStruct{
|
||||
SkippedFieldYAML: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field skipped by yaml tag",
|
||||
obj: &nestedStruct{
|
||||
SkippedFieldFORM: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{},
|
||||
},
|
||||
//
|
||||
// Nested in an array.
|
||||
//
|
||||
{
|
||||
name: "nested in an array",
|
||||
obj: &nestedStruct{
|
||||
OtherArray: [5]*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherArray.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherArray.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherArray.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherArray.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in an array with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherArrayJSON: [5]*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_array.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_array.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_array.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_array.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in an array with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherArrayYAML: [5]*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherArray.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherArray.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherArray.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherArray.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in an array with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherArrayFORM: [5]*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-array.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-array.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-array.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-array.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Nested in a slice.
|
||||
//
|
||||
{
|
||||
name: "nested in a slice",
|
||||
obj: &nestedStruct{
|
||||
OtherSlice: []*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherSlice.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherSlice.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherSlice.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherSlice.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a slice with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherSliceJSON: []*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_slice.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_slice.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_slice.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_slice.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a slice with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherSliceYAML: []*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherSlice.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherSlice.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherSlice.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherSlice.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a slice with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherSliceFORM: []*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-slice.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-slice.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-slice.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-slice.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Nested in an string map.
|
||||
//
|
||||
{
|
||||
name: "nested in a string map",
|
||||
obj: &nestedStruct{
|
||||
OtherStringMap: map[string]*validatableStruct{
|
||||
"hello": mixedValidationErrors,
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherStringMap.hello.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStringMap.hello",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStringMap.world.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStringMap.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a string map with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStringMapJSON: map[string]*validatableStruct{
|
||||
"hello": mixedValidationErrors,
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_string_map.hello.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_string_map.hello",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_string_map.world.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_string_map.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a string map with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStringMapYAML: map[string]*validatableStruct{
|
||||
"hello": mixedValidationErrors,
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherStringMap.hello.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStringMap.hello",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStringMap.world.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStringMap.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a string map with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStringMapFORM: map[string]*validatableStruct{
|
||||
"hello": mixedValidationErrors,
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-string-map.hello.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-string-map.hello",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-string-map.world.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-string-map.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Nested in an int map.
|
||||
//
|
||||
{
|
||||
name: "nested in a int map",
|
||||
obj: &nestedStruct{
|
||||
OtherIntMap: map[int]*validatableStruct{
|
||||
42: mixedValidationErrors,
|
||||
64: mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherIntMap.42.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherIntMap.42",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherIntMap.64.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherIntMap.64",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherIntMapJSON: map[int]*validatableStruct{
|
||||
42: mixedValidationErrors,
|
||||
64: mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_int_map.42.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_int_map.42",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_int_map.64.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_int_map.64",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherIntMapYAML: map[int]*validatableStruct{
|
||||
42: mixedValidationErrors,
|
||||
64: mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherIntMap.42.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherIntMap.42",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherIntMap.64.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherIntMap.64",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherIntMapFORM: map[int]*validatableStruct{
|
||||
42: mixedValidationErrors,
|
||||
64: mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-int-map.42.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-int-map.42",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-int-map.64.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-int-map.64",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Nested in another struct.
|
||||
//
|
||||
{
|
||||
name: "nested in another struct",
|
||||
obj: &nestedStruct{
|
||||
OtherStruct: &nestedStruct{
|
||||
OtherField: mixedValidationErrors,
|
||||
OtherStringMap: map[string]*validatableStruct{
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherStruct.OtherField.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStruct.OtherField",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStruct.OtherStringMap.world.Foo",
|
||||
Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStruct.OtherStringMap.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStructJSON: &nestedStruct{
|
||||
OtherFieldJSON: mixedValidationErrors,
|
||||
OtherStringMapJSON: map[string]*validatableStruct{
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_struct.other_field.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_struct.other_field",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_struct.other_string_map.world.Foo",
|
||||
Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_struct.other_string_map.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStructYAML: &nestedStruct{
|
||||
OtherFieldYAML: mixedValidationErrors,
|
||||
OtherStringMapYAML: map[string]*validatableStruct{
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherStruct.otherField.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStruct.otherField",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStruct.otherStringMap.world.Foo",
|
||||
Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStruct.otherStringMap.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStructFORM: &nestedStruct{
|
||||
OtherFieldFORM: mixedValidationErrors,
|
||||
OtherStringMapFORM: map[string]*validatableStruct{
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-struct.other-field.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-struct.other-field",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-struct.other-string-map.world.Foo",
|
||||
Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-struct.other-string-map.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Validate(tt.obj)
|
||||
|
||||
if len(tt.wantErrs) == 0 {
|
||||
assert.Nil(t, err, "validation error should be nil")
|
||||
}
|
||||
|
||||
got := validate.Errors(err)
|
||||
assert.ElementsMatch(t, tt.wantErrs, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
161
validator.go
Normal file
161
validator.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
// FieldNameFunc is a function which converts a given reflect.StructField to a
|
||||
// string. The default will lookup json, yaml, and form field tags.
|
||||
type FieldNameFunc func(reflect.StructField) string
|
||||
|
||||
// FieldJoinFunc joins a path slice with a given field. Both path and field may
|
||||
// be empty values.
|
||||
type FieldJoinFunc func(path []string, field string) string
|
||||
|
||||
// Validator validates Validatable objects.
|
||||
type Validator struct {
|
||||
fieldName FieldNameFunc
|
||||
fieldJoin FieldJoinFunc
|
||||
}
|
||||
|
||||
// New creates a new Validator.
|
||||
func New() *Validator {
|
||||
return &Validator{}
|
||||
}
|
||||
|
||||
// Validate will validate the given object. Structs, maps, slices, and arrays
|
||||
// will have each of their fields/items validated, effectively performing a
|
||||
// deep-validation.
|
||||
func (s *Validator) Validate(data interface{}) error {
|
||||
if s.fieldName == nil {
|
||||
s.fieldName = DefaultFieldName
|
||||
}
|
||||
|
||||
if s.fieldJoin == nil {
|
||||
s.fieldJoin = DefaultFieldJoin
|
||||
}
|
||||
|
||||
return s.validate(nil, data)
|
||||
}
|
||||
|
||||
// FieldNameFunc allows setting a custom FieldNameFunc method. It receives a
|
||||
// reflect.StructField, and must return a string for the name of that field. If
|
||||
// the returned string is empty, validation will not run against the field's
|
||||
// value, or any nested data within.
|
||||
func (s *Validator) FieldNameFunc(f FieldNameFunc) {
|
||||
s.fieldName = f
|
||||
}
|
||||
|
||||
// FieldJoinFunc allows setting a custom FieldJoinFunc method. It receives a
|
||||
// string slice of parent fields, and a string of the field name the error is
|
||||
// reported against. All parent paths, must be joined with the current.
|
||||
func (s *Validator) FieldJoinFunc(f FieldJoinFunc) {
|
||||
s.fieldJoin = f
|
||||
}
|
||||
|
||||
func (s *Validator) validate(path []string, data interface{}) error {
|
||||
var errs error
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
d := reflect.ValueOf(data)
|
||||
if d.Kind() == reflect.Ptr {
|
||||
if d.IsNil() {
|
||||
return nil
|
||||
}
|
||||
d = d.Elem()
|
||||
}
|
||||
|
||||
if v, ok := data.(Validatable); ok {
|
||||
verrs := v.Validate()
|
||||
for _, err := range multierr.Errors(verrs) {
|
||||
// Create a new Error for all errors returned by Validate function
|
||||
// to correctly resolve field name, and also field path in relation
|
||||
// to parent objects being validated.
|
||||
newErr := &Error{}
|
||||
|
||||
e := &Error{}
|
||||
if ok := errors.As(err, &e); ok {
|
||||
field := e.Field
|
||||
if field != "" {
|
||||
if sf, ok := d.Type().FieldByName(e.Field); ok {
|
||||
field = s.fieldName(sf)
|
||||
}
|
||||
}
|
||||
newErr.Field = s.fieldJoin(path, field)
|
||||
newErr.Msg = e.Msg
|
||||
newErr.Err = e.Err
|
||||
} else {
|
||||
newErr.Field = s.fieldJoin(path, "")
|
||||
newErr.Err = err
|
||||
}
|
||||
|
||||
errs = multierr.Append(errs, newErr)
|
||||
}
|
||||
}
|
||||
|
||||
switch d.Kind() { //nolint:exhaustive
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < d.Len(); i++ {
|
||||
v := d.Index(i)
|
||||
err := s.validate(append(path, strconv.Itoa(i)), v.Interface())
|
||||
errs = multierr.Append(errs, err)
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, k := range d.MapKeys() {
|
||||
v := d.MapIndex(k)
|
||||
err := s.validate(append(path, fmt.Sprintf("%v", k)), v.Interface())
|
||||
errs = multierr.Append(errs, err)
|
||||
}
|
||||
case reflect.Struct:
|
||||
for i := 0; i < d.NumField(); i++ {
|
||||
v := d.Field(i)
|
||||
fldName := s.fieldName(d.Type().Field(i))
|
||||
if v.CanSet() && fldName != "" {
|
||||
err := s.validate(append(path, fldName), v.Interface())
|
||||
errs = multierr.Append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// DefaultFieldName is the default FieldNameFunc used by Validator.
|
||||
//
|
||||
// Uses json, yaml, and form field tags to lookup field name first.
|
||||
func DefaultFieldName(fld reflect.StructField) string {
|
||||
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
|
||||
|
||||
if name == "" {
|
||||
name = strings.SplitN(fld.Tag.Get("yaml"), ",", 2)[0]
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
name = strings.SplitN(fld.Tag.Get("form"), ",", 2)[0]
|
||||
}
|
||||
|
||||
if name == "-" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return fld.Name
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// DefaultFieldJoin is the default FieldJoinFunc used by Validator.
|
||||
func DefaultFieldJoin(path []string, field string) string {
|
||||
if field != "" {
|
||||
path = append(path, field)
|
||||
}
|
||||
|
||||
return strings.Join(path, ".")
|
||||
}
|
||||
86
validator_test.go
Normal file
86
validator_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//
|
||||
// Test helper types
|
||||
//
|
||||
|
||||
type testStruct struct {
|
||||
Foo string `json:"foo"`
|
||||
|
||||
f func() error
|
||||
}
|
||||
|
||||
func (s *testStruct) Validate() error {
|
||||
if s.f == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.f()
|
||||
}
|
||||
|
||||
type testNestedStruct struct {
|
||||
OtherField *testStruct `yaml:"other_field"`
|
||||
}
|
||||
|
||||
type MyStruct struct {
|
||||
Name string
|
||||
Kind string
|
||||
}
|
||||
|
||||
//
|
||||
// Tests
|
||||
//
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
got := New()
|
||||
|
||||
assert.IsType(t, &Validator{}, got)
|
||||
}
|
||||
|
||||
func TestValidator_FieldNameFunc(t *testing.T) {
|
||||
v := New()
|
||||
v.FieldNameFunc(func(sf reflect.StructField) string {
|
||||
return "<" + strings.ToUpper(sf.Name) + ">"
|
||||
})
|
||||
err := v.Validate(&testNestedStruct{
|
||||
OtherField: &testStruct{f: func() error {
|
||||
return &Error{Field: "Foo", Msg: "oops"}
|
||||
}},
|
||||
})
|
||||
|
||||
got := Errors(err)
|
||||
|
||||
assert.ElementsMatch(t, []error{
|
||||
&Error{Field: "<OTHERFIELD>.<FOO>", Msg: "oops"},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestValidator_FieldJoinFunc(t *testing.T) {
|
||||
v := New()
|
||||
v.FieldJoinFunc(func(path []string, field string) string {
|
||||
if field != "" {
|
||||
path = append(path, field)
|
||||
}
|
||||
|
||||
return "[" + strings.Join(path, "][") + "]"
|
||||
})
|
||||
err := v.Validate(&testNestedStruct{
|
||||
OtherField: &testStruct{f: func() error {
|
||||
return &Error{Field: "Foo", Msg: "oops"}
|
||||
}},
|
||||
})
|
||||
|
||||
got := Errors(err)
|
||||
|
||||
assert.ElementsMatch(t, []error{
|
||||
&Error{Field: "[other_field][foo]", Msg: "oops"},
|
||||
}, got)
|
||||
}
|
||||
Reference in New Issue
Block a user