mirror of
https://github.com/romdo/go-validate.git
synced 2026-02-19 08:06:40 +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:
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, ".")
|
||||
}
|
||||
Reference in New Issue
Block a user