packages feed

validationt 0.2.1.0 → 0.3.0

raw patch · 5 files changed

+281/−44 lines, 5 filesdep +doctestdep +validationtdep −transformers-liftdep ~QuickCheckdep ~basedep ~exceptionsPVP ok

version bump matches the API change (PVP)

Dependencies added: doctest, validationt

Dependencies removed: transformers-lift

Dependency ranges changed: QuickCheck, base, exceptions, mtl

API changes (from Hackage documentation)

- Control.Monad.Validation: instance Control.Monad.Trans.Lift.Local.LiftLocal (Control.Monad.Validation.ValidationT e)
- Control.Monad.Validation: memptyWrap :: (Eq a, Monoid a) => b -> (a -> b) -> a -> b
- Control.Monad.Validation: mmAppend :: (Ord k, Semigroup v) => MonoidMap k v -> MonoidMap k v -> MonoidMap k v
+ Control.Monad.Validation: instance (GHC.Base.Monad m, GHC.Base.Monoid e) => GHC.Base.Alternative (Control.Monad.Validation.ValidationT e m)
+ Control.Monad.Validation: instance (GHC.Base.Monad m, GHC.Base.Monoid e) => GHC.Base.MonadPlus (Control.Monad.Validation.ValidationT e m)
+ Control.Monad.Validation: instance Control.Monad.Catch.MonadMask m => Control.Monad.Catch.MonadMask (Control.Monad.Validation.ValidationT e m)
+ Control.Monad.Validation: instance Control.Monad.Fail.MonadFail m => Control.Monad.Fail.MonadFail (Control.Monad.Validation.ValidationT e m)
+ Control.Monad.Validation: instance Control.Monad.Fix.MonadFix m => Control.Monad.Fix.MonadFix (Control.Monad.Validation.ValidationT e m)
+ Control.Monad.Validation: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Control.Monad.Validation.ValidationT e m)
+ Control.Monad.Validation: instance Control.Monad.Trans.Control.MonadBaseControl b m => Control.Monad.Trans.Control.MonadBaseControl b (Control.Monad.Validation.ValidationT e m)
+ Control.Monad.Validation: instance Data.Foldable.Foldable (Control.Monad.Validation.MonoidMap k)
+ Control.Monad.Validation: instance Data.Functor.Contravariant.Contravariant m => Data.Functor.Contravariant.Contravariant (Control.Monad.Validation.ValidationT e m)
+ Control.Monad.Validation: vPromote :: (Monad m, Monoid a, Eq a) => ValidationT a m x -> ValidationT a m x
- Control.Monad.Validation: MonoidMap :: (Map k v) -> MonoidMap k v
+ Control.Monad.Validation: MonoidMap :: Map k v -> MonoidMap k v
- Control.Monad.Validation: _MonoidMap :: forall k_abHs v_abHt k_alTR v_alTS. Iso (MonoidMap k_alTR v_alTS) (MonoidMap k_abHs v_abHt) (Map k_alTR v_alTS) (Map k_abHs v_abHt)
+ Control.Monad.Validation: _MonoidMap :: forall k_aaP3 v_aaP4 k_afwg v_afwh. Iso (MonoidMap k_afwg v_afwh) (MonoidMap k_aaP3 v_aaP4) (Map k_afwg v_afwh) (Map k_aaP3 v_aaP4)
- Control.Monad.Validation: neConcat :: (Foldable f, Eq (f a), Monoid a, Monoid (f a)) => (a -> a -> a) -> f a -> Maybe a
+ Control.Monad.Validation: neConcat :: Foldable f => (a -> a -> a) -> f a -> Maybe a
- Control.Monad.Validation: setMempty :: (Monoid s) => ASetter' s a -> a -> s
+ Control.Monad.Validation: setMempty :: Monoid s => ASetter' s a -> a -> s
- Control.Monad.Validation: vError :: (Monad m) => e -> ValidationT e m a
+ Control.Monad.Validation: vError :: Monad m => e -> ValidationT e m a

Files

