Skip to content

Commit

Permalink
validation: add generic validation framework (#2480)
Browse files Browse the repository at this point in the history
* [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
msanft and daniel-weisse authored Oct 24, 2023
1 parent 2f745a2 commit a104936
Show file tree
Hide file tree
Showing 6 changed files with 1,184 additions and 0 deletions.
26 changes: 26 additions & 0 deletions internal/validation/BUILD.bazel
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",
],
)
153 changes: 153 additions & 0 deletions internal/validation/constraints.go
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
},
}
}
Loading

0 comments on commit a104936

Please sign in to comment.