Skip to content

Commit

Permalink
declare the function once
Browse files Browse the repository at this point in the history
  • Loading branch information
nieomylnieja committed Oct 4, 2023
1 parent 907d1f9 commit eb60608
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 58 deletions.
51 changes: 26 additions & 25 deletions manifest/v1alpha/project/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,33 @@ import (
"github.com/nobl9/nobl9-go/validation"
)

var validateProject = validation.RulesForStruct[Project](
// Example: my-name
validation.RulesForField(
"metadata.name",
func(p Project) string { return p.Metadata.Name },
).With(
validation.StringRequired(),
validation.StringIsDNSSubdomain()),
// Example: My Name!
validation.RulesForField(
"metadata.displayName",
func(p Project) string { return p.Metadata.DisplayName },
).With(validation.StringLength(0, 63)),
// Example: this:that,hey=lol
validation.RulesForField(
"metadata.labels",
func(p Project) v1alpha.Labels { return p.Metadata.Labels },
).With(v1alpha.ValidationRuleLabels()),
// Example: Very long description
validation.RulesForField(
"spec.description",
func(p Project) string { return p.Spec.Description },
).With(validation.StringDescription()),
).Validate

func validate(p Project) error {
v := validation.RulesForStruct(
validation.RulesForField(
"metadata.name",
func() string { return p.Metadata.Name },
).
With(
validation.StringRequired(),
validation.StringIsDNSSubdomain()),
validation.RulesForField(
"metadata.displayName",
func() string { return p.Metadata.DisplayName },
).
With(validation.StringLength(0, 63)),
validation.RulesForField(
"metadata.labels",
func() v1alpha.Labels { return p.Metadata.Labels },
).
With(v1alpha.ValidationRuleLabels()),
validation.RulesForField(
"spec.description",
func() string { return p.Spec.Description },
).
With(validation.StringDescription()),
)
if errs := v.Validate(); len(errs) > 0 {
if errs := validateProject(p); len(errs) > 0 {
return v1alpha.NewObjectError(p, errs)
}
return nil
Expand Down
34 changes: 17 additions & 17 deletions validation/rules.go
Original file line number Diff line number Diff line change
@@ -1,47 +1,47 @@
package validation

type fieldRules interface {
Validate() error
type fieldRules[S any] interface {
Validate(s S) error
}

func RulesForStruct(rules ...fieldRules) StructRules {
return StructRules{fieldRules: rules}
func RulesForStruct[S any](rules ...fieldRules[S]) StructRules[S] {
return StructRules[S]{fieldRules: rules}
}

type StructRules struct {
fieldRules []fieldRules
type StructRules[S any] struct {
fieldRules []fieldRules[S]
}

func (s StructRules) Validate() []error {
func (r StructRules[S]) Validate(st S) []error {
var errors []error
for _, field := range s.fieldRules {
if err := field.Validate(); err != nil {
for _, field := range r.fieldRules {
if err := field.Validate(st); err != nil {
errors = append(errors, err)
}
}
return errors
}

// RulesForField creates a typed FieldRules instance for the field which access is defined through getter function.
func RulesForField[T any](fieldPath string, getter func() T) FieldRules[T] {
return FieldRules[T]{fieldPath: fieldPath, getter: getter}
func RulesForField[T, S any](fieldPath string, getter func(S) T) FieldRules[T, S] {
return FieldRules[T, S]{fieldPath: fieldPath, getter: getter}
}

// FieldRules is responsible for validating a single struct field.
type FieldRules[T any] struct {
type FieldRules[T, S any] struct {
fieldPath string
getter func() T
getter func(S) T
rules []Rule[T]
predicates []func() bool
}

func (r FieldRules[T]) Validate() error {
func (r FieldRules[T, S]) Validate(st S) error {
for _, pred := range r.predicates {
if pred != nil && !pred() {
return nil
}
}
fv := r.getter()
fv := r.getter(st)
var errors []error
for i := range r.rules {
if err := r.rules[i].Validate(fv); err != nil {
Expand All @@ -54,12 +54,12 @@ func (r FieldRules[T]) Validate() error {
return nil
}

func (r FieldRules[T]) If(predicate func() bool) FieldRules[T] {
func (r FieldRules[T, S]) If(predicate func() bool) FieldRules[T, S] {
r.predicates = append(r.predicates, predicate)
return r
}

func (r FieldRules[T]) With(rules ...Rule[T]) FieldRules[T] {
func (r FieldRules[T, S]) With(rules ...Rule[T]) FieldRules[T, S] {
r.rules = append(r.rules, rules...)
return r
}
34 changes: 18 additions & 16 deletions validation/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,26 @@ import (

func TestRulesForStruct(t *testing.T) {
t.Run("no errors", func(t *testing.T) {
r := RulesForStruct(
RulesForField[string]("test", func() string { return "test" }).
r := RulesForStruct[mockStruct](
RulesForField[string]("test", func(m mockStruct) string { return "test" }).
With(SingleRule[string](func(v string) error { return nil })),
)
errs := r.Validate()
errs := r.Validate(mockStruct{})
assert.Empty(t, errs)
})

t.Run("errors", func(t *testing.T) {
err1 := errors.New("1")
err2 := errors.New("2")
r := RulesForStruct(
RulesForField[string]("test", func() string { return "test" }).
r := RulesForStruct[mockStruct](
RulesForField[string]("test", func(m mockStruct) string { return "test" }).
With(SingleRule[string](func(v string) error { return nil })),
RulesForField[string]("test.name", func() string { return "name" }).
RulesForField[string]("test.name", func(m mockStruct) string { return "name" }).
With(SingleRule[string](func(v string) error { return err1 })),
RulesForField[string]("test.display", func() string { return "display" }).
RulesForField[string]("test.display", func(m mockStruct) string { return "display" }).
With(SingleRule[string](func(v string) error { return err2 })),
)
errs := r.Validate()
errs := r.Validate(mockStruct{})
require.Len(t, errs, 2)
assert.Equal(t, []error{
&FieldError{
Expand All @@ -48,17 +48,17 @@ func TestRulesForStruct(t *testing.T) {

func TestRulesForField(t *testing.T) {
t.Run("no predicates, no error", func(t *testing.T) {
r := RulesForField[string]("test.path", func() string { return "path" }).
r := RulesForField[string]("test.path", func(m mockStruct) string { return "path" }).
With(SingleRule[string](func(v string) error { return nil }))
err := r.Validate()
err := r.Validate(mockStruct{})
assert.NoError(t, err)
})

t.Run("no predicates, validate", func(t *testing.T) {
expectedErr := errors.New("ops!")
r := RulesForField[string]("test.path", func() string { return "path" }).
r := RulesForField[string]("test.path", func(m mockStruct) string { return "path" }).
With(SingleRule[string](func(v string) error { return expectedErr }))
err := r.Validate()
err := r.Validate(mockStruct{})
require.Error(t, err)
assert.Equal(t, FieldError{
FieldPath: "test.path",
Expand All @@ -68,24 +68,24 @@ func TestRulesForField(t *testing.T) {
})

t.Run("predicate matches, don't validate", func(t *testing.T) {
r := RulesForField[string]("test.path", func() string { return "value" }).
r := RulesForField[string]("test.path", func(m mockStruct) string { return "value" }).
If(func() bool { return true }).
If(func() bool { return true }).
If(func() bool { return false }).
With(SingleRule[string](func(v string) error { return errors.New("ops!") }))
err := r.Validate()
err := r.Validate(mockStruct{})
assert.NoError(t, err)
})

t.Run("multiple rules", func(t *testing.T) {
err1 := errors.New("oh no!")
err2 := errors.New("ops!")
r := RulesForField[string]("test.path", func() string { return "value" }).
r := RulesForField[string]("test.path", func(m mockStruct) string { return "value" }).
With(SingleRule[string](func(v string) error { return nil })).
With(SingleRule[string](func(v string) error { return err1 })).
With(SingleRule[string](func(v string) error { return nil })).
With(SingleRule[string](func(v string) error { return err2 }))
err := r.Validate()
err := r.Validate(mockStruct{})
require.Error(t, err)
assert.Equal(t, FieldError{
FieldPath: "test.path",
Expand All @@ -94,3 +94,5 @@ func TestRulesForField(t *testing.T) {
}, *err.(*FieldError))
})
}

type mockStruct struct{}

0 comments on commit eb60608

Please sign in to comment.