packages feed

data-check (empty) → 0.1.0

raw patch · 7 files changed

+462/−0 lines, 7 filesdep +QuickCheckdep +basedep +containerssetup-changed

Dependencies added: QuickCheck, base, containers, data-check, hspec, semigroups

Files

+ CHANGELOG.md view
@@ -0,0 +1,3 @@+## Data Check 0.1.0++* Initial release.
+ Data/Check.hs view
@@ -0,0 +1,178 @@+-- |+-- Module      :  Data.Check+-- Copyright   :  © 2016 Mark Karpov+-- License     :  BSD 3 clause+--+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>+-- Stability   :  experimental+-- Portability :  portable+--+-- This module provides generalized approach to checking and verification of+-- data, it's useful, for example, for validation of fields on web forms.+--+-- Typically, there are a number of transformations and checks you may want+-- to perform on a particular type of data, such as text. Thus, it makes+-- sense to create all those transformations and checks once and then+-- combine them to get more complex validators that may vary on per-field+-- basis.+--+-- Certainly, if we can normalize and validate, we should normalize first.+-- However, if we have many normalizing operations, we need a way to specify+-- in which order they should be performed, or result can be unpredictable.+--+-- To specify order in which transformations are performed, 'normalizer' and+-- 'normalizerM' functions take a “priority” argument, which is just a+-- 'Natural' number. The bigger the number, the later the function will be+-- applied, so the transformation with priority 0 will always run first.+--+-- This method applies to validators just as well. It's possible to create a+-- vocabulary of validators that can be mixed together and the result will+-- be always deterministic.+--+-- To support more real-world use cases, every check can be performed inside+-- of a monad, allowing to query a database for example.+--+-- One last thing to note is that every normalizer and every validator+-- should have a unique priority number. Normalizers (and validators) with+-- the same priority will overwrite each other. This is by design. Note that+-- normalizer won't override validator with the same priority though, their+-- priority-spaces are separate.++{-# LANGUAGE GADTs #-}++module Data.Check+  ( -- * Normalizers+    normalizer+  , normalizerM+    -- * Validators+  , validator+  , validatorM+    -- * Checkers+  , Checker+  , runChecker+  , runCheckerM )+where++import Control.Monad+import Data.Functor.Identity+import Data.Semigroup+import Data.Set (Set)+import Numeric.Natural+import qualified Data.Set as S++----------------------------------------------------------------------------+-- Normalizers++-- | @'Normalizer' m a@ is a normalizer that works on values of type @a@ in+-- monad @m@.++data Normalizer m a  where+  Normalizer :: Monad m => Natural -> (a -> m a) -> Normalizer m a++instance Eq (Normalizer m a) where+  (Normalizer x _) == (Normalizer y _) = x == y++instance Ord (Normalizer m a) where+  (Normalizer x _) `compare` (Normalizer y _) = x `compare` y++-- | Create a normalizing 'Checker'. Every normalizer has a priority — the+-- bigger the number, the later the normalizer runs. Every normalizer you+-- use should have a unique priority number.++normalizer :: Monad m+  => Natural           -- ^ Priority+  -> (a -> a)          -- ^ Normalizing transformation+  -> Checker m e a     -- ^ Normalizing 'Checker'+normalizer n f = normalizerM n (return . f)++-- | The same as 'normalizer', but allows to perform normalization inside of+-- a monad.++normalizerM :: Monad m+  => Natural           -- ^ Priority+  -> (a -> m a)        -- ^ Normalizing transformation+  -> Checker m e a     -- ^ Normalizing 'Checker'+normalizerM n f = Checker (S.singleton $ Normalizer n f) S.empty++----------------------------------------------------------------------------+-- Validators++-- | @'Validator' m e a@ is a validator that checks values of type @a@,+-- works in @m@ monad, and can return @e@ messages on failure.++data Validator m e a where+  Validator :: Natural -> (a -> m (Maybe e)) -> Validator m e a++instance Eq (Validator m e a) where+  (Validator x _) == (Validator y _) = x == y++instance Ord (Validator m e a) where+  (Validator x _) `compare` (Validator y _) = x `compare` y++-- | Create a validating 'Checker'. Every validator has a priority — the+-- bigger the number, the later the validation step runs. Every validator+-- you use should have a unique priority number.++validator :: Monad m+  => Natural           -- ^ Priority+  -> (a -> Maybe e)    -- ^ 'Nothing' if everything is OK+  -> Checker m e a     -- ^ Validating 'Checker'+validator n f = validatorM n (return . f)++-- | The same as 'validator', but allows to perform normalization inside of+-- a monad.++validatorM :: Monad m+  => Natural           -- ^ Priority+  -> (a -> m (Maybe e)) -- ^ 'Nothing' if everything is OK+  -> Checker m e a     -- ^ Validating 'Checker'+validatorM n f = Checker S.empty (S.singleton $ Validator n f)++----------------------------------------------------------------------------+-- Checkers++-- | @'Checker' m e a@ is a checker that checks value of type @a@, can+-- perform the check in @m@ monad, returning @e@ message when check fails.+--+-- 'Checker' is a 'Semigroup' and 'Monoid' — this is how you combine+-- different checkers and build more complex ones.++data Checker m e a where+  Checker :: Monad m+    => Set (Normalizer m a)+    -> Set (Validator m e a)+    -> Checker m e a++instance Semigroup (Checker m e a) where+  (Checker ns vs) <> (Checker ns' vs') = Checker (S.union ns ns') (S.union vs vs')++instance Monad m => Monoid (Checker m e a) where+  mempty = Checker S.empty S.empty+  mappend = (<>)++-- | Run a 'Checker' on given value. This is version for cases when all+-- transformations and validations are pure.++runChecker+  :: Checker Identity e a -- ^ The 'Checker' to run+  -> a                 -- ^ Value to check+  -> Either e a        -- ^ Result, 'Right' on success, 'Left' on failure+runChecker c x = runIdentity (runCheckerM c x)++-- | Version of 'runChecker' that can run transformations and checks in any+-- monad.++runCheckerM :: Monad m+  => Checker m e a     -- ^ The 'Checker' to run+  -> a                 -- ^ Value to check+  -> m (Either e a)    -- ^ Result, 'Right' on success, 'Left' on failure+runCheckerM (Checker ns vs) = n >=> \a -> maybe (Right a) Left <$> v a+  where+    n = appEndo (foldMap (Endo . nf) . S.toDescList $ ns) . return+    nf (Normalizer _ f) = (>>= f)+    v a = appEndo (foldMap (Endo . vf a) . S.toDescList $ vs) (return Nothing)+    vf a (Validator _ f) m = do+      x <- m+      case x of+        Nothing -> f a+        Just  e -> return (Just e)
+ LICENSE.md view
@@ -0,0 +1,28 @@+Copyright © 2016 Mark Karpov++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 Mark Karpov nor the names of 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 “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 HOLDERS 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.
+ README.md view
@@ -0,0 +1,45 @@+# Data Check++[![License BSD3](https://img.shields.io/badge/license-BSD3-brightgreen.svg)](http://opensource.org/licenses/BSD-3-Clause)+[![Hackage](https://img.shields.io/hackage/v/data-check.svg?style=flat)](https://hackage.haskell.org/package/data-check)+[![Stackage Nightly](http://stackage.org/package/data-check/badge/nightly)](http://stackage.org/nightly/package/data-check)+[![Stackage LTS](http://stackage.org/package/data-check/badge/lts)](http://stackage.org/lts/package/data-check)+[![Build Status](https://travis-ci.org/mrkkrp/data-check.svg?branch=master)](https://travis-ci.org/mrkkrp/data-check)+[![Coverage Status](https://coveralls.io/repos/mrkkrp/data-check/badge.svg?branch=master&service=github)](https://coveralls.io/github/mrkkrp/data-check?branch=master)++This is a library that implements generalized approach to checking and+verification of data, it's useful, for example, for validation of fields on+web forms.++Typically, there are a number of transformations and checks you may want to+perform on a particular type of data, such as text. Thus, it makes sense to+create all those transformations and checks once and then combine them to+get more complex validators that may vary on per-field basis.++Certainly, if we can normalize and validate, we should normalize first.+However, if we have many normalizing operations, we need a way to specify in+which order they should be performed, or result can be unpredictable.++To specify order in which transformations are performed, `normalizer` and+`normalizerM` functions take a “priority” argument, which is just a+`Natural` number. The bigger the number, the later the function will be+applied, so the transformation with priority 0 will always run first.++This method applies to validators just as well. It's possible to create a+vocabulary of validators that can be mixed together and the result will+be always deterministic.++To support more real-world use cases, every check can be performed inside of+a monad, allowing to query a database for example.++One last thing to note is that every normalizer and every validator should+have a unique priority number. Normalizers (and validators) with the same+priority will overwrite each other. This is by design. Note that normalizer+won't override validator with the same priority though, their+priority-spaces are separate.++## License++Copyright © 2016 Mark Karpov++Distributed under BSD 3 clause license.
+ Setup.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Distribution.Simple++main :: IO ()+main = defaultMain
+ data-check.cabal view
@@ -0,0 +1,82 @@+--+-- Cabal configuration for ‘data-check’ package.+--+-- Copyright © 2016 Mark Karpov <markkarpov@openmailbox.org>+--+-- 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 Mark Karpov nor the names of 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 “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 HOLDERS 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.++name:                 data-check+version:              0.1.0+cabal-version:        >= 1.10+license:              BSD3+license-file:         LICENSE.md+author:               Mark Karpov <markkarpov@openmailbox.org>+maintainer:           Mark Karpov <markkarpov@openmailbox.org>+homepage:             https://github.com/mrkkrp/data-check+bug-reports:          https://github.com/mrkkrp/data-check/issues+category:             Data, Web+synopsis:             Library for checking and normalization of data (e.g. from web forms)+build-type:           Simple+description:          Library for checking and normalization of data (e.g. from web forms).+extra-source-files:   CHANGELOG.md+                    , README.md++source-repository head+  type:               git+  location:           https://github.com/mrkkrp/data-check.git++flag dev+  description:        Turn on development settings.+  manual:             True+  default:            False++library+  build-depends:      base             >= 4.8 && < 5.0+                    , containers       >= 0.5 && < 0.6+  if !impl(ghc >= 8.0)+    build-depends:    semigroups   == 0.18.*+  exposed-modules:    Data.Check+  if flag(dev)+    ghc-options:      -Wall -Werror+  else+    ghc-options:      -O2 -Wall+  default-language:   Haskell2010++test-suite tests+  main-is:            Main.hs+  hs-source-dirs:     tests+  type:               exitcode-stdio-1.0+  build-depends:      base             >= 4.7 && < 5.0+                    , QuickCheck       >= 2.4 && < 3.0+                    , data-check       >= 0.1.0+                    , hspec            >= 2.0 && < 3.0+  if flag(dev)+    ghc-options:      -Wall -Werror+  else+    ghc-options:      -O2 -Wall+  default-language:   Haskell2010
+ tests/Main.hs view
@@ -0,0 +1,120 @@+--+-- Tests for the ‘data-check’ package.+--+-- Copyright © 2016 Mark Karpov <markkarpov@openmailbox.org>+--+-- 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 Mark Karpov nor the names of 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 “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 HOLDERS 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.++{-# LANGUAGE MultiWayIf #-}++module Main (main) where++import Data.Check+import Data.Monoid+import Test.Hspec+import Test.QuickCheck++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+  context "when two normalizers have the same priority" $+    it "the left one overrides the right one" $+      property $ \x -> do+        runChecker (addOneNorm <> addTwoNorm) x `shouldBe` Right (x + 1)+        runChecker (addTwoNorm <> addOneNorm) x `shouldBe` Right (x + 2)+  context "when two validators have the same priority" $+    it "the left one overrides the right one" $+      property $ \x -> do+        runChecker (validatorTrue <> validatorFalse) x `shouldBe` Left True+        runChecker (validatorFalse <> validatorTrue) x `shouldBe` Left False+  context "adding mempty to the max" $+    it "has no effect" $+      property $ \x -> do+        runChecker (addOneNorm <> mempty) x `shouldBe` Right (x + 1)+        runChecker (mempty <> addOneNorm) x `shouldBe` Right (x + 1)+        runChecker (validatorTrue <> mempty) x `shouldBe` Left True+        runChecker (mempty <> validatorTrue) x `shouldBe` Left True+  context "when using several normalizers" $+    it "they are applied, and applied in order" $+      property $ \x -> do+        runChecker (addOneNorm <> mulThreeNorm) x+          `shouldBe` Right (x * 3 + 1)+        runChecker (addThreeNorm <> mulThreeNorm) x+          `shouldBe` Right ((x + 3) * 3)+  context "when using several validators" $+    it "they are run, and run in order" $+      property $ \x ->+        runChecker (validatorGT50 <> validatorLT100) x+          `shouldBe` if | x <= 50   -> Left False+                        | x >= 100  -> Left True+                        | otherwise -> Right x+  context "when we have both normalizers and validators" $+    it "normalizers are run before validators and their output is used" $+      property $ \x ->+        runChecker (mulThreeNorm <> validatorGT50) x+          `shouldBe` if x * 3 > 50+                       then Right (x * 3)+                       else Left False+  it "normalizers and validators can run inside a monad" $+    property $ \x ->+      runCheckerM (breakingNorm <> addOneNorm <> validatorLT100) x+        `shouldBe` Nothing++----------------------------------------------------------------------------+-- Collection of test normazilers and validators++addOneNorm :: Monad m => Checker m Bool Int+addOneNorm = normalizer 3 (+ 1)++addTwoNorm :: Monad m => Checker m Bool Int+addTwoNorm = normalizer 3 (+ 2)++mulThreeNorm :: Monad m => Checker m Bool Int+mulThreeNorm = normalizer 2 (* 3)++addThreeNorm :: Monad m => Checker m Bool Int+addThreeNorm = normalizer 1 (+ 3)++breakingNorm :: Checker Maybe Bool Int+breakingNorm = normalizerM 0 (const Nothing)++validatorTrue :: Monad m => Checker m Bool Int+validatorTrue = validator 3 (const $ return True)++validatorFalse :: Monad m => Checker m Bool Int+validatorFalse = validator 3 (const $ return False)++validatorGT50 :: Monad m => Checker m Bool Int+validatorGT50 = validator 3 $ \x ->+  if x > 50 then Nothing else Just False++validatorLT100 :: Monad m => Checker m Bool Int+validatorLT100 = validator 4 $ \x ->+  if x < 100 then Nothing else Just True