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 +3/−0
- Data/Check.hs +178/−0
- LICENSE.md +28/−0
- README.md +45/−0
- Setup.hs +6/−0
- data-check.cabal +82/−0
- tests/Main.hs +120/−0
+ 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++[](http://opensource.org/licenses/BSD-3-Clause)+[](https://hackage.haskell.org/package/data-check)+[](http://stackage.org/nightly/package/data-check)+[](http://stackage.org/lts/package/data-check)+[](https://travis-ci.org/mrkkrp/data-check)+[](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