diff --git a/api/control/validation.go2 b/api/control/validation.go2 new file mode 100644 index 0000000..4acdd2a --- /dev/null +++ b/api/control/validation.go2 @@ -0,0 +1,176 @@ +// 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] + Filter(func(T) bool) Option[Validation[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))) +} + +// 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))} +} + +// 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))} +} + +// 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))} +} + +// 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 +} + +// 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) +} + +// 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 +} + +// 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) +} + +// 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 new file mode 100644 index 0000000..72b82ef --- /dev/null +++ b/api/control/validation_test.go2 @@ -0,0 +1,221 @@ +package control + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "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") + } +} + +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") + } +} + +func TestMapValidation(t *testing.T) { + var mapper = func(value int) string { + return strconv.Itoa(value) + } + var mapValid = MapValidation[error, int, string](valid, mapper) + if mapValid.OrElse("good") != "10" { + t.Errorf("value should be 10") + } + + var mapInvalid = MapValidation[error, int, string](invalid, mapper) + if mapInvalid.IsValid() { + t.Errorf("should be an Invalid Validation") + } +} + +func TestMapErrorValidation(t *testing.T) { + var mapper = func(value error) string { + return value.Error() + } + var mapErrorValid = MapErrorValidation[error, int, string](valid, mapper) + if mapErrorValid.OrElse(20) != 10 { + t.Errorf("value should be 10") + } + + var mapInvalid = MapErrorValidation[error, int, string](invalid, mapper) + if mapInvalid.IsValid() { + t.Errorf("should be an Invalid Validation") + } +} + +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") + } +} + +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()) + } +}