-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
validation: add generic validation framework (#2480)
* [wip] validation framework Signed-off-by: Moritz Sanft <[email protected]> * [wip] wip Signed-off-by: Moritz Sanft <[email protected]> * working for shallow structs!!! Signed-off-by: Moritz Sanft <[email protected]> * fix needle pointer deref Signed-off-by: Moritz Sanft <[email protected]> * add comment Signed-off-by: Moritz Sanft <[email protected]> * fix nested structs Signed-off-by: Moritz Sanft <[email protected]> * fix nested struct pointers Signed-off-by: Moritz Sanft <[email protected]> * add tests Signed-off-by: Moritz Sanft <[email protected]> * fix slices / arrays Signed-off-by: Moritz Sanft <[email protected]> * fix struct parsing Signed-off-by: Moritz Sanft <[email protected]> * extend tests Signed-off-by: Moritz Sanft <[email protected]> * expose API Signed-off-by: Moritz Sanft <[email protected]> * extend in-package documentation Signed-off-by: Moritz Sanft <[email protected]> * linter fixes Signed-off-by: Moritz Sanft <[email protected]> * fix naming Signed-off-by: Moritz Sanft <[email protected]> * add missing license headers Signed-off-by: Moritz Sanft <[email protected]> * Apply suggestions from code review Co-authored-by: Daniel Weiße <[email protected]> * align with review Signed-off-by: Moritz Sanft <[email protected]> --------- Signed-off-by: Moritz Sanft <[email protected]> Co-authored-by: Daniel Weiße <[email protected]>
- Loading branch information
1 parent
2f745a2
commit a104936
Showing
6 changed files
with
1,184 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
load("@io_bazel_rules_go//go:def.bzl", "go_library") | ||
load("//bazel/go:go_test.bzl", "go_test") | ||
|
||
go_library( | ||
name = "validation", | ||
srcs = [ | ||
"constraints.go", | ||
"errors.go", | ||
"validation.go", | ||
], | ||
importpath = "github.com/edgelesssys/constellation/v2/internal/validation", | ||
visibility = ["//:__subpackages__"], | ||
) | ||
|
||
go_test( | ||
name = "validation_test", | ||
srcs = [ | ||
"errors_test.go", | ||
"validation_test.go", | ||
], | ||
embed = [":validation"], | ||
deps = [ | ||
"@com_github_stretchr_testify//assert", | ||
"@com_github_stretchr_testify//require", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}, | ||
} | ||
} |
Oops, something went wrong.