validations (empty) → 0.1.0.0
raw patch · 6 files changed
+452/−0 lines, 6 filesdep +HUnitdep +QuickCheckdep +basesetup-changed
Dependencies added: HUnit, QuickCheck, base, containers, digestive-functors, mtl, test-framework, test-framework-hunit, test-framework-quickcheck2, text, transformers
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- src/Validations.hs +13/−0
- src/Validations/Tutorial.lhs +334/−0
- tests/TestSuite.hs +10/−0
- validations.cabal +63/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2014, mavenraven.org++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of mavenraven.org nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Validations.hs view
@@ -0,0 +1,13 @@+module Validations+ ( module Validations.Types+ , module Validations.Internal+ , module Validations.Adapters+ , module Validations.Validator+ , module Validations.Validation+ ) where++import Validations.Types+import Validations.Internal+import Validations.Adapters+import Validations.Validator+import Validations.Validation
+ src/Validations/Tutorial.lhs view
@@ -0,0 +1,334 @@+> {-# LANGUAGE OverloadedStrings #-}+> {-# LANGUAGE RankNTypes #-}++> module Validations.Tutorial where++> import Prelude hiding ((.))+> import Control.Monad.Identity(Identity, runIdentity)+> import Validations.Internal.Lens(Lens, lens)+> import Validations.Adapters.Digestive(validateView, testEnv)+> import Text.Digestive.Form(Form, text, (.:), validate)+> import Text.Digestive.Types(Result(..))+> import Validations.Types(Checker)+> import Data.Text(Text, isPrefixOf, compareLength)+> import Control.Applicative((<$>), (<*>))+> import Text.Digestive.View(View, postForm)+> import Data.Monoid(Monoid(..), mempty, (<>))+> import Control.Arrow((>>>))+> import Control.Monad((>=>))+> import Validations.Validator(attach)+> import Validations.Validation(Validation, validation, runValidation)+++> eitherToResult :: Either a b -> Result a b+> eitherToResult x = case x of+> Left x' -> Error x'+> Right x' -> Success x'++validations+===========++table of contents+-----------------++[what is "validations"?](#what-is-validations)+++[existing solutions, and their problems](#existing-solutions-and-their-problems)++ [smart constructors](#smart-constructors)++ [digestive-functors formlet style](#digestive-functors-formlet-style)+++[ideas behind the structure of "validations"](#ideas-behind-the-structure-of-validations)+++[hello world](#hello-world)+++[testing](#testing)+++[digestive-functors integration](#digestive-functors-integration)+++What is "validations"?+----------------------++**validations** is a Haskell library that provides a flexible,+composable way to define validations of a domain model.++existing solutions, and their problems+--------------------------------------++There is a number of ways to do domain model validation in Haskell, but+each current method has drawbacks. Let's imagine a simple user model:++> data User = User+> { _firstName :: Text+> , _lastName :: Text+> , _emailAddress :: Text+> } deriving Show++We want to check that the first name is not empty and starts with the letter+A, that the last name is not empty, that the email address is not empty, and that +the email address is confirmed by a value that isn't stored in User. +We also want all checkers to conform to the type ++< a -> Either e b++or ++< (Monad m) => a -> m (Either a b)++.++So, our checkers could look something like++> notEmpty :: (Monoid a, Eq a) => a -> Either Text a+> notEmpty x =+> if (x == mempty)+> then Left "is empty"+> else Right x++,++> startsWith :: Text -> Text -> Either Text Text+> startsWith predicate input = +> if predicate `isPrefixOf` input+> then Right input+> else Left $ "does not start with " <> predicate++, and++> confirms :: (Eq a) => a -> a -> Either Text a+> confirms a b = case (a == b) of+> True -> Right b+> False -> Left "fields do not match."++.++ ### smart constructors ###++The simplest way to do this is with a smart constructor:++> user :: Text -> Text -> Text -> Text -> Either Text User+> user firstName lastName emailAddress emailAddressConfirm = do+> firstName' <- notEmpty firstName >>= startsWith "A"+> lastName' <- notEmpty lastName+> emailAddress' <- notEmpty emailAddress+> confirmed <- emailAddressConfirm `confirms` emailAddress'+> return $ User {_firstName = firstName', _lastName = lastName', _emailAddress = confirmed }++This will enforce all of our invariants, but there's a problem. If any of our validations+fail, then we only get the results of the failure of first validation. If firstName and+lastName are both empty, we'd like to know that the validation logic failed for both. Also, if we+use the pattern of exposing only the smart constructor (user), and keeping the data constructor (User)+hidden, then a User record can only be used in contexts where all the invariants must always be held,+which can be inflexible.+++ ### digestive-functors formlet style ###++**digestive-functors** solves the multiple validations problem. Our formlet could look like:++> userForm :: (Monad m) => Form Text m User+> userForm = User+> <$> "firstName" .: validate ((notEmpty >=> startsWith "A") >>> eitherToResult) (text Nothing)+> <*> "lastName " .: validate (notEmpty >>> eitherToResult) (text Nothing)+> <*> "emailAddress" .: validate (notEmpty >>> eitherToResult) (text Nothing)++But, how do we handle the email confirmations? Since formlets are applicatives+and not monadic, one way we could do this is by threading state through the monad and using validateM,+but that's mistake prone, and puts unnecessary constraints on our monad. We could intercept the View+record from **digestive-functors** once the form has been rendered, but then we're splitting our validation+logic into two different places.+++ideas behind the structure of "validations"+---------------------------------------------++**validations** is based around 4 different data types. First, a *Checker* is+function with type++< a -> Either e b++Checkers tend to be non domain model specific, reusable pieces of code. For+example, ++> nonEmpty :: (Monoid a, Eq a) => Checker Text a a+> nonEmpty x =+> if (x == mempty)+> then Left "is empty"+> else Right x++. Notice that a Checker can transform its input as well, which consumer is free+to ignore. This is useful for turning string typed user input into structured data.+++A *Monadic Checker* is the same as a checker, but with type++< (Monad m) => a -> m (Either e b)++.++The next data type is a *Validator*. It's a function with type++< a -> monad (Either (errorKey, errorValue) b)++It's very similar to a Monadic Checker, but it also uses an "errorKey" type.+This allows us to map a validator failure back to some given input (e.g. a form input field).+If you look at the type signature for a Validator, you'll notice that it's very similar to++< a -> m b++, so it's a [Kleisli category](http://www.haskell.org/haskellwiki/Monad_laws),+but the Either inside doesn't allow us to use Haskell stuff for composing Kleisi categories (e.g. (>=>)).+However, we do now how to unwrap and rewrap Either, so validations provides an instance of +Category to allow for validator composition. To create validators, we typically combine either a+checker with a field name using+++< attach :: (Monad m) => Checker ev a b -> ek -> Validator ek ev m a b++or ++< attachM :: (Monad m) => MonadicChecker ev m a b -> ek -> Validator ek ev m a b++for monadic checkers. Both attach and attachM are included with validations, but you're free+to wrap any conforming function as the Validator data constructor is public.++The final important data type is a *Validation*. The type of a validations is:++ state -> monad (newState, errors)++where state is a type like a user record. newState is typically the same type as state, but+a transformation is allowed. Validations can be constructed with++ validation :: (Monad m) => Lens b s -> a -> Validator ek ev m a b -> Validation [(ek,ev)] m s s++Validations also form a category and can be composed, similar to Validators.++hello world+-----------++Let's see the Validators and Validations in action. First, let's define a Account record:++> data Account = Account+> { _name :: Text+> , _accountNumber :: Text+> } deriving Show++Next, we want to define lenses for accessing and+mutating the fields. In this example, we are using the internal lens functionality of **validations**,+but we'd typically use something like [lens](http://hackage.haskell.org/package/lens) in our+application. ++> name :: Lens Text Account+> name = lens _name (\s a -> s {_name = a})+++> accountNumber :: Lens Text Account+> accountNumber = lens _accountNumber (\s a -> s {_accountNumber = a})++We also want to use **digestive-functors** to define our form to bring data in.++> nameField :: Text+> nameField = "name"++> confirmNameField :: Text+> confirmNameField = "confirmName"++> accountNumberField :: Text+> accountNumberField = "accountNumber"++> accountForm :: (Monad m) => Form Text m (Text, Text, Text)+> accountForm = (,,)+> <$> nameField .: (text Nothing)+> <*> confirmNameField .: (text Nothing)+> <*> accountNumberField .: (text Nothing)+++We don't use field names directly in our formlet because we need to use+them in our validation as well. Also, notice that we are outputting a 3-tuple+instead of a Account record. This is because there isn't a one to one correspondence+between our input fields and Account record fields (the name confirm field will be discarded).+So, our validation looks like++> lengthIs :: Int -> Checker Text Text Text+> lengthIs predicate txt = case (compareLength txt predicate) of+> EQ -> Right txt+> _ -> Left "account number not correct length"++> accountValidation :: (Monad m) => (Text, Text, Text) -> Validation [(Text, Text)] m Account Account+> accountValidation (f1, f2, f3) = +> validation name f1 (+> notEmpty `attach` nameField+> >>>+> (f2 `confirms`) `attach` confirmNameField+> )+> >>>+> validation accountNumber f3 (+> notEmpty `attach` accountNumberField+> >>>+> (lengthIs 10) `attach` accountNumberField+> ) +++What's going on here? First, since both Validations and Validators are Categories, we+can use the (>>>) operator from Control.Arrow. If you're unfamiliar with this operator,+for normal functions, ++< a >>> b ≡ b . a++so you can think of it as a composition operator that reads left to right. ++Next, let's take a look at our first validation. It takes the "f1" parameter passed into "accountValidation"+and feeds it into "notEmpty". If "notEmpty" returns a "Left", then the validation will make no+state changes, and instead adds ++< ("name" , "is empty")++to its "errors" value. On the other hand, if "f1" is not empty,+it will be passed onto the confirms function, where similar validation will happen. If confirms+also succeeds, the outputted value will passed to the "name" lens, and the outputted state will+be mutated with a new name value.++testing+-------++Testing a validation is simple. For example,++> _ = runIdentity $ (runValidation (accountValidation ("hi", "hi", "1234567890")) Account { _name = "", _accountNumber = "" } :: Identity (Account, [(Text, Text)]))++yields++< (Account {_name = "hi", _accountNumber = "1234567890"},[])++.++> _ = runIdentity $ (runValidation (accountValidation ("hi", "bye", "12345678900")) Account { _name = "", _accountNumber = "" } :: Identity (Account, [(Text, Text)]))++yields++< (Account {_name = "", _accountNumber = ""},[("confirmName","fields do not match."),("accountNumber","account number not correct length")])++.+++digestive-functors integration+---------------------------++Integration with **digestive-functors** is also pretty simple. Instead of++> posted :: (Monad m) => m (View Text, Maybe (Text,Text,Text))+> posted = postForm "f" accountForm $ testEnv [("f.name", "hello"), ("f.confirmName", "hello"), ("f.phoneNumber", "1(333)333-3333x3")]++add validateView in as well, like++> validatedPosted :: (Monad m) => m (View Text, Maybe Account)+> validatedPosted = posted >>= validateView accountValidation Account { _name = "", _accountNumber = "" }++You can also use validateView' if your domain record has a Monoid instance.+
+ tests/TestSuite.hs view
@@ -0,0 +1,10 @@+module Main+ ( main+ ) where++import Test.Framework (defaultMain)++main :: IO ()+main = defaultMain+ [ + ]
+ validations.cabal view
@@ -0,0 +1,63 @@+-- Initial digestive-functors-validations.cabal generated by cabal init. +-- For further documentation, see http://haskell.org/cabal/users-guide/++name: validations+version: 0.1.0.0+synopsis: A nice way to define field validations in Haskell.+description: See <https://github.com/mavenraven/validations> for description and tutorial.+ Code used in the tutorial can be found in src\/Validations\/Tutorial.lhs.+homepage: https://github.com/mavenraven/validations+license: BSD3+license-file: LICENSE+author: mavenraven.org+maintainer: developers@mavenraven.org+copyright: http://mavenraven.org+category: Web+build-type: Simple+-- extra-source-files: +cabal-version: >=1.10++Library+ exposed-modules: + Validations+ Validations.Tutorial+ -- other-modules: + -- other-extensions: + build-depends:+ base >= 4.6 && <4.7,+ digestive-functors >= 0.7 && < 0.8,+ text >= 1.0.0.1 && < 1.2,+ containers >= 0.5.0 && < 0.6,+ mtl >= 2.1.2 && < 2.2,+ transformers >= 0.3.0.0 && < 3.1++ default-language: Haskell2010+ Hs-source-dirs: src+ Ghc-options: -Wall -fwarn-tabs -fno-warn-unused-do-bind++Test-suite digestive-functors-validations-tests+ Type: exitcode-stdio-1.0+ default-language: Haskell2010+ Hs-source-dirs: src tests+ Main-is: TestSuite.hs+ Ghc-options: -Wall -Werror -fwarn-tabs -fno-warn-unused-do-bind+++ Build-depends:+ HUnit >= 1.2 && < 1.3,+ QuickCheck >= 2.5 && < 2.7,+ test-framework >= 0.4 && < 0.9,+ test-framework-hunit >= 0.3 && < 0.4,+ test-framework-quickcheck2 >= 0.3 && < 0.4,+ -- Copied from regular dependencies:+ base >= 4.6 && <4.7,+ digestive-functors >= 0.7 && < 0.8,+ text >= 1.0.0.1 && < 1.2,+ containers >= 0.5.0 && < 0.6,+ mtl >= 2.1.2 && < 2.2,+ transformers >= 0.3.0.0 && < 3.1+++Source-repository head+ Type: git+ Location: https://github.com/mavenraven/validations