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:
Tom Wiesing 2023-02-12 18:13:52 +01:00
parent 568c005d15
commit 945329a080
No known key found for this signature in database
70 changed files with 1150 additions and 350 deletions

View 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
}

View 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
View 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
}