mirror of
https://github.com/romdo/go-validate.git
synced 2026-02-19 08:06:40 +00:00
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.
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 != "" {
|
|
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, ".")
|
|
}
|