packages feed

monad-validate 1.0.0.0 → 1.1.0.0

raw patch · 7 files changed

+153/−28 lines, 7 filesdep ~transformersPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: transformers

API changes (from Hackage documentation)

- Control.Monad.Validate.Class: instance Control.Monad.Validate.Class.MonadValidate e m => Control.Monad.Validate.Class.MonadValidate e (Control.Monad.Trans.Cont.ContT r m)
- Control.Monad.Validate.Class: instance Control.Monad.Validate.Class.MonadValidate e m => Control.Monad.Validate.Class.MonadValidate e (Control.Monad.Trans.RWS.CPS.RWST r w s m)
- Control.Monad.Validate.Class: instance Control.Monad.Validate.Class.MonadValidate e m => Control.Monad.Validate.Class.MonadValidate e (Control.Monad.Trans.Writer.CPS.WriterT w m)
+ Control.Monad.Validate: embedValidateT :: forall e m a. MonadValidate e m => ValidateT e m a -> m a
+ Control.Monad.Validate: mapErrors :: forall e e' m a. (Monad m, Semigroup e') => (e -> e') -> ValidateT e m a -> ValidateT e' m a
+ Control.Monad.Validate: tolerate :: (MonadValidate e m, MonadTransControl t, MonadValidate e m', m ~ t m') => m a -> m (Maybe a)
+ Control.Monad.Validate.Class: instance (Control.Monad.Validate.Class.MonadValidate e m, GHC.Base.Monoid w) => Control.Monad.Validate.Class.MonadValidate e (Control.Monad.Trans.RWS.CPS.RWST r w s m)
+ Control.Monad.Validate.Class: instance (Control.Monad.Validate.Class.MonadValidate e m, GHC.Base.Monoid w) => Control.Monad.Validate.Class.MonadValidate e (Control.Monad.Trans.Writer.CPS.WriterT w m)
+ Control.Monad.Validate.Class: tolerate :: (MonadValidate e m, MonadTransControl t, MonadValidate e m', m ~ t m') => m a -> m (Maybe a)
+ Control.Monad.Validate.Internal: embedValidateT :: forall e m a. MonadValidate e m => ValidateT e m a -> m a
+ Control.Monad.Validate.Internal: mapErrors :: forall e e' m a. (Monad m, Semigroup e') => (e -> e') -> ValidateT e m a -> ValidateT e' m a

Files

CHANGELOG.md view
@@ -1,3 +1,9 @@+# 1.1.0.0 [2019-08-05]++- Added the `tolerate` method to `MonadValidate`, which allows relaxing validation errors from fatal to nonfatal.+- Added the `embedValidateT` and `mapErrors` functions, which can be used together to locally alter the type of validation errors in `ValidateT` computations.+- Removed the `MonadValidate` instance for `ContT`, which is no longer possible to implement due to the addition of `tolerate`.+ # 1.0.0.0 [2019-08-04]  - Initial release.
monad-validate.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 55a4fb3ebfe9bd3e5b9df3ebd02552c9f299a050ca7d67961179a4214569126a+-- hash: 79775fef96c91f15bf687d1020e2db81124f7fb2e493f1bf2e780efe2d7b04b5  name:           monad-validate-version:        1.0.0.0+version:        1.1.0.0 synopsis:       A monad transformer for data validation. description:    Provides the 'ValidateT' monad transformer, designed for writing data validations that provide                 high-quality error reporting without much effort. 'ValidateT' automatically exploits the data@@ -50,7 +50,7 @@     , exceptions >=0.9 && <1     , monad-control >=1 && <2     , mtl-    , transformers+    , transformers >=0.5.6     , transformers-base <1   default-language: Haskell2010 @@ -75,7 +75,7 @@     , mtl     , scientific     , text-    , transformers+    , transformers >=0.5.6     , transformers-base <1     , unordered-containers     , vector
package.yaml view
@@ -1,5 +1,5 @@ name: monad-validate-version: 1.0.0.0+version: 1.1.0.0 category: Control copyright: 2019 Hasura license: ISC@@ -65,7 +65,7 @@ - exceptions >= 0.9 && < 1 - monad-control >= 1 && < 2 - mtl-- transformers+- transformers >= 0.5.6 - transformers-base < 1  library:
src/Control/Monad/Validate.hs view
@@ -7,6 +7,8 @@     ValidateT   , runValidateT   , execValidateT+  , embedValidateT+  , mapErrors    -- * The MonadValidate class   , MonadValidate(..)
src/Control/Monad/Validate/Class.hs view
@@ -15,11 +15,12 @@ import qualified Control.Monad.Trans.Writer.Strict as Strict  import Control.Monad.Trans.Class-import Control.Monad.Trans.Cont+import Control.Monad.Trans.Control import Control.Monad.Trans.Except import Control.Monad.Trans.Identity import Control.Monad.Trans.Maybe import Control.Monad.Trans.Reader+import Data.Functor  {-| The class of validation monads, intended to be used to validate data structures while collecting errors along the way. In a sense, 'MonadValidate' is like a combination of@@ -33,32 +34,65 @@      computation to globally fail, it just doesn’t affect local execution.  For a more thorough explanation, with examples, see the documentation for-'Control.Monad.Validate.ValidateT'.--}+'Control.Monad.Validate.ValidateT'. -} class (Monad m, Semigroup e) => MonadValidate e m | m -> e where   -- | Raises a fatal validation error. Aborts the current branch of the validation (i.e. does not   -- return).+  --+  -- @+  -- >>> 'Control.Monad.Validate.runValidate' ('refute' ["boom"] '>>' 'refute' ["bang"])+  -- 'Left' ["boom"]+  -- @   refute :: e -> m a    -- | Raises a non-fatal validation error. The overall validation fails, and the error is recorded,   -- but validation continues in an attempt to try and discover more errors.+  --+  -- @+  -- >>> 'Control.Monad.Validate.runValidate' ('dispute' ["boom"] '>>' 'dispute' ["bang"])+  -- 'Left' ["boom", "bang"]+  -- @   dispute :: e -> m () +  -- | @'tolerate' m@ behaves like @m@, except that any fatal errors raised by 'refute' are altered+  -- to non-fatal errors that return 'Nothing'. This allows @m@’s result to be used for further+  -- validation if it succeeds without preventing further validation from occurring upon failure.+  --+  -- @+  -- >>> 'Control.Monad.Validate.runValidate' ('tolerate' ('refute' ["boom"]) '>>' 'refute' ["bang"])+  -- 'Left' ["boom", "bang"]+  -- @+  --+  -- @since 1.1.0.0+  tolerate :: m a -> m (Maybe a)+   default refute :: (MonadTrans t, MonadValidate e m', m ~ t m') => e -> m a   refute = lift . refute   default dispute :: (MonadTrans t, MonadValidate e m', m ~ t m') => e -> m ()   dispute = lift . dispute+  default tolerate :: (MonadTransControl t, MonadValidate e m', m ~ t m') => m a -> m (Maybe a)+  tolerate m = liftWith (\run -> tolerate (run m)) >>=+    maybe (pure Nothing) (fmap Just . restoreT . pure)+  {-# INLINE refute #-}+  {-# INLINE dispute #-}+  {-# INLINE tolerate #-} -instance (MonadValidate e m) => MonadValidate e (ContT r m) instance (MonadValidate e m) => MonadValidate e (ExceptT a m) instance (MonadValidate e m) => MonadValidate e (IdentityT m) instance (MonadValidate e m) => MonadValidate e (MaybeT m) instance (MonadValidate e m) => MonadValidate e (ReaderT r m)-instance (MonadValidate e m) => MonadValidate e (CPS.RWST r w s m) instance (MonadValidate e m, Monoid w) => MonadValidate e (Lazy.RWST r w s m) instance (MonadValidate e m, Monoid w) => MonadValidate e (Strict.RWST r w s m) instance (MonadValidate e m) => MonadValidate e (Lazy.StateT s m) instance (MonadValidate e m) => MonadValidate e (Strict.StateT s m)-instance (MonadValidate e m) => MonadValidate e (CPS.WriterT w m) instance (MonadValidate e m, Monoid w) => MonadValidate e (Lazy.WriterT w m) instance (MonadValidate e m, Monoid w) => MonadValidate e (Strict.WriterT w m)++instance (MonadValidate e m, Monoid w) => MonadValidate e (CPS.WriterT w m) where+  tolerate m = CPS.writerT $ tolerate (CPS.runWriterT m) <&>+    maybe (Nothing, mempty) (\(v, w) -> (Just v, w))+  {-# INLINE tolerate #-}+instance (MonadValidate e m, Monoid w) => MonadValidate e (CPS.RWST r w s m) where+  tolerate m = CPS.rwsT $ \r s1 -> tolerate (CPS.runRWST m r s1) <&>+    maybe (Nothing, s1, mempty) (\(v, s2, w) -> (Just v, s2, w))+  {-# INLINE tolerate #-}
src/Control/Monad/Validate/Internal.hs view
@@ -81,7 +81,7 @@   qrAuth           <- withKey o "auth_token" parseAuthToken   ~(qrTable, info) <- withKey o "table" parseTableName   qrQuery          <- withKey o "query" parseQuery-  'Data.Foldable.for_' info $ \tableInfo -> 'local' (pushPath "query") '$'+  'Data.Foldable.for_' info '$' \tableInfo -> pushPath "query" '$'     validateQuery qrTable tableInfo (atIsAdmin qrAuth) qrQuery   'pure' QueryRequest { qrAuth, qrTable, qrQuery } @@@ -432,8 +432,11 @@     let !e3 = monoMaybe e2 (<> e2) e1 in pure (Left e3)   dispute e2 = validateT $ \e1 ->     let !e3 = monoMaybe e2 (<> e2) e1 in pure (Right (MJust e3, ()))+  tolerate m = validateT $ \e1 ->+    Right . either (\e2 -> (MJust e2, Nothing)) (fmap Just) <$> unValidateT e1 m   {-# INLINABLE refute #-}   {-# INLINABLE dispute #-}+  {-# INLINABLE tolerate #-}  -- | Runs a 'ValidateT' computation, returning the errors raised by 'refute' or 'dispute' if any, -- otherwise returning the computation’s result.@@ -446,23 +449,71 @@ -- | Runs a 'ValidateT' computation, returning the errors on failure or 'mempty' on success. The -- computation’s result, if any, is discarded. ----- >>> execValidate (refute ["bang"])+-- @+-- >>> 'execValidate' ('refute' ["bang"]) -- ["bang"]--- >>> execValidate @[] (pure 42)+-- >>> 'execValidate' @[] ('pure' 42) -- []+-- @ execValidateT :: forall e m a. (Monoid e, Functor m) => ValidateT e m a -> m e execValidateT = fmap (either id mempty) . runValidateT +{-| Runs a 'ValidateT' transformer by interpreting it in an underlying transformer with a+'MonadValidate' instance. That might seem like a strange thing to do, but it can be useful in+combination with 'mapErrors' to locally alter the error type in a larger 'ValidateT' computation.+For example:++@+throwsIntegers :: 'MonadValidate' ['Integer'] m => m ()+throwsIntegers = 'dispute' [42]++throwsBools :: 'MonadValidate' ['Bool'] m => m ()+throwsBools = 'dispute' ['False']++throwsBoth :: 'MonadValidate' ['Either' 'Integer' 'Bool'] m => m ()+throwsBoth = do+  'embedValidateT' '$' 'mapErrors' ('map' 'Left') throwsIntegers+  'embedValidateT' '$' 'mapErrors' ('map' 'Right') throwsBools++>>> 'runValidate' throwsBoth+'Left' ['Left' 42, 'Right' False]+@++@since 1.1.0.0 -}+embedValidateT :: forall e m a. (MonadValidate e m) => ValidateT e m a -> m a+embedValidateT m = unValidateT MNothing m >>= \case+  Left e              -> refute e+  Right (MJust e, v)  -> dispute e $> v+  Right (MNothing, v) -> pure v++-- | Applies a function to all validation errors produced by a 'ValidateT' computation.+--+-- @+-- >>> 'runValidate' '$' 'mapErrors' ('map' 'show') ('refute' [11, 42])+-- 'Left' ["11", "42"]+-- @+--+-- @since 1.1.0.0+mapErrors+  :: forall e e' m a. (Monad m, Semigroup e')+  => (e -> e') -> ValidateT e m a -> ValidateT e' m a+mapErrors f m = lift (unValidateT MNothing m) >>= \case+  Left e              -> refute (f e)+  Right (MJust e, v)  -> dispute (f e) $> v+  Right (MNothing, v) -> pure v+ -- | 'ValidateT' specialized to the 'Identity' base monad. See 'ValidateT' for usage information. type Validate e = ValidateT e Identity  -- | See 'runValidateT'. runValidate :: forall e a. Validate e a -> Either e a runValidate = runIdentity . runValidateT+{-# INLINE runValidate #-}  -- | See 'execValidateT'. execValidate :: forall e a. (Monoid e) => Validate e a -> e execValidate = runIdentity . execValidateT+{-# INLINE execValidate #-}  {-| Monotonically increasing 'Maybe' values. A function with the type 
test/Control/Monad/ValidateSpec.hs view
@@ -15,7 +15,6 @@ import Data.Aeson.QQ (aesonQQ) import Data.Foldable import Data.Functor-import Data.Maybe import Data.Scientific (toBoundedInteger) import Data.Text (Text) import Data.Typeable@@ -73,7 +72,7 @@   qrAuth           <- withKey o "auth_token" parseAuthToken   ~(qrTable, info) <- withKey o "table" parseTableName   qrQuery          <- withKey o "query" parseQuery-  for_ info $ \tableInfo -> local (pushPath "query") $+  for_ info $ \tableInfo -> pushPath "query" $     validateQuery qrTable tableInfo (atIsAdmin qrAuth) qrQuery   pure QueryRequest { qrAuth, qrTable, qrQuery }   where@@ -86,11 +85,13 @@      parseTableName v = withObject "table name" v $ \o -> do       name <- TableName <$> withKey o "schema" asString <*> withKey o "name" asString-      info <- lookup name <$> asks envTables-      when (isNothing info) $-        disputeErr $ UnknownTableName name+      info <- tolerate $ validateTableName name       pure (name, info) +    validateTableName name = do+      info <- lookup name <$> asks envTables+      maybe (refuteErr $ UnknownTableName name) pure info+     parseQuery :: forall a. (Typeable a) => Value -> m (Query a)     parseQuery q = withSingleKeyObject "query expression" q $ \k v -> case k of       "lit"    -> withType $ QLit <$> asInteger v@@ -105,21 +106,23 @@       loop :: Query a -> m ()       loop = \case         QLit _ -> pure ()-        QSelect colName -> local (pushPath "select") $ case lookup colName tableInfo of+        QSelect colName -> pushPath "select" $ case lookup colName tableInfo of           Just colInfo             | ciAdminOnly colInfo && not isAdmin             -> disputeErr $ InsufficientPermissions tableName colName             | otherwise -> pure ()           Nothing -> disputeErr $ UnknownColumnName tableName colName-        QAdd a b -> local (pushPath "add") $ loop a *> loop b-        QEqual a b -> local (pushPath "equal") $ loop a *> loop b-        QIf a b c -> local (pushPath "if") $ loop a *> loop b *> loop c+        QAdd a b -> pushPath "add" $ loop a *> loop b+        QEqual a b -> pushPath "equal" $ loop a *> loop b+        QIf a b c -> pushPath "if" $ loop a *> loop b *> loop c      parseColumnName = fmap ColumnName . asString -    pushPath path env = env { envPath = path : envPath env }+    pushPath :: Text -> m a -> m a+    pushPath path = local (\env -> env { envPath = path : envPath env })     mkErr info = asks envPath <&> \path -> Error (reverse path) info     refuteErr = mkErr >=> \err -> refute [err]+    disputeErr :: ErrorInfo -> m ()     disputeErr = mkErr >=> \err -> dispute [err]      withType :: forall a b. (Typeable a, Typeable b) => m (Query a) -> m (Query b)@@ -139,14 +142,43 @@     withObject name v f = case v of { Object o -> f o; _ -> refuteErr $ JSONBadValue name v }      withKey :: Object -> Text -> (Value -> m a) -> m a-    withKey o k f = maybe (refuteErr $ JSONMissingKey k) (local (pushPath k) . f) $ M.lookup k o+    withKey o k f = maybe (refuteErr $ JSONMissingKey k) (pushPath k . f) $ M.lookup k o      withSingleKeyObject :: Text -> Value -> (Text -> Value -> m a) -> m a     withSingleKeyObject name i f = withObject name i $ \o -> case M.toList o of-      { [(k, v)] -> local (pushPath k) $ f k v; _ -> refuteErr $ JSONBadValue name i }+      { [(k, v)] -> pushPath k $ f k v; _ -> refuteErr $ JSONBadValue name i }  spec :: Spec-spec = describe "ValidateT" $+spec = describe "ValidateT" $ do+  describe "tolerate" $ do+    it "has no effect on computations without fatal errors" $ do+      runValidate ((dispute ["boom"] $> ["bang"]) >>= dispute)+        `shouldBe` Left (["boom", "bang"] :: [Text])+      runValidate (tolerate (dispute ["boom"] $> ["bang"]) >>= traverse_ dispute)+        `shouldBe` Left (["boom", "bang"] :: [Text])++    it "converts fatal errors to non-fatal errors" $ do+      runValidate (refute ["boom"] >> dispute ["bang"])+        `shouldBe` Left (["boom"] :: [Text])+      runValidate (tolerate (refute ["boom"]) >> dispute ["bang"])+        `shouldBe` Left (["boom", "bang"] :: [Text])++  describe "mapErrors" $ do+    it "applies a function to all validation errors" $+      runValidate (mapErrors (map show) (refute [True] *> dispute [False]))+        `shouldBe` Left ["True", "False"]++    it "can be used with embedValidateT to locally change the type of errors" $ do+      let foo :: (MonadValidate [Integer] m) => m ()+          foo = dispute [42]+          bar :: (MonadValidate [Bool] m) => m ()+          bar = dispute [False]+          baz :: (MonadValidate [Either Integer Bool] m) => m ()+          baz = do+            embedValidateT $ mapErrors (map Left) foo+            embedValidateT $ mapErrors (map Right) bar+      runValidate baz `shouldBe` Left [Left 42, Right False]+   it "collects validation information from all sub-branches of <*>" $ do     let tables =           [ (TableName "public" "users",