Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions api/control/validation.go2
Original file line number Diff line number Diff line change
@@ -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)
}
221 changes: 221 additions & 0 deletions api/control/validation_test.go2
Original file line number Diff line number Diff line change
@@ -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())
}
}