"Two dimentional", semigroupoid based, composable validation toolkit.
This library explores and gives some potential tools for really strict validation processing and I'm using it in some of my projects, but... If you want something more lightweight to work with (for example solution for forms processing) I want to promote my other solution: purescript-polyform.
Let's build update/create password component.
User should provide non empty password twice...
passwordFields =
buildRecord
(addFieldFromQuery (SProxy ∷ SProxy "password1") nonEmptyString >>>
addFieldFromQuery (SProxy ∷ SProxy "password2") nonEmptyString)...and if they contain the same value...
passwordsEqual = check (\r → r.password1 == r.password2)...we are accepting them as correct and build a password based on the value:
createPassword = pureV (_.password1 >>> Password >>> Right))Let's combine these steps together and check them in action:
password =
(tag (SProxy ∷ SProxy "fields") passwordFields) >>>
(tag (SProxy ∷ SProxy "equals") (passwordsEqual >>> createPassword))
When you see SProxy :: SProxy label you can think of a labeling validation step. Labeling steps is necessary in case of "sum validation" - where validation is chained one after another. It's basically a -> Either e b chain where every e can and should be labeled to form single coproduct type.
On the other hand we have also products/records build up steps. This kind of validation steps aggregates all results into product (through addField or dedicated query helper addFieldFromQuery). In this context labeling is just giving record field a name. Resulting product value would contain all valid, but also invalid values (values wrapped in Either), but if the whole validation processes passes you would get record without any additional wrapping!
Few additional comments regarding following examples:
-
I'm using debug log (
traceAnyA) in case of failure -
we have used
Data.Validation.Jaws.Http.nonEmptyStringas entry point for our fields together withaddFieldFromQueryso our validation is build upontype Query = StrMap (Array (Maybe String)) -
this library is not particularly tide to http validation
-
to run validation we just call
runValidation password queryData
I've written simple (and ugly) reporter which validates and prints the results:
validateAndPrint ∷ ∀ a e i eff m r. (Show i) ⇒ (Show r) ⇒ Validation (Eff (console ∷ CONSOLE | eff)) e i r → i → Eff (console ∷ CONSOLE | eff) Unit
validateAndPrint v d = do
r ← runValidation v d
case r of
Right v → logShow (Right v ∷ Either Unit r)
e → traceAnyA eLet's validate:
-- both values missing
validateAndPrint password empty-- one value missing - errors occures during `password2.fields` and `password1.fields` steps
Left {
value0:
{ type: 'fields',
value:
{ password1: Left { value0: { type: 'nonEmpty', value: {} } },
password2: Left { value0: { type: 'nonEmpty', value: {} } } } } }-- one value missing - error occures during `password2.fields` step
validateAndPrint password (fromFoldable [Tuple "password1" [Just "admin"]]) Left {
value0:
{ type: 'fields',
value:
{ password1: Right { value0: 'admin' },
password2: Left { value0: { type: 'nonEmpty', value: {} } } } } }
-- non equal passwords - error occures during `equals` step
validateAndPrint password (fromFoldable [Tuple "password1" [Just "admin"], Tuple "password2" [Just "pass"]]) Left {
value0:
{ type: 'equals',
value: { password1: 'admin', password2: 'pass' } } }-- correct data
validateAndPrint password (fromFoldable [Tuple "password1" [Just "secret"], Tuple "password2" [Just "secret"]]) (Right "secret")Let's assume that our registration process requires these data:
newtype Registration = Registration
{ nickname ∷ Nickname
, email ∷ Email
, password ∷ Password
}We can easily build validation reusing our existing password component:
registration =
Registration <$> buildRecord
((addField (SProxy ∷ SProxy "password") password) >>>
(addFieldFromQuery (SProxy ∷ SProxy "email") (nonEmptyString >>> email') >>>
(addFieldFromQuery (SProxy ∷ SProxy "nickname") (Nickname <$> nonEmptyString))))And like previously if we provide correct input we are going to get just plain value:
correctData =
(fromFoldable
[ Tuple "password1" [Just "pass"]
, Tuple "password2" [Just "pass"]
, Tuple "email" [Just "email@example.com"]
, Tuple "nickname" [Just "nick"]
])
validateAndPrint registration correctData (Registration { email: "email@example.com", nickname: "nick", password: "pass" })but in case of invalid input...
passwordMismatch =
(fromFoldable
[ Tuple "password1" [Just "wrong"]
, Tuple "password2" [Just "pass"]
, Tuple "email" [Just "email@example.com"]
, Tuple "nickname" [Just "nick"]
])
validateAndPrint registration passwordMismatch...we are getting precise representation of failure but other data validated:
Left {
value0:
{ password:
Left {
value0:
{ type: 'equals',
value: { password1: 'wrong', password2: 'pass' } } },
email: Right { value0: 'email@example.com' },
nickname: Right { value0: 'nick' } } }Now let's consider a bit more difficult reusability scenario - we want to provide profile edit form:
newtype Profile = Profile
{ nickname ∷ Nickname
, bio ∷ Maybe String
, age ∷ Maybe Int
, password ∷ Maybe Password
}
derive instance genericProfile ∷ Generic Profile _
instance showProfile ∷ Show Profile where
show = genericShowBut in this context we want to validate also case where user leaves two passwords empty. In that case we don't update it's value in our db.
To add this additional scenario we should bulid validator which passes when two values from query (just a recap - type Query = StrMap (Array (Maybe String))) are empty.
In this case our final password value should be Nothing.
As usual we are going to use pureV constructor which has type pureV ∷ (Monad m) ⇒ (a → Either e b) → Validation m e a b in other words it lifts simple validation function
into our validation monad stack. a type is an result from previous validation steps.
Lets build this simple combinator from scratch:
missingValue ∷ ∀ a m. Monad m ⇒ String → Validation m Unit Query Query
missingValue p = check (\query → case lookup p query of
Nothing → true
Just [Nothing] → true
Just [Just ""] → true
_ → false)WAT? Yes, we are considering these THREE values as emtpy ;-)
Now we can validate both passwords using above combinator to form final validation:
emptyPasswords ∷ ∀ a m. Monad m ⇒ Validation m Unit Query (Maybe a)
emptyPasswords = (missingValue "password1" >>> missingValue "password2" >>> pure Nothing)We can read above Validation (there is also complementary type for "product validation") signature as follows:
m- monad which we are using as the context for validationUnit- error typeQuery- previous validation step resultMaybe a- successful validation result type (thisapolimorphism doesn't hurt here ;-))
and tag this step too:
emptyPasswords' = tag (SProxy "emptyPasswords") emptyPasswordsTODO: more docs soon...
profile =
Profile <$>
(buildRecord
((addField (SProxy ∷ SProxy "password") (emptyPasswords' <|> (Just <$> password)) >>>
addFieldFromQuery (SProxy ∷ SProxy "bio") (scalar <|> pure Nothing) >>>
addFieldFromQuery (SProxy ∷ SProxy "age") (catMaybesV >>> optional int') >>>
addFieldFromQuery (SProxy ∷ SProxy "nickname") (Nickname <$> nonEmptyString)))
let
onlyNickname =
(fromFoldable
[Tuple "nickname" [Just "nick"]])
validateAndPrint profile onlyNickname(Right (Profile { age: Nothing, bio: Nothing, nickname: "nick", password: Nothing }))let
nicknameAndPassword =
(fromFoldable
[ Tuple "nickname" [Just "nick"]
, Tuple "password1" [Just "new"]
, Tuple "password2" [Just "new"]
])validateAndPrint profile nicknameAndPassword(Right (Profile { age: Nothing, bio: Nothing, nickname: "nick", password: (Just "new") }))let
nicknameAndPasswordMismatch =
(fromFoldable
[ Tuple "nickname" [Just "nick"]
, Tuple "password1" [Just "wrong"]
, Tuple "password2" [Just "new"]
])
validateAndPrint profile nicknameAndPasswordMismatch Left {
value0:
{ password:
Left {
value0:
{ type: 'equals',
value: { password1: 'wrong', password2: 'new' } } },
bio: Right { value0: Nothing {} },
age: Right { value0: Nothing {} },
nickname: Right { value0: 'nick' } } }-
All constructors with
'at the end expectsSProxy las first arguments and do tagging step too -
All combinators with
'at the end are already tagged
-
Experiment scenario: drop monad instance for
ProductValidationand rebuildApplicativeinstance dependent onb, add apply instance toResultand... write test for applicative validation -
Use
Data.Record.Builderinternally -
Provide more basic validators for http
-
How to do tagging in more principled way? How to return list of error paths?