internal/validation/runner.go (101 lines of code) (raw):

package validation import ( "context" "errors" "reflect" "strings" ) // Validatable is anything that can be validated. type Validatable[O any] interface { DeepCopy() O } // Validation validates the system for a type O. type Validation[O Validatable[O]] struct { Name string Validate Validate[O] } func New[O Validatable[O]](name string, validate Validate[O]) Validation[O] { return Validation[O]{Name: name, Validate: validate} } // Validate is the logic for a validation of a type O. type Validate[O Validatable[O]] func(ctx context.Context, informer Informer, obj O) error // Runner allows to compose and run validations. type Runner[O Validatable[O]] struct { validations []Validation[O] informer Informer config RunnerConfig } type Informer interface { Starting(ctx context.Context, name, message string) Done(ctx context.Context, name string, err error) } // RunnerConfig holds the configuration for the Runner. type RunnerConfig struct { skipValidations []string } // RunnerOpt allows to configure the Runner. type RunnerOpt func(*RunnerConfig) // WithSkipValidation configures the runner to skip // the validations with the given names. func WithSkipValidations(namesToSkip ...string) RunnerOpt { return func(c *RunnerConfig) { c.skipValidations = append(c.skipValidations, namesToSkip...) } } // NewRunner constructs a new Runner. func NewRunner[O Validatable[O]](informer Informer, opts ...RunnerOpt) *Runner[O] { r := &Runner[O]{ informer: informer, } for _, opt := range opts { opt(&r.config) } return r } // Register adds validations to the Runner. func (r *Runner[O]) Register(validations ...Validation[O]) { for _, v := range validations { if r.shouldRegister(v.Name) { r.validations = append(r.validations, v) } } } // Sequentially runs all validations one after the other and waits until they all finish, // aggregating the errors if present. obj must not be modified. If it is, this // indicates a programming error and the method will panic. func (r *Runner[O]) Sequentially(ctx context.Context, obj O) error { copyObj := obj.DeepCopy() var errs []error for _, validation := range r.validations { err := validation.Validate(ctx, r.informer, copyObj) if err != nil { errs = append(errs, Unwrap(err)...) } } if !reflect.DeepEqual(obj, copyObj) { panic("validations must not modify the object under validation") } return errors.Join(errs...) } func (r *Runner[O]) UntilError(validations ...Validation[O]) Validation[O] { var accepted []Validate[O] var names []string for _, v := range validations { if r.shouldRegister(v.Name) { accepted = append(accepted, v.Validate) names = append(names, v.Name) } } return New("until-error-"+strings.Join(names, "/"), UntilError(accepted...)) } func (r *Runner[O]) shouldRegister(name string) bool { for _, skip := range r.config.skipValidations { if skip == name { return false } } return true } // Unwrap unfolds and flattens errors if err implements Unwrap []error. // If it doesn't implement it, it just returns a slice with one single error. func Unwrap(err error) []error { if agg, ok := err.(interface{ Unwrap() []error }); ok { return agg.Unwrap() } return []error{err} } // UntilError returns a composed validate that runs all validations until one fails. func UntilError[O Validatable[O]](validates ...Validate[O]) Validate[O] { return func(ctx context.Context, informer Informer, obj O) error { for _, v := range validates { if err := v(ctx, informer, obj); err != nil { return err } } return nil } }