Move to yaml-based configuration
This commit updates the configuration to be yaml-based and updates the configuration to read in a yaml file.
This commit is contained in:
parent
568c005d15
commit
945329a080
70 changed files with 1150 additions and 350 deletions
88
pkg/validator/validator.go
Normal file
88
pkg/validator/validator.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
validateTag = "validate"
|
||||
recurseTag = "recurse"
|
||||
dfltTag = "default"
|
||||
)
|
||||
|
||||
// Validate validates an object of type T, setting defaults where appropriate.
|
||||
//
|
||||
// T must be a struct type, when this is not the case, returns ErrNotAStruct.
|
||||
// validators should contain a set of validators.
|
||||
//
|
||||
// Validate iterates over the fields and tags of those fields as follows:
|
||||
// - If the 'validate' tag is not the empty string, read the appropriate validator from the map, and call the function.
|
||||
// If the element in the validators map does not exist, returns an error that unwraps to type UnknownValidator.
|
||||
// If the element in the validators map is not a validator, returns an error that unwraps to type NotAValidator.
|
||||
// If the type of validator function does not match the field type, returns an error that unwraps to type IncompatibleValidator.
|
||||
// - If the 'recurse' tag is not the empty string, recurse into the struct type by calling Validate on it.
|
||||
// If the annotated field is not a struct, return an error.
|
||||
//
|
||||
// Any error is wrapped in a FieldError, indicating the field they occured in.
|
||||
// Recursive validate calls may result in FieldError wraps.
|
||||
// For a description of struct tags, see [reflect.StructTag].
|
||||
func Validate[T any](data *T, validators map[string]any) error {
|
||||
return validate(reflect.ValueOf(data).Elem(), validators)
|
||||
}
|
||||
|
||||
// FieldError wraps an error to indicate which field it occured in.
|
||||
type FieldError struct {
|
||||
Field string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (fe FieldError) Error() string {
|
||||
return fmt.Sprintf("field %q: %s", fe.Field, fe.Err)
|
||||
}
|
||||
|
||||
func (fe FieldError) Unwrap() error {
|
||||
return fe.Err
|
||||
}
|
||||
|
||||
var ErrNotAStruct = errors.New("validate called on non-struct type")
|
||||
|
||||
func validate(datum reflect.Value, validators Collection) error {
|
||||
// make sure that we have a struct type
|
||||
typ := datum.Type()
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return ErrNotAStruct
|
||||
}
|
||||
|
||||
fieldC := typ.NumField()
|
||||
for i := 0; i < fieldC; i++ {
|
||||
field := typ.Field(i)
|
||||
|
||||
// if the recurse tag is set, do the recursion!
|
||||
if field.Tag.Get(recurseTag) != "" {
|
||||
if err := validate(datum.FieldByName(field.Name), validators); err != nil {
|
||||
return FieldError{Field: field.Name, Err: err}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// check if there is a validator associated with this tag
|
||||
// and if not, skip it!
|
||||
validator := field.Tag.Get(validateTag)
|
||||
if validator == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// call the actual validator
|
||||
if err := validators.Call(
|
||||
validator,
|
||||
datum.FieldByName(field.Name),
|
||||
field.Tag.Get(dfltTag),
|
||||
); err != nil {
|
||||
return FieldError{Field: field.Name, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
123
pkg/validator/validator_test.go
Normal file
123
pkg/validator/validator_test.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func ExampleValidate() {
|
||||
var value struct {
|
||||
Number int `validate:"positive" default:"234"`
|
||||
String string `validate:"nonempty" default:"stuff"`
|
||||
Recursive struct {
|
||||
Number int `validate:"positive" default:"45"`
|
||||
String string `validate:"nonempty" default:"more"`
|
||||
} `recurse:"true"`
|
||||
}
|
||||
|
||||
collection := make(Collection, 2)
|
||||
Add(collection, "positive", func(value *int, dflt string) error {
|
||||
if *value == 0 {
|
||||
i, err := strconv.ParseInt(dflt, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*value = int(i)
|
||||
return nil
|
||||
}
|
||||
if *value < 0 {
|
||||
return errors.New("not positive")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
Add(collection, "nonempty", func(value *string, dflt string) error {
|
||||
if *value == "" {
|
||||
*value = dflt
|
||||
}
|
||||
if *value == "" {
|
||||
return errors.New("empty string")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := Validate(&value, collection)
|
||||
fmt.Printf("%v\n", value)
|
||||
fmt.Println(err)
|
||||
// Output: {234 stuff {45 more}}
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleValidate_fail() {
|
||||
var value struct {
|
||||
Number int `validate:"positive" default:"12"`
|
||||
String string `validate:"nonempty" default:"stuff"`
|
||||
Recursive struct {
|
||||
Number int `validate:"positive" default:"12"`
|
||||
String string `validate:"nonempty"`
|
||||
} `recurse:"true"`
|
||||
}
|
||||
|
||||
collection := make(Collection, 2)
|
||||
Add(collection, "positive", func(value *int, dflt string) error {
|
||||
if *value == 0 {
|
||||
i, err := strconv.ParseInt(dflt, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*value = int(i)
|
||||
return nil
|
||||
}
|
||||
if *value < 0 {
|
||||
return errors.New("not positive")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
Add(collection, "nonempty", func(value *string, dflt string) error {
|
||||
if *value == "" {
|
||||
*value = dflt
|
||||
}
|
||||
if *value == "" {
|
||||
return errors.New("empty string")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := Validate(&value, collection)
|
||||
fmt.Printf("%v\n", value)
|
||||
fmt.Println(err)
|
||||
// Output: {12 stuff {12 }}
|
||||
// field "Recursive": field "String": empty string
|
||||
}
|
||||
|
||||
func ExampleValidate_notastruct() {
|
||||
var value int
|
||||
err := Validate(&value, nil)
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
func ExampleValidate_notavalidator() {
|
||||
var value struct {
|
||||
Field int `validate:"generic"`
|
||||
}
|
||||
collection := make(Collection, 2)
|
||||
collection["generic"] = func(x, y int) error {
|
||||
panic("never reached")
|
||||
}
|
||||
err := Validate(&value, collection)
|
||||
fmt.Println(err)
|
||||
// Output: field "Field": entry "generic" in validators is not a valiator
|
||||
}
|
||||
|
||||
func ExampleValidate_invalid() {
|
||||
var value struct {
|
||||
Field int `validate:"string"`
|
||||
}
|
||||
collection := make(Collection, 2)
|
||||
collection["string"] = func(value *string, dflt string) error {
|
||||
panic("never reached")
|
||||
}
|
||||
err := Validate(&value, collection)
|
||||
fmt.Println(err)
|
||||
// Output: field "Field": validator "string": got type string, expected type int
|
||||
}
|
||||
131
pkg/validator/vmap.go
Normal file
131
pkg/validator/vmap.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/tkw1536/goprogram/lib/reflectx"
|
||||
)
|
||||
|
||||
// Collection represents a set of validators.
|
||||
// The zero value is not ready to use; it should be created using make().
|
||||
//
|
||||
// A validator is a non-nil function with signature func(value *F, dflt string) error.
|
||||
// Here F is the type of a value of a field.
|
||||
// The value is the initialized value to be validated.
|
||||
// The validator may perform abitrary normalization on the value.
|
||||
// dflt is the default value (read from the 'default' tag).
|
||||
// error should be an appropriate error that occured.
|
||||
//
|
||||
// A validator function is applied by calling it.
|
||||
type Collection map[string]any
|
||||
|
||||
// Add adds a Validator to the provided collection of validators.
|
||||
// Any previously validator of the same name is overwritten.
|
||||
func Add[F any](coll Collection, name string, validator func(value *F, dflt string) error) {
|
||||
coll[name] = validator
|
||||
}
|
||||
|
||||
// AddSlice adds a Validator to the provided collection of validators that validates a slice of the given type. The default is seperated by seperator.
|
||||
func AddSlice[F any](coll Collection, name string, sep string, validator func(value *F, dflt string) error) {
|
||||
Add(coll, name, func(value *[]F, dflt string) error {
|
||||
// some value is set, so we do not need to set the default!
|
||||
if *value != nil {
|
||||
for i := range *value {
|
||||
if err := validator(&(*value)[i], ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no default provided => set if to an empty slice
|
||||
if dflt == "" {
|
||||
*value = make([]F, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// some default provided => iterate over the underlying validator
|
||||
dflts := strings.Split(dflt, sep)
|
||||
*value = make([]F, len(dflts))
|
||||
for i := range *value {
|
||||
if err := validator(&(*value)[i], dflts[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
errTyp = reflectx.TypeOf[error]()
|
||||
strTyp = reflectx.TypeOf[string]()
|
||||
)
|
||||
|
||||
// UnknownValidator is an error returned from Validate if a validator does not exist
|
||||
type UnknownValidator string
|
||||
|
||||
func (uv UnknownValidator) Error() string {
|
||||
return fmt.Sprintf("unknown validator %q", string(uv))
|
||||
}
|
||||
|
||||
// NotAValidator is an error returned from Validate if an entry in the validators map is not a validator
|
||||
type NotAValidator string
|
||||
|
||||
func (nv NotAValidator) Error() string {
|
||||
return fmt.Sprintf("entry %q in validators is not a valiator", string(nv))
|
||||
}
|
||||
|
||||
// IncompatibleValidator is returned when a validator in the validators map is incompatible
|
||||
type IncompatibleValidator struct {
|
||||
Validator string
|
||||
GotType reflect.Type
|
||||
ExpectedType reflect.Type
|
||||
}
|
||||
|
||||
func (iv IncompatibleValidator) Error() string {
|
||||
return fmt.Sprintf("validator %q: got type %s, expected type %s", iv.Validator, iv.GotType, iv.ExpectedType)
|
||||
}
|
||||
|
||||
// Call calls the validator with the given name, on the given value, and with the provided default.
|
||||
// See documentation of [Validate] for details.
|
||||
func (coll Collection) Call(name string, field reflect.Value, dflt string) error {
|
||||
validator, ok := coll[name]
|
||||
if !ok {
|
||||
return UnknownValidator(name)
|
||||
}
|
||||
|
||||
// get the type of the validator
|
||||
vFunc := reflect.ValueOf(validator)
|
||||
vTyp := vFunc.Type()
|
||||
|
||||
// ensure that vTyp is of type func(*F,string) error
|
||||
// where T is the type of the field
|
||||
//
|
||||
// - the first if assumes checks for some type F
|
||||
// - the second if checks if the F is the right one
|
||||
if validator == nil || vTyp.Kind() != reflect.Func || // func
|
||||
vTyp.NumIn() != 2 || vTyp.In(0).Kind() != reflect.Pointer || vTyp.In(1) != strTyp || // (*F,string)
|
||||
vTyp.NumOut() != 1 || vTyp.Out(0) != errTyp { // error
|
||||
return NotAValidator(name)
|
||||
}
|
||||
if vTyp.In(0).Elem() != field.Type() { // the correct *F
|
||||
return IncompatibleValidator{
|
||||
Validator: name,
|
||||
GotType: vTyp.In(0).Elem(),
|
||||
ExpectedType: field.Type(),
|
||||
}
|
||||
}
|
||||
|
||||
// call the validator function, and return an error
|
||||
results := vFunc.Call([]reflect.Value{field.Addr(), reflect.ValueOf(dflt)})
|
||||
|
||||
// turn the result into an error
|
||||
// NOTE: We can't just .(error) here because that panic()s on err == nil
|
||||
err := results[0].Interface()
|
||||
if err, ok := err.(error); ok {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue