valida
Simple applicative validation for product types, batteries included!
https://github.com/TotallyNotChase/valida#readme
LTS Haskell 22.37: | 1.1.0 |
Stackage Nightly 2024-10-11: | 1.1.0 |
Latest on Hackage: | 1.1.0 |
valida-1.1.0@sha256:b256210e2c4737775a680fdc6a008c56f61ff31dbdd23ce2bde5678350794a3c,2008
Module documentation for 1.1.0
Valida
Simple, elegant, profunctorial, applicative validation for product types - batteries included!
Read the documentation on hackage.
Highlights
-
Minimal - Singular external dependency: profunctors.
If you’d like a more lightweight version. Checkout valida-base, which offers similar functionalites without any external dependency.
-
Batteries included -
Validator
combinators for almost every scenario. -
Validation without the boiler plate - Implementation of contravariance to conveniently model the common validation usecases, without extra boilerplate.
-
Profunctorial, Applicative Validator - Relating to the previous point, the provided
Validator
type is not only an applicative functor, but also a profunctor. This is what allows the contravariance on its input argument.
Quick Taste
import Data.List.NonEmpty (NonEmpty)
import Valida
data InputForm = InpForm
{ inpName :: String
, inpAge :: Int
, inpEmail :: Maybe String
, inpFreeCake :: Maybe Bool
} deriving (Show)
data ValidInput = ValidInput
{ vInpName :: String
, vInpAge :: Int
, vInpEmail :: Maybe String
, vFreeCake :: Bool
} deriving (Show)
data FormErr
= InvalidNameLength
| InvalidAge
| NoAtCharInMail
| NoPeriodInMail
| InvalidEmailLength
| YouMustGetFreeCake
deriving (Show)
-- | Validator for each field in the input form - built using 'Validator' combinators.
inpFormValidator :: Validator (NonEmpty FormErr) InputForm ValidInput
inpFormValidator = ValidInput
-- Name should be between 1 and 20 characters long
<$> inpName -?> lengthWithin (1, 20) InvalidNameLength
-- Age should be between 18 and 120
<*> inpAge -?> valueWithin (18, 120) InvalidAge
-- Email, if provided, should contain '@', and '.', and be atleast 5 characters long
<*> inpEmail -?> optionally (minLengthOf 5 InvalidEmailLength
<> mustContain '@' NoAtCharInMail
<> mustContain '.' NoPeriodInMail)
<*> inpFreeCake -?> failureUnless id YouMustGetFreeCake `withDefault` True
goodInput :: InputForm
goodInput = InpForm "John Doe" 42 Nothing (Just True)
badInput :: InputForm
badInput = InpForm "John Doe" 17 (Just "@") Nothing
main :: IO ()
main = do
print (runValidator inpFormValidator goodInput)
-- Prints- Success (ValidInput {vInpName = "John Doe", vInpAge = 42, vInpEmail = Nothing, vFreeCake = true})
print (runValidator inpFormValidator badInput)
-- Prints- Failure (InvalidAge :| [InvalidEmailLength])
You can also find more examples here.
Quick Start
The primary purpose of the Validator
type is to validate each field in product types. To do this, you’ll use verify
.
verify
takes 2 inputs-
- The “selector”, which essentially just takes the product type as input, and returns the specific value of the specific field to validate.
- The
Validator
, which specifies the predicate the field must satisfy, the error value to yield if it doesn’t satisfy said predicate, and the output upon successful validation.
Let’s validate a pair for example, the first field should be an int less than 10, the second field should be a non empty string. Then, the validator would look like-
pairValidator :: Validator (NonEmpty String) (Int, String) (Int, String)
pairValidator = (,) <$> verify (failureIf (>=10) "NotLessThan10") fst <*> verify (notEmpty "EmptyString") snd
Or, if you prefer using operators - you can use -?>
, which is a flipped version of verify
.
pairValidator :: Validator (NonEmpty String) (Int, String) (Int, String)
pairValidator = (,)
<$> fst -?> failureIf (>=10) "NotLessThan10"
<*> snd -?> notEmpty "EmptyString"
You can then run the validator on your input using runValidator
-
>>> runValidator pairValidator (9, "foo")
Success (9,"foo")
>>> runValidator pairValidator (10, "")
Failure ("NotLessThan10" :| ["EmptyString"])
>>> runValidator pairValidator (5, "")
Failure ("EmptyString" :| [])
This is the core concept for building the validators. You can use the primitive combinators (e.g failureIf
, failureUnless
) to build Validator
s directly from predicate functions, or you can choose one of the many derivate combinators (e.g notEmpty
) to build Validator
s. Check out the Valida.Combinators
module documentation to view all the included combinators.
Combining multiple Validator
s
Often, you’ll find yourself in situations where you expect the input to satisfy multiple Validator
s (but don’t need applicative composition), or situations where you expect the input to satisfy at least one of multiple Validator
s. This is where andAlso
, and orElse
come into play.
Combining multiple Validator
s with andAlso
andAlso
is the semigroup implementation of Validator
, and thus is the same as <>
. Combining 2 validators with <>
creates a new validator that is only satisfied when both of the given validators are satisfied.
Otherwise, the first (left most) failure value is returned - and the rest are not tried. Upon successful validation, the right-most Success
value is returned. This means that if all validators succeed, only the right-most validator’s success value is returned.
The following validator only succeeds if the input is odd, and not divisble by 3.
validator :: Validator (NonEmpty String) Int Int
validator = failureIf even "IsEven" `andAlso` failureIf ((==0) . flip mod 3) "IsDivisbleBy3"
(OR)
validator :: Validator (NonEmpty String) Int Int
validator = failureIf even "IsEven" <> failureIf ((==0) . flip mod 3) "IsDivisbleBy3"
Usages-
>>> runValidator validator 5
Success 5
>>> runValidator validator 4
Failure ("IsEven" :| [])
>>> runValidator validator 15
Failure ("IsDivisbleBy3" :| [])
>>> runValidator validator 6
Failure ("IsEven" :| [])
Combining multiple Validator
s with orElse
orElse
also forms a semigroup, </>
is aliased to orElse
. Combining 2 validators with </>
creates a new validator that is satisfied when either of the given validators are satsified. If all of them fail, the Failure
values are accumulated. The left-most Success
value is returned, remaining validators are not tried.
The following validator succeeds if the input is either odd, or not divisble by 3.
validator :: Validator (NonEmpty String) Int Int
validator = failureIf even "IsEven" `orElse` failureIf ((==0) . flip mod 3) "IsDivisbleBy3"
(OR)
validator :: Validator (NonEmpty String) Int Int
validator = failureIf even "IsEven" </> failureIf ((==0) . flip mod 3) "IsDivisbleBy3"
Usages-
>>> runValidator validator 5
Success 5
>>> runValidator validator 4
Success 4
>>> runValidator validator 15
Success 15
>>> runValidator validator 6
Failure ("IsEven" :| ["IsDivisbleBy3"])
Combining a foldable of Validator
s
You can combine a foldable of Validator
s using satisfyAll
and satisfyAny
. satisfyAll
folds using andAlso
/<>
, while satisfyAny
folds using orElse
/</>
.
Ignoring errors
Although, highly inadvisable and generally not useful in serious code, you may use alternative versions of Validator
combinators that use ()
(unit) as the error type so you don’t have to supply error values. For example, failureIf'
does not require an error value to be supplied. In case of failure, it simply yields Failure ()
.
>>> runValidator (failureIf' even) 2
Failure ()
Re-assigning errors
Using the label
/<?>
function, you can override the errors Validator
s yield.
For example, to re assign the error on a Validator
-
label "IsEven" (failureIf even "Foo")
(OR)
failureIf even "Foo" <?> "IsEven"
This is useful with Validator
s that use unit as their error type. You can create a Validator
, skip assigning an error to it - and label a specific error when you need to later.
label "IsEven" (failureIf' even)
Re-labeled Validator
s will yield the newly assigned error value when the validator is not satisfied.
Core Idea
All usecases of applicative validation, involving validation of product types, have one noticable thing in common. A well written validator typically looks something like-
data InputForm = InpForm
{ inpName :: String
, inpAge :: Int
, inpDate :: String
} deriving (Show)
validateName :: String -> Validation [String] String
validateAge :: Int -> Validation [String] Int
validateDate :: String -> Validation [String] String
validateForm :: InputForm -> Validation [String] InputForm
validateForm form = InputForm
<$> validateName (inpName form)
<*> validateAge (inpAge form)
<*> validateDate (inpDate form)
There’s a few things unideal with this. The functions validateName
, validateAge
, and validateDate
are defined elsewhere - but all of their definitions are really similar. Yet, without handy combinators - they can’t be defined in a terse way. However, the bigger problem, is how all of the validators need to be fed their specific input by selecting the field from the product type. It could look better if the validator functions could somehow just be linked to a specific field selector in an elegant way. Something like inpName -?> validateName
, perhaps.
This is the perfect usecase for contravariance. A validation function, is really just a Predicate
, the idiomatic example of a contravariant functor. However, it also needs to be an applicative functor to allow for the elegant composition. In fact, the type of a validation function needs to parameterize on 3 types - inp -> Validation e a
- The input type
- The error type
- The output type
The output is covariant, but the input is contravariant. This is a Profunctor! With a profunctorial validator, you now have the ability to not only map the output type, but also contramap the input type.
Given a validator that makes sure an int input is even, and returns said int input as output - evenValidator
, you can easily use it in the applicative validation of a (Int, Int)
using lmap
-
(,) <$> lmap fst evenValidator <*> lmap snd evenValidator
Contravariant input, mixed with covariant output - is the bread and butter of Valida! It allows for elegant encoding of well composable validators using only 2 simple concepts.
There’s one more core idea that Valida
uses though - fixV
. fixV
“fixes” a validator’s output, to be the same as its input. fmap
lets you map over the output, lmap
lets you contramap over the input, fixV
allows fmap
to now map over the input value, on the output position. fixV
also allows you to regain the input value in the output position if a validator has been fmap
ed on.
Comparison and Motivation
The concept of the Validation
data type used in this package isn’t new. It’s also used in the following packages-
Valida aims to be a minimal in terms of dependencies, but batteries included in terms of API. It borrows many philosophies from Data.Validation
(from validation
) and Validation
(from validation-selective
), and aims to provide a convenient, minimal way to model the common usecases of them.
The verify
function, combined with the built in Validator
combinators, and the parsec-esque Validator
aims to assist in easily modeling typical validation usecases without too much boilerplate, using applicative composition and contravariant input.
In essence, the validation style itself, is designed to look like forma. Though the actual types, and core concepts are significantly different.
Changes
Changelog for valida
1.1.0 (Aug 25, 2021)
- Add
withDefault
combinator. - Make
optionally
preserve output of given validator. - Remove redundant
fixV
usage from examples.
1.0.0 (Aug 24, 2021)
-
Add
profunctors
dependency. -
Add
Profunctor
instance forValidator
. -
Rework
Validator
semigroup instance.The new semigroup instance now works similar to the
ValidationRule
semigroup instance. -
Add
Monoid
instance forValidator
. -
Remove
ValidationRule
. -
Remove
Selector
type alias. -
Rewrite all previous
ValidationRule
combinators to work withValidator
s instead. -
Rename
negateRule
->negateV
. -
Rename
falseRule
->failV
. -
Add
fixV
- refer to the documentation for more information. -
Remove
validate
- no longer necessary. -
Rename
labelV
->label
. -
Rename
<??>
-><?>
. Infix precedence 6. -
Remove
label
and<?>
. -
verify
is now an alias to flippedlmap
fromData.Profunctor
, specialized forValidator
s. -
Allow usage with base == 4.12
-
Explicitly constraint smallcheck dependency to 1.2.0+
0.1.0 (Aug 20, 2021)
Initial release.