From bc777c9665f6c5cc9b4e19fa5d080605aac44f0a Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Tue, 1 Jun 2021 22:12:32 +0200 Subject: [PATCH 1/6] Add first steps of Validation implementation Signed-off-by: Guillaume Lours --- api/control/validation.go2 | 126 ++++++++++++++++++++++++++++ api/control/validation_test.go2 | 142 ++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 api/control/validation.go2 create mode 100644 api/control/validation_test.go2 diff --git a/api/control/validation.go2 b/api/control/validation.go2 new file mode 100644 index 0000000..87d25a1 --- /dev/null +++ b/api/control/validation.go2 @@ -0,0 +1,126 @@ +// Package control provides control structures such as Option, Try or Either... +package control + +// Validation control type returns a valid value of type T or all errors accumulated in a value of type T. +type Validation[E, T any] interface { + IsValid() bool + IsInvalid() bool + OrElse(value T) T + ErrorOrElse(err E) E + Swap() Validation[T, E] + ToEither() Either[E, T] +} + +// ValidOf returns a Validation[E,T] with a valid value T. +func ValidOf[E, T any](value T) Validation[E, T] { + return Valid[E, T]{value} +} + +// InvalidOf returns a Validation[E,T] with a invalid value E. +func InvalidOf[E, T any](err E) Validation[E, T] { + return Invalid[E, T]{err} +} + +// FromTry returns a Validation[error, T] with a T value if Try is a success or +// returns an Invalid instance if Try is a Failure. +func FromTry[E error, T any](try Try[T]) Validation[error, T] { + if try.IsFailure() { + _, err := try.OrElseCause() + return InvalidOf[error, T](err) + } + return ValidOf[error, T](try.OrElse(*new(T))) +} + +// FromEither returns a Validation[E, T] with the T value if Either is Right or E value if Left. +func FromEither[E, T any](either Either[E, T]) Validation[E, T] { + if either.IsRight() { + return ValidOf[E, T](either.GetOrElse(*new(T))) + } + return InvalidOf[E, T](either.GetLeftOrElse(*new(E))) +} + +// Valid is an implementation of Validation with a valid T value. +type Valid[E, T any] struct { + value T +} + +// IsValid checks if the current validation is valid or not. +// Valid implementation always returns true +func (v Valid[E, T]) IsValid() bool { + return true +} + +// IsInvalid checks if the current validation is invalid or not. +// Valid implementation always returns false +func (v Valid[E, T]) IsInvalid() bool { + return false +} + +// OrElse returns the value of the current Validation if valid or the value passed as parameter for invalid one. +// Valid implementation always returns the value of the Validation. +func (v Valid[E, T]) OrElse(value T) T { + return v.value +} + +// ErrorOrElse returns the "error" E of the current Validation if invalid +// or the value passed as parameter if the Validation is a valid one. +// Valid implementation always returns the value of type E passed as parameter. +func (v Valid[E, T]) ErrorOrElse(err E) E { + return err +} + +// Swap converts a Valid Validation to an Invalid one and vis versa. +// Valid implementation returns a new Invalid Validation setup with the previous valid value. +func (v Valid[E, T]) Swap() Validation[T, E] { + return InvalidOf[T, E](v.value) +} + +// ToEither returns an Either with a right value if the Validation is Valid +// Or an Either with a left value if the Validation is invalid. +// Valid implementation returns a Right Either with a value of type T +func (v Valid[E, T]) ToEither() Either[E, T] { + return RightOf[E, T](v.value) +} + +// Invalid is an implementation of Validation with a invalid E value. +type Invalid[E, T any] struct { + error E +} + +// IsValid checks if the current validation is valid or not. +// Invalid implementation always returns false +func (i Invalid[E, T]) IsValid() bool { + return false +} + +// IsInvalid checks if the current validation is invalid or not. +// Invalid implementation always returns true +func (i Invalid[E, T]) IsInvalid() bool { + return true +} + +// OrElse returns the value of the current Validation if valid or the value passed as parameter for invalid one. +// Invalid implementation always returns the value passed as parameter. +func (i Invalid[E, T]) OrElse(value T) T { + return value +} + +// ErrorOrElse returns the "error" E of the current Validation if invalid +// or the value passed as parameter if the Validation is a valid one. +// Invalid implementation always returns the value of the current Validation. +func (i Invalid[E, T]) ErrorOrElse(err E) E { + return i.error +} + +// Swap converts a Valid Validation to an Invalid one and vis versa. +// Invalid implementation returns a new Valid Validation setup with the previous invalid value. +func (i Invalid[E, T]) Swap() Validation[T, E] { + return ValidOf[T, E](i.error) +} + +// ToEither returns an Either with a right value if the Validation is Valid +// Or an Either with a left value if the Validation is invalid. +// Invalid implementation returns a Left Either with a value of type E +func (i Invalid[E, T]) ToEither() Either[E, T] { + return LeftOf[E, T](i.error) +} diff --git a/api/control/validation_test.go2 b/api/control/validation_test.go2 new file mode 100644 index 0000000..fb96e87 --- /dev/null +++ b/api/control/validation_test.go2 @@ -0,0 +1,142 @@ +package control + +import ( + "errors" + "fmt" + "regexp" + "testing" + "time" +) + +type User struct { + firstName string + lastName string + birthdate time.Time +} + +type UserValidator struct { + isAlpha *regexp.Regexp + minAge int +} + +func (u *UserValidator) validateUser(fistName string, lastName string, birthdate time.Time) { + +} + +func (u *UserValidator) validateFirstName(firstName string) Validation[error, string] { + if u.isAlpha.MatchString(firstName) { + return ValidOf[error, string](firstName) + } + return InvalidOf[error, string](fmt.Errorf("user first name is not valid %s", firstName)) +} + +func (u *UserValidator) validateLastName(lastName string) Validation[error, string] { + if u.isAlpha.MatchString(lastName) { + return ValidOf[error, string](lastName) + } + return InvalidOf[error, string](fmt.Errorf("user last name is not valid %s", lastName)) +} + +func (u *UserValidator) validateAge(birthdate time.Time) Validation[error, time.Time] { + age := (time.Now().Sub(birthdate).Hours() / 24) / 365 + if int(age) >= u.minAge { + return ValidOf[error, time.Time](birthdate) + } + return InvalidOf[error, time.Time](fmt.Errorf("user is to young %d", int(age))) +} + +func NewUserValidator() UserValidator { + return UserValidator{ + isAlpha: regexp.MustCompile(`^[a-zA-Z]+$`), + minAge: 18, + } +} + +var ( + defaultError = errors.New("default error Validation") + _ Validation[error, int] = Valid[error, int]{10} + _ Validation[error, int] = Invalid[error, int]{defaultError} + valid Validation[error, int] = ValidOf[error, int](10) + invalid Validation[error, int] = InvalidOf[error, int](defaultError) +) + +func TestIsValid(t *testing.T) { + if !valid.IsValid() { + t.Errorf("should be Valid not Invalid") + } + + if invalid.IsValid() { + t.Errorf("should be Invalid not Valid") + } +} + +func TestIsInvalid(t *testing.T) { + if valid.IsInvalid() { + t.Errorf("should be Valid not Invalid") + } + + if !invalid.IsInvalid() { + t.Errorf("should be Invalid not Valid") + } +} + +func TestValidationOrElse(t *testing.T) { + if valid.OrElse(20) != 10 { + t.Errorf("value should be 10") + } + + if invalid.OrElse(20) != 20 { + t.Errorf("value should be 20") + } +} + +func TestValidationErrorOrElse(t *testing.T) { + localError := errors.New("error for ErrorOrElse") + if err := valid.ErrorOrElse(localError); err != localError { + t.Errorf("error should be %s but is %s", localError.Error(), err) + } + + if err := invalid.ErrorOrElse(localError); err == localError { + t.Errorf("error should be %s but is %s", defaultError.Error(), err) + } +} + +func TestValidationSwap(t *testing.T) { + if valid.Swap().IsValid() { + t.Errorf("should be Invalid not Valid") + } + + if invalid.Swap().IsInvalid() { + t.Errorf("should be Valid not Invalid") + } +} + +func TestFromTry(t *testing.T) { + if FromTry[error, int](success).IsInvalid() { + t.Errorf("should be Valid not Invalid") + } + + if FromTry[error, int](failure).IsValid() { + t.Errorf("should be Invalid not Valid") + } +} + +func TestFromEither(t *testing.T) { + if FromEither[error, int](right).IsInvalid() { + t.Errorf("should be Valid not Invalid") + } + + if FromEither[error, int](left).IsValid() { + t.Errorf("should be Invalid not Valid") + } +} + +func TestToEither(t *testing.T) { + if valid.ToEither().IsLeft() { + t.Errorf("should be Right not Left") + } + + if invalid.ToEither().IsRight() { + t.Errorf("should be Left not Right") + } +} From 9e3ea744b709f8ef76fcbeccbd8835bf1731b5e2 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Tue, 1 Jun 2021 23:17:36 +0200 Subject: [PATCH 2/6] Add filter function to Validation type Signed-off-by: Guillaume Lours --- api/control/validation.go2 | 15 +++++++++++++++ api/control/validation_test.go2 | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/api/control/validation.go2 b/api/control/validation.go2 index 87d25a1..068fbf3 100644 --- a/api/control/validation.go2 +++ b/api/control/validation.go2 @@ -9,6 +9,7 @@ type Validation[E, T any] interface { ErrorOrElse(err E) E Swap() Validation[T, E] ToEither() Either[E, T] + Filter(func(T) bool) Option[Validation[E, T]] } // ValidOf returns a Validation[E,T] with a valid value T. @@ -82,6 +83,14 @@ func (v Valid[E, T]) ToEither() Either[E, T] { return RightOf[E, T](v.value) } +// Filter returns a Some Option with the current Validation if value matches the predicate and an Empty Option otherwise. +func (v Valid[E, T]) Filter(predicate func(T) bool) Option[Validation[E, T]] { + if predicate(v.value) { + return Of[Validation[E, T]](v) + } + return Empty[Validation[E, T]]() +} + // Invalid is an implementation of Validation with a invalid E value. type Invalid[E, T any] struct { error E @@ -124,3 +133,9 @@ func (i Invalid[E, T]) Swap() Validation[T, E] { func (i Invalid[E, T]) ToEither() Either[E, T] { return LeftOf[E, T](i.error) } + +// Filter returns a Some Option with the current Validation if value matches the predicate and an Empty Option otherwise. +// Invalid implementation always return Some Option. +func (i Invalid[E, T]) Filter(func(T) bool) Option[Validation[E, T]] { + return Of[Validation[E, T]](i) +} diff --git a/api/control/validation_test.go2 b/api/control/validation_test.go2 index fb96e87..53fb307 100644 --- a/api/control/validation_test.go2 +++ b/api/control/validation_test.go2 @@ -140,3 +140,18 @@ func TestToEither(t *testing.T) { t.Errorf("should be Left not Right") } } + +func TestValidationFilter(t *testing.T) { + if valid.Filter(EvenPredicate).IsEmpty() { + t.Error("should be a Some of Validation") + } + + if invalid.Filter(EvenPredicate).IsEmpty() { + t.Error("should be a Some of Validation") + } + + odd := ValidOf[error, int](11) + if !odd.Filter(EvenPredicate).IsEmpty() { + t.Error("should be a Empty of Validation") + } +} From 6cdec0824fed658bc44b2c5c3962da9d58f8d09b Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Tue, 1 Jun 2021 23:32:33 +0200 Subject: [PATCH 3/6] Add MapValidation to Validation type Signed-off-by: Guillaume Lours --- api/control/validation.go2 | 9 +++++++++ api/control/validation_test.go2 | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/api/control/validation.go2 b/api/control/validation.go2 index 068fbf3..4a67f9c 100644 --- a/api/control/validation.go2 +++ b/api/control/validation.go2 @@ -40,6 +40,15 @@ func FromEither[E, T any](either Either[E, T]) Validation[E, T] { return InvalidOf[E, T](either.GetLeftOrElse(*new(E))) } +// MapValidation maps the valid value of a Validation[E, T] to a new Validation with a valid element of type U. +// the mapper function should take a T value and return a U value. +func MapValidation[E, T, U any](validation Validation[E, T], mapper func(T) U) Validation[E, U] { + if validation.IsValid() { + return Valid[E, U]{mapper(validation.OrElse(*new(T)))} + } + return Invalid[E, U]{validation.ErrorOrElse(*new(E))} +} + // Valid is an implementation of Validation with a valid T value. type Valid[E, T any] struct { value T diff --git a/api/control/validation_test.go2 b/api/control/validation_test.go2 index 53fb307..5549dc1 100644 --- a/api/control/validation_test.go2 +++ b/api/control/validation_test.go2 @@ -4,6 +4,7 @@ import ( "errors" "fmt" "regexp" + "strconv" "testing" "time" ) @@ -155,3 +156,18 @@ func TestValidationFilter(t *testing.T) { t.Error("should be a Empty of Validation") } } + +func TestMapValidation(t *testing.T) { + var mapper = func(value int) string { + return strconv.Itoa(value) + } + var mapValid Validation[error, string] = MapValidation[error, int, string](valid, mapper) + if mapValid.OrElse("good") != "10" { + t.Errorf("value should be 10") + } + + var mapInvalid Validation[error, string] = MapValidation[error, int, string](invalid, mapper) + if mapInvalid.IsValid() { + t.Errorf("should be an Invalid Validation") + } +} From ec1755c24f3b020064987c3e4632366a80b8fd23 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Wed, 2 Jun 2021 22:32:55 +0200 Subject: [PATCH 4/6] Add MapErrorValidation to Validation type Signed-off-by: Guillaume Lours --- api/control/validation.go2 | 9 +++++++++ api/control/validation_test.go2 | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/api/control/validation.go2 b/api/control/validation.go2 index 4a67f9c..a5280dd 100644 --- a/api/control/validation.go2 +++ b/api/control/validation.go2 @@ -49,6 +49,15 @@ func MapValidation[E, T, U any](validation Validation[E, T], mapper func(T) U) V return Invalid[E, U]{validation.ErrorOrElse(*new(E))} } +// MapErrorValidation maps the invalid value of a Validation[E, T] to a new Validation with a invalid element of type U. +// the mapper function should take a E value and return a U value. +func MapErrorValidation[E, T, U any](validation Validation[E, T], mapper func(E) U) Validation[U, T] { + if validation.IsInvalid() { + return Invalid[U, T]{mapper(validation.ErrorOrElse(*new(E)))} + } + return Valid[U, T]{validation.OrElse(*new(T))} +} + // Valid is an implementation of Validation with a valid T value. type Valid[E, T any] struct { value T diff --git a/api/control/validation_test.go2 b/api/control/validation_test.go2 index 5549dc1..98f0a11 100644 --- a/api/control/validation_test.go2 +++ b/api/control/validation_test.go2 @@ -171,3 +171,18 @@ func TestMapValidation(t *testing.T) { t.Errorf("should be an Invalid Validation") } } + +func TestMapErrorValidation(t *testing.T) { + var mapper = func(value error) string { + return value.Error() + } + var mapErrorValid Validation[string, int] = MapErrorValidation[error, int, string](valid, mapper) + if mapErrorValid.OrElse(20) != 10 { + t.Errorf("value should be 10") + } + + var mapInvalid Validation[string, int] = MapErrorValidation[error, int, string](invalid, mapper) + if mapInvalid.IsValid() { + t.Errorf("should be an Invalid Validation") + } +} \ No newline at end of file From ac68691a2147bee11eb94dc0be82f5c8f4da0cf6 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Wed, 2 Jun 2021 22:51:38 +0200 Subject: [PATCH 5/6] Add FlatMapValidation to Validation type Signed-off-by: Guillaume Lours --- api/control/validation.go2 | 9 +++++++++ api/control/validation_test.go2 | 25 ++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/api/control/validation.go2 b/api/control/validation.go2 index a5280dd..0b3609e 100644 --- a/api/control/validation.go2 +++ b/api/control/validation.go2 @@ -58,6 +58,15 @@ func MapErrorValidation[E, T, U any](validation Validation[E, T], mapper func(E) return Valid[U, T]{validation.OrElse(*new(T))} } +// FlatMapValidation maps the valid value of a Validation[E, T] to a new Validation with a valid element of type U. +// the mapper function should take a T value and return a Validation[E, U] value. +func FlatMapValidation[E, T, U any](validation Validation[E, T], mapper func(T) Validation[E, U]) Validation[E, U] { + if validation.IsValid() { + return mapper(validation.OrElse(*new(T))) + } + return Invalid[E, U]{validation.ErrorOrElse(*new(E))} +} + // Valid is an implementation of Validation with a valid T value. type Valid[E, T any] struct { value T diff --git a/api/control/validation_test.go2 b/api/control/validation_test.go2 index 98f0a11..59c673f 100644 --- a/api/control/validation_test.go2 +++ b/api/control/validation_test.go2 @@ -161,12 +161,12 @@ func TestMapValidation(t *testing.T) { var mapper = func(value int) string { return strconv.Itoa(value) } - var mapValid Validation[error, string] = MapValidation[error, int, string](valid, mapper) + var mapValid = MapValidation[error, int, string](valid, mapper) if mapValid.OrElse("good") != "10" { t.Errorf("value should be 10") } - var mapInvalid Validation[error, string] = MapValidation[error, int, string](invalid, mapper) + var mapInvalid = MapValidation[error, int, string](invalid, mapper) if mapInvalid.IsValid() { t.Errorf("should be an Invalid Validation") } @@ -176,13 +176,28 @@ func TestMapErrorValidation(t *testing.T) { var mapper = func(value error) string { return value.Error() } - var mapErrorValid Validation[string, int] = MapErrorValidation[error, int, string](valid, mapper) + var mapErrorValid = MapErrorValidation[error, int, string](valid, mapper) if mapErrorValid.OrElse(20) != 10 { t.Errorf("value should be 10") } - var mapInvalid Validation[string, int] = MapErrorValidation[error, int, string](invalid, mapper) + var mapInvalid = MapErrorValidation[error, int, string](invalid, mapper) if mapInvalid.IsValid() { t.Errorf("should be an Invalid Validation") } -} \ No newline at end of file +} + +func TestFlatMapValidation(t *testing.T) { + var mapper = func(value int) Validation[error, string] { + return ValidOf[error, string](strconv.Itoa(value)) + } + var flatMapValid = FlatMapValidation(valid, mapper) + if flatMapValid.OrElse("good") != "10" { + t.Errorf("value should be 10") + } + + var flatMapInvalid = FlatMapValidation(invalid, mapper) + if flatMapInvalid.IsValid() { + t.Errorf("should be an Invalid Validation") + } +} From 6957f87136935c4942aa328fd81124ea2f536108 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Wed, 2 Jun 2021 23:30:45 +0200 Subject: [PATCH 6/6] Add FoldValidation to Validation type Signed-off-by: Guillaume Lours --- api/control/validation.go2 | 8 ++++++++ api/control/validation_test.go2 | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/api/control/validation.go2 b/api/control/validation.go2 index 0b3609e..4acdd2a 100644 --- a/api/control/validation.go2 +++ b/api/control/validation.go2 @@ -67,6 +67,14 @@ func FlatMapValidation[E, T, U any](validation Validation[E, T], mapper func(T) return Invalid[E, U]{validation.ErrorOrElse(*new(E))} } +// Fold transforms this Validation[E, T] to a U type value. +func FoldValidation[E, T, U any](validation Validation[E, T], mapperValid func(T) U, mapperInvalid func(E) U) U { + if validation.IsValid() { + return mapperValid(validation.OrElse(*new(T))) + } + return mapperInvalid(validation.ErrorOrElse(*new(E))) +} + // Valid is an implementation of Validation with a valid T value. type Valid[E, T any] struct { value T diff --git a/api/control/validation_test.go2 b/api/control/validation_test.go2 index 59c673f..72b82ef 100644 --- a/api/control/validation_test.go2 +++ b/api/control/validation_test.go2 @@ -201,3 +201,21 @@ func TestFlatMapValidation(t *testing.T) { t.Errorf("should be an Invalid Validation") } } + +func TestFoldValidation(t *testing.T) { + var mapperValid = func(value int) string { + return strconv.Itoa(value) + } + var mapperInvalid = func(err error) string { + return err.Error() + } + var foldValid = FoldValidation(valid, mapperValid, mapperInvalid) + if foldValid != "10" { + t.Errorf("value should be 10") + } + + var foldInvalid = FoldValidation(invalid, mapperValid, mapperInvalid) + if foldInvalid != defaultError.Error() { + t.Errorf("value should be %s but is %s", foldInvalid, defaultError.Error()) + } +}