ChangeLog.md view
@@ -1,5 +1,11 @@ # Revision history for validationt +## 0.3.0++* Added documentation.+* Fixed `textErrors` behaviour.+* Dropped `transformers-lift` dependency.+ ## 0.2.1.0  * GHC 8.4 support
+ README.md view
@@ -0,0 +1,76 @@+![Travis CI Badge](https://img.shields.io/travis/typeable/validationt)++# ValidationT++A simple data validation library. The main idea is to provide an easy way to+validate web form data by aggregating errors for each field.++## Usage++Suppose you want to validate some data from the user. Say, that the password adheres to some rules.++You might do something like this:++```haskell+validatePassword :: Monad m => String -> ValidationT [String] m ()+validatePassword password = do+  vLength password+  vAlpha password+  vNum password+  where+    vLength p = when (length p < 8)+      $ vWarning ["The password should be at least 8 characters long."]+    vAlpha p = unless (any isAlpha p)+      $ vWarning ["The password should contain at least one alphabetic character."]+    vNum p = unless (any isDigit p)+      $ vWarning ["The password should contain at least one numeric character."]+```++`ValidationT e m a` essentially just gathers the errors thrown by `vWarning`.++```haskell+vWarning :: (Monad m, Monoid e) => e -> ValidationT e m ()+```++`vWarning` `mappend`s the given `e` to the already collected errors. This is why the warnings are contained inside a list.++There is also `vError`. The only difference between `vWarning` and `vError` is that `vError` stops further execution (and further collection of errors and warnings).++You would use the validation like this:++```haskell+main :: IO ()+main = do+  password <- getLine+  result <- runValidationTEither . validatePassword $ password+  putStrLn $ case result of+    Left errs -> unlines err+    Right () -> "You are fine."+```++You could, of course, do more complicated things like use `vWarningL` and `vErrorL` to add an error to a `mempty` structure, which gets `mappend`ed with other errors.++The library comes with a `MonoidMap` `newtype` wrapper around `Map`, which `mappend`s the values themselves on conflict. This can be useful if you have a multiple points of failure and you want to distinguich between them -- validating a username and a password for example:++```haskell+data Piece+  = Password+  | UserName+  deriving (Eq, Show, Ord)++validatePassword :: Monad m => String -> ValidationT (MonoidMap Piece [String]) m ()+validatePassword password = do+  vLength password+  vAlpha password+  vNum password+  where+    warning = mmSingleton UserName+    vLength p = when (length p < 8)+      $ warning ["The password should be at least 8 characters long."]+    vAlpha p = unless (any isAlpha p)+      $ warning ["The password should contain at least one alphabetic character."]+    vNum p = unless (any isDigit p)+      $ warning ["The password should contain at least one numeric character."]+```++(`mmSingleton` is a covenience initializer for `MonoidMap`.)
src/Control/Monad/Validation.hs view
@@ -3,17 +3,39 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-} -module Control.Monad.Validation where+module Control.Monad.Validation+  ( ValidationT(..)+  , runValidationT+  , runValidationTEither+  , handleValidationT+  , vError+  , vWarning+  , vErrorL+  , vWarningL+  , vZoom+  , vZoomL+  , vPromote+  , mmSingleton+  , setMempty+  , neConcat+  , textErrors+  , _MonoidMap+  , MonoidMap(..)+  ) where +import Control.Applicative import Control.Lens hiding ((.=)) import Control.Monad.Base import Control.Monad.Catch import Control.Monad.Except+import Control.Monad.Fail import Control.Monad.State.Strict-import Control.Monad.Trans.Lift.Local+import Control.Monad.Trans.Control import Data.Aeson import Data.Foldable as F+import Data.Functor import Data.List as L import Data.Map.Strict as M import Data.Monoid@@ -21,23 +43,32 @@ import Data.Vector as V import Test.QuickCheck --- | Collects all throwed "warnings" throwed through StateT and "errors" throwed--- through ExceptT to single value using Monoid---  FIXME: give more instances like HReaderT and MonadBaseControl/MonadMask++-- | Collects all thrown warnings in 'StateT' and errors+-- in 'ExceptT' into a single value using 'Monoid'. newtype ValidationT e m a = ValidationT   { unValidationT :: ExceptT e (StateT e m) a   } deriving ( Functor, Applicative, Monad, MonadThrow, MonadCatch-             , MonadBase b )+             , MonadBase b, Alternative, MonadFix, MonadFail, Contravariant+             , MonadIO, MonadPlus, MonadBaseControl b, MonadMask )  instance MonadTrans (ValidationT e) where   lift = ValidationT . lift . lift -instance LiftLocal (ValidationT e) where-  liftLocal _ l f = ValidationT . mapExceptT (mapStateT $ l f) . unValidationT- -- | Map with 'Monoid' instance which 'mappend' its values+--+-- This can be used as the `e` in `ValidationT e m a` to provide different+-- sets of errors and warnings for different keys.+--+-- >>> :{+--   mconcat+--   [ MonoidMap $ M.fromList [(1, "foo"), (2, "hello, "), (3, "oh no")]+--   , MonoidMap $ M.fromList [(1, "bar"), (2, "world")]+--   ]+-- :}+-- MonoidMap (fromList [(1,"foobar"),(2,"hello, world"),(3,"oh no")]) newtype MonoidMap k v = MonoidMap (Map k v)-  deriving (Eq, Ord, Show, Arbitrary)+  deriving (Eq, Ord, Show, Arbitrary, Foldable)  makePrisms ''MonoidMap @@ -65,7 +96,7 @@         , "value" .= v ]  instance (Ord k, FromJSON k, FromJSON v) => FromJSON (MonoidMap k v) where-  parseJSON v = withArray "MonoidMap" go v+  parseJSON = withArray "MonoidMap" go     where       go arr = do         keyvals <- traverse fromObj arr@@ -82,35 +113,60 @@ #endif mmAppend (MonoidMap a) (MonoidMap b) = MonoidMap $ M.unionWith (<>) a b --- | Convenient for 'vZoom' as first artument. Will prevent generation--- of map with 'mempty' values+-- | Convenient for 'vZoom' as first argument. Will prevent generation+-- of map with 'mempty' values. mmSingleton :: (Eq v, Monoid v, Ord k) => k -> v -> MonoidMap k v-mmSingleton k = memptyWrap mempty $ MonoidMap . M.singleton k+mmSingleton k v+  | v == mempty = mempty+  | otherwise   = MonoidMap . M.singleton k $ v --- | Set given value to 'mempty'+-- | Sets given value to 'mempty'. setMempty :: (Monoid s) => ASetter' s a -> a -> s setMempty setter a = set setter a mempty -memptyWrap :: (Eq a, Monoid a) => b -> (a -> b) -> a -> b-memptyWrap b f a-  | a == mempty = b-  | otherwise = f a---- | If given container is not 'mempty', then use given function to--- append all its elements and return 'Just' result-neConcat-  :: (Foldable f, Eq (f a), Monoid a, Monoid (f a))-  => (a -> a -> a)-  -> f a-  -> Maybe a-neConcat f = memptyWrap Nothing (Just . F.foldl' f mempty)+neConcat :: Foldable f => (a -> a -> a) -> f a -> Maybe a+neConcat f a+  | F.null a  = Nothing+  | otherwise = Just $ F.foldr1 f a +-- | Returns the strings, concatanated with @", "@ if the list is not empty.+--+-- Returns Nothing if the list is empty+--+-- >>> textErrors ["foo", "bar"]+-- Just "foo, bar"+--+-- >>> textErrors ["foo"]+-- Just "foo"+--+-- >>> textErrors []+-- Nothing textErrors :: [Text] -> Maybe Text textErrors = neConcat (\a b -> a <> ", " <> b) --- | Returns `mempty` instead of error if no warnings was occured. So, your--- error should have `Eq` instance to detect that any error was occured. Returns--- Nothing for second element of tuple if compuration was interruped by 'vError'+-- | Returns 'mempty' instead of error if no warnings have occurred.+-- Returns 'Nothing' as the second element of tuple if computation was+-- interrupted by 'vError'.+--+-- Returns all concatenated errors and warnings and the result if no+-- errors have occurred (warnings could have occurred).+--+-- >>> :{+--  runValidationT $ do+--    vWarning ["warning1"]+--    vError ["error"]+--    vWarning ["warning2"]+--    return 8+-- :}+-- (["error","warning1"],Nothing)+--+-- >>> :{+--  runValidationT $ do+--    vWarning ["warning1"]+--    vWarning ["warning2"]+--    return 8+-- :}+-- (["warning1","warning2"],Just 8) runValidationT :: (Monoid e, Monad m) => ValidationT e m a -> m (e, Maybe a) runValidationT (ValidationT m) = do   (res, warnings) <- runStateT (runExceptT m) mempty@@ -118,6 +174,25 @@     Left err -> (err <> warnings, Nothing)     Right a  -> (warnings, Just a) +-- | Like 'runValidationT' but doesn't return the result+-- if any warning has occurred.+--+-- >>> :{+--  runValidationTEither $ do+--    vWarning ["warning1"]+--    vError ["error"]+--    vWarning ["warning2"]+--    return 8+-- :}+-- Left ["error","warning1"]+--+-- >>> :{+--  runValidationTEither $ do+--    vWarning ["warning1"]+--    vWarning ["warning2"]+--    return 8+-- :}+-- Left ["warning1","warning2"] runValidationTEither   :: (Monoid e, Eq e, Monad m)   => ValidationT e m a@@ -128,28 +203,71 @@     Just a | err == mempty -> Right a     _                      -> Left err +-- | Like 'runValidationTEither', but takes an error handler instead of+-- returning errors and warnings.+--+-- >>> :{+--  handleValidationT (\_ -> return 11) $ do+--    vWarning ["warning1"]+--    vError ["error"]+--    vWarning ["warning2"]+--    return 8+-- :}+-- 11+--+-- >>> :{+--  handleValidationT (\_ -> return 11) $ do+--    vWarning ["warning1"]+--    vWarning ["warning2"]+--    return 8+-- :}+-- 11 handleValidationT   :: (Monoid e, Monad m, Eq e)   => (e -> m a)   -> ValidationT e m a   -> m a-handleValidationT handler action = do+handleValidationT handler action =   runValidationTEither action >>= either handler return --- | Stops further execution of validation+-- | Stops further execution and appends the given error. vError :: (Monad m) => e -> ValidationT e m a vError e = ValidationT $ throwError e --- | Does not stop further execution, append warning to+-- | Does not stop further execution and appends the given warning. vWarning :: (Monad m, Monoid e) => e -> ValidationT e m () vWarning e = ValidationT $ modify' (<> e) +-- | Like 'vError' but allows you to use a setter to insert an error somewhere+-- deeper into an empty ('mempty') "e" from "ValidationT e m x", which is then+-- combined with all gathered warnings. vErrorL :: (Monad m, Monoid e) => ASetter' e a -> a -> ValidationT e m x vErrorL l a = vError $ setMempty l a +-- | Like 'vWarning' but allows you to use a setter to insert an error somewhere+-- deeper into an empty ('mempty') "e" from "ValidationT e m x", which is then+-- combined with all gathered warnings. vWarningL :: (Monad m, Monoid e) => ASetter' e a -> a -> ValidationT e m () vWarningL l a = vWarning $ setMempty l a +-- | Allows you apply a transformation to the "e" in "ValidationT e m x".+--+-- >>> :{+--runValidationT . vZoom (Data.Map.singleton "password errors") $ do+--  vWarning ["warning1"]+--  vError ["error"]+--  vWarning ["warning2"]+--  return 8+-- :}+-- (fromList [("password errors",["error","warning1"])],Nothing)+--+-- >>> :{+--  runValidationT . vZoom (Data.Map.singleton "password errors") $ do+--    vWarning ["warning1"]+--    vWarning ["warning2"]+--    return 8+-- :}+-- (fromList [("password errors",["warning1","warning2"])],Just 8) vZoom   :: (Monad m, Monoid a, Monoid b)   => (a -> b)@@ -159,11 +277,21 @@   (err, res) <- lift $ runValidationT action   case res of     Nothing -> vError $ up err-    Just a  -> vWarning (up err) *> return a+    Just a  -> vWarning (up err) $> a +-- | Like 'vZoom' but takes a setter instead of a function. vZoomL   :: (Monad m, Monoid a, Monoid b)   => ASetter' b a   -> ValidationT a m x   -> ValidationT b m x-vZoomL l action = vZoom (setMempty l) action+vZoomL l = vZoom (setMempty l)++-- | Turn any warnings the have occurred into errors.+vPromote+  :: (Monad m, Monoid a, Eq a)+  => ValidationT a m x+  -> ValidationT a m x+vPromote m = do+  err <- lift $ runValidationTEither m+  either vError return err
+ test/Doc.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Test.DocTest++main :: IO ()+main = doctest [ "-XOverloadedStrings", "-isrc", "src" ]
validationt.cabal view
@@ -2,9 +2,9 @@ -- documentation, see http://haskell.org/cabal/users-guide/  name:                validationt-version:             0.2.1.0-synopsis:            Straightforward validation monad.-description:         Convenient solution for validating web forms and APIs.+version:             0.3.0+synopsis:            Straightforward validation monad+description:         A simple data validation library. The main idea is to provide an easy way to validate web form data by aggregating errors for each field. homepage:            https://github.com/typeable/validationt license:             BSD3 license-file:        LICENSE@@ -14,11 +14,12 @@ category:            Control build-type:          Simple extra-source-files:  ChangeLog.md+                   , README.md cabal-version:       >=1.10-tested-with:         GHC == 7.10.3-                   , GHC == 8.0.2-                   , GHC == 8.2.2-                   , GHC == 8.4.1+tested-with:         GHC == 8.4.4+                   , GHC == 8.6.5+                   , GHC == 8.8.4+                   , GHC == 8.10.2  source-repository head   type:     git@@ -37,7 +38,27 @@                      , text                      , transformers                      , transformers-base-                     , transformers-lift                      , vector   hs-source-dirs:      src   default-language:    Haskell2010+  ghc-options:         -fwarn-unused-binds+                       -Wall+                       -Wincomplete-uni-patterns+                       -Wincomplete-record-updates+                       -Wpartial-fields+                       -Werror=missing-home-modules+                       -Wmissing-home-modules+                       -Widentities+                       -Wredundant-constraints+                       -Wmissing-export-lists++test-suite doctest+  default-language: Haskell2010+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  ghc-options: -Wall+  main-is: Doc.hs+  build-depends:+      base >= 4.10.0.0+    , doctest >= 0.11.4+    , validationt