mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-03 06:44:50 -04:00
validation: add generic validation framework (#2480)
* [wip] validation framework Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * [wip] wip Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * working for shallow structs!!! Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix needle pointer deref Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add comment Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix nested structs Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix nested struct pointers Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix slices / arrays Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix struct parsing Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * extend tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * expose API Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * extend in-package documentation Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * linter fixes Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix naming Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add missing license headers Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * align with review Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>
This commit is contained in:
parent
2f745a2edb
commit
a104936bc6
6 changed files with 1184 additions and 0 deletions
153
internal/validation/constraints.go
Normal file
153
internal/validation/constraints.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Constraint is a constraint on a document or a field of a document.
|
||||
type Constraint struct {
|
||||
// Satisfied returns no error if the constraint is satisfied.
|
||||
// Otherwise, it returns the reason why the constraint is not satisfied.
|
||||
Satisfied func() error
|
||||
}
|
||||
|
||||
/*
|
||||
WithFieldTrace adds a well-formatted trace to the field to the error message
|
||||
shown when the constraint is not satisfied. Both "doc" and "field" must be pointers:
|
||||
- "doc" must be a pointer to the top level document
|
||||
- "field" must be a pointer to the field to be validated
|
||||
|
||||
Example for a non-pointer field:
|
||||
|
||||
Equal(d.IntField, 42).WithFieldTrace(d, &d.IntField)
|
||||
|
||||
Example for a pointer field:
|
||||
|
||||
NotEmpty(d.StrPtrField).WithFieldTrace(d, d.StrPtrField)
|
||||
|
||||
Due to Go's addressability limititations regarding maps, if a map field is
|
||||
to be validated, WithMapFieldTrace must be used instead of WithFieldTrace.
|
||||
*/
|
||||
func (c *Constraint) WithFieldTrace(doc any, field any) Constraint {
|
||||
// we only want to dereference the needle once to dereference the pointer
|
||||
// used to pass it to the function without losing reference to it, as the
|
||||
// needle could be an arbitrarily long chain of pointers. The same
|
||||
// applies to the haystack.
|
||||
derefedField := pointerDeref(reflect.ValueOf(field))
|
||||
fieldRef := referenceableValue{
|
||||
value: derefedField,
|
||||
addr: derefedField.UnsafeAddr(),
|
||||
_type: derefedField.Type(),
|
||||
}
|
||||
derefedDoc := pointerDeref(reflect.ValueOf(doc))
|
||||
docRef := referenceableValue{
|
||||
value: derefedDoc,
|
||||
addr: derefedDoc.UnsafeAddr(),
|
||||
_type: derefedDoc.Type(),
|
||||
}
|
||||
return c.withTrace(docRef, fieldRef)
|
||||
}
|
||||
|
||||
/*
|
||||
WithMapFieldTrace adds a well-formatted trace to the map field to the error message
|
||||
shown when the constraint is not satisfied. Both "doc" and "field" must be pointers:
|
||||
- "doc" must be a pointer to the top level document
|
||||
- "field" must be a pointer to the map containing the field to be validated
|
||||
- "mapKey" must be the key of the field to be validated in the map pointed to by "field"
|
||||
|
||||
Example:
|
||||
|
||||
Equal(d.IntField, 42).WithMapFieldTrace(d, &d.MapField, mapKey)
|
||||
|
||||
For non-map fields, WithFieldTrace should be used instead of WithMapFieldTrace.
|
||||
*/
|
||||
func (c *Constraint) WithMapFieldTrace(doc any, field any, mapKey string) Constraint {
|
||||
// we only want to dereference the needle once to dereference the pointer
|
||||
// used to pass it to the function without losing reference to it, as the
|
||||
// needle could be an arbitrarily long chain of pointers. The same
|
||||
// applies to the haystack.
|
||||
derefedField := pointerDeref(reflect.ValueOf(field))
|
||||
fieldRef := referenceableValue{
|
||||
value: derefedField,
|
||||
addr: derefedField.UnsafeAddr(),
|
||||
_type: derefedField.Type(),
|
||||
mapKey: mapKey,
|
||||
}
|
||||
derefedDoc := pointerDeref(reflect.ValueOf(doc))
|
||||
docRef := referenceableValue{
|
||||
value: derefedDoc,
|
||||
addr: derefedDoc.UnsafeAddr(),
|
||||
_type: derefedDoc.Type(),
|
||||
}
|
||||
return c.withTrace(docRef, fieldRef)
|
||||
}
|
||||
|
||||
// withTrace wraps the constraint's error message with a well-formatted trace.
|
||||
func (c *Constraint) withTrace(docRef, fieldRef referenceableValue) Constraint {
|
||||
return Constraint{
|
||||
Satisfied: func() error {
|
||||
if err := c.Satisfied(); err != nil {
|
||||
return newError(docRef, fieldRef, err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MatchRegex is a constraint that if s matches regex.
|
||||
func MatchRegex(s string, regex string) *Constraint {
|
||||
return &Constraint{
|
||||
Satisfied: func() error {
|
||||
if !regexp.MustCompile(regex).MatchString(s) {
|
||||
return fmt.Errorf("%s must match the pattern %s", s, regex)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Equal is a constraint that if s is equal to t.
|
||||
func Equal[T comparable](s T, t T) *Constraint {
|
||||
return &Constraint{
|
||||
Satisfied: func() error {
|
||||
if s != t {
|
||||
return fmt.Errorf("%v must be equal to %v", s, t)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NotEmpty is a constraint that if s is not empty.
|
||||
func NotEmpty[T comparable](s T) *Constraint {
|
||||
return &Constraint{
|
||||
Satisfied: func() error {
|
||||
var zero T
|
||||
if s == zero {
|
||||
return fmt.Errorf("%v must not be empty", s)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Empty is a constraint that if s is empty.
|
||||
func Empty[T comparable](s T) *Constraint {
|
||||
return &Constraint{
|
||||
Satisfied: func() error {
|
||||
var zero T
|
||||
if s != zero {
|
||||
return fmt.Errorf("%v must be empty", s)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue