Files
go-validate/validator.go
Jim Myhrberg 464467ec86 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.
2021-08-22 21:53:02 +01:00

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, ".")
}