"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.nonEmptyString
as entry point for our fields together withaddFieldFromQuery
so 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 e
Let'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 = genericShow
But 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 (thisa
polimorphism doesn't hurt here ;-))
and tag this step too:
emptyPasswords' = tag (SProxy "emptyPasswords") emptyPasswords
TODO: 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 l
as first arguments and do tagging step too -
All combinators with
'
at the end are already tagged
-
Experiment scenario: drop monad instance for
ProductValidation
and rebuildApplicative
instance dependent onb
, add apply instance toResult
and... write test for applicative validation -
Use
Data.Record.Builder
internally -
Provide more basic validators for http
-
How to do tagging in more principled way? How to return list of error paths?