mirror of
https://github.com/romdo/go-validate.git
synced 2026-02-18 23:56:41 +00:00
When validating non-struct types, we should not try and convert field values in errors, because there is no struct field to lookup. Hence this fix stops a panic from happening. It is up to the Validate() method itself to provide the correct and final field value when validating non-struct types.
162 lines
4.0 KiB
Go
162 lines
4.0 KiB
Go
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 != "" && d.Kind() == reflect.Struct {
|
|
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, ".")
|
|
}
|