Last active
February 12, 2021 16:24
-
-
Save pete-murphy/0938ec48c879efbcd740e95465cc2a60 to your computer and use it in GitHub Desktop.
Semiring Validation example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Main where | |
import Prelude | |
import Control.Alt ((<|>)) | |
import Control.Apply (lift2) | |
import Control.Monad.Reader (ReaderT(..)) | |
import Control.Monad.Reader as Reader | |
import Data.Array as Array | |
import Data.Maybe (Maybe(..)) | |
import Data.Maybe as Maybe | |
import Data.Semiring.Free (Free(..)) | |
import Data.Semiring.Free as Free | |
import Data.String as String | |
import Data.String.Regex as Regex | |
import Data.String.Regex.Unsafe as Regex.Unsafe | |
import Data.Validation.Semiring (V) | |
import Data.Validation.Semiring as V | |
import Effect (Effect) | |
import Effect.Exception as Exception | |
import React.Basic.DOM as R | |
import React.Basic.DOM.Events as DOM.Events | |
import React.Basic.Events as Events | |
import React.Basic.Hooks (Component, (/\)) | |
import React.Basic.Hooks as React | |
import Web.HTML as HTML | |
import Web.HTML.HTMLDocument as HTMLDocument | |
import Web.HTML.HTMLElement as HTMLElement | |
import Web.HTML.Window as Window | |
main :: Effect Unit | |
main = do | |
maybeBody <- HTMLDocument.body =<< Window.document =<< HTML.window | |
case maybeBody of | |
Nothing -> Exception.throw "Could not find body." | |
Just body -> do | |
app <- mkApp | |
R.render (app unit) (HTMLElement.toElement body) | |
mkApp :: Component Unit | |
mkApp = do | |
React.component "App" \_ -> React.do | |
value /\ setValue <- React.useState' "" | |
pure | |
( R.div_ | |
[ R.input | |
{ onChange: | |
Events.handler DOM.Events.targetValue \targetValue -> do | |
setValue (Maybe.fromMaybe "" targetValue) | |
, value | |
} | |
, R.div_ | |
[ R.p_ | |
[ R.text "Output of " | |
, R.code_ [ R.text "validate" ] | |
, R.pre_ | |
[ R.text (toString (Reader.runReaderT validate value)) ] | |
] | |
] | |
, R.div_ | |
[ R.p_ | |
[ R.text "Output of " | |
, R.code_ [ R.text "validate'" ] | |
, R.pre_ | |
[ R.text (toString (Reader.runReaderT validate' value)) ] | |
] | |
] | |
] | |
) | |
validate :: Validator String | |
validate = | |
( -- | This is saying "A password is valid if it has *both* mixed case AND it | |
-- | contains a number ... | |
hasMixedCase | |
<* hasNumber | |
-- | ... OR it's valid if it's between 4 & 8 characters long" | |
<|> hasLengthBetween 4 8 | |
) | |
-- | This special-character requirement is "multiplied" across both branches (this | |
-- | is effectively the same as "adding" this requirement to each path individually) | |
<* hasSpecialChar | |
validate' :: Validator String | |
validate' = | |
( -- | This says "A password is valid if it has mixed case ... | |
hasMixedCase | |
-- | ... OR it can be valid if it has a number and length between 4 & 8 characters ... | |
<|> hasNumber | |
<* hasLengthBetween 4 8 | |
) | |
-- | ... OR it can just contain a special character" | |
<|> hasSpecialChar | |
toString :: V (Free String) String -> String | |
toString = | |
V.unV | |
( ("Password must " <> _) | |
<<< String.joinWith "\nOR " | |
<<< map (String.joinWith "\nAND ") | |
<<< \(Free as) -> | |
Array.fromFoldable | |
(map Array.fromFoldable as) | |
) | |
("Success: " <> _) | |
type Validator | |
= ReaderT String (V (Free String)) | |
hasMixedCase :: Validator String | |
hasMixedCase = | |
fromPredicate | |
( lift2 (&&) | |
(Regex.test (Regex.Unsafe.unsafeRegex "[A-Z]" mempty)) | |
(Regex.test (Regex.Unsafe.unsafeRegex "[a-z]" mempty)) | |
) | |
"contain upper & lower-case letters" | |
hasNumber :: Validator String | |
hasNumber = | |
fromPredicate | |
(Regex.test (Regex.Unsafe.unsafeRegex "\\d" mempty)) | |
"contain a number" | |
hasLengthBetween :: Int -> Int -> Validator String | |
hasLengthBetween n m = | |
fromPredicate | |
(\x -> String.length x >= n && String.length x <= m) | |
("be between " <> show n <> "—" <> show m <> " characters long") | |
hasSpecialChar :: Validator String | |
hasSpecialChar = | |
fromPredicate | |
(Regex.test (Regex.Unsafe.unsafeRegex "[^A-Za-z0-9]" mempty)) | |
"contain a special character" | |
fromPredicate :: (String -> Boolean) -> String -> Validator String | |
fromPredicate p err = | |
ReaderT \s -> | |
if p s then | |
pure s | |
else | |
V.invalid (Free.free err) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment