{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeSynonymInstances #-} -- | -- Module: Data.Salak -- Copyright: (c) 2019 Daniel YU -- License: BSD3 -- Maintainer: Daniel YU -- Stability: experimental -- Portability: portable -- -- Data Validation inspired by JSR305 -- module Data.Menshen( -- * How to use this library -- $use -- * Definition HasValid(..) , Validator , ValidationException(..) , HasI18n(..) , ValidatorErr(..) , VerifyResult(..) -- * Validation Functions , HasValidSize(..) , notNull , assertNull , assertTrue , assertFalse , positive , positiveOrZero , negative , negativeOrZero , minInt , maxInt , minDecimal , maxDecimal , pattern , email -- * Validation Operations , (?) , verify , (?:) , vcvt -- * Reexport Functions , (=~) ) where import Control.Exception (Exception (..), SomeException) import Data.Scientific import qualified Data.Text as TS import qualified Data.Text.Lazy as TL import Data.Word import Text.Regex.TDFA #if __GLASGOW_HASKELL__ > 708 import Data.Function ((&)) #else infixl 1 & (&) :: a -> (a -> b) -> b x & f = f x #endif -- | apply record validation to the value infixl 5 ? (?) :: HasValid m => m a -> Validator a -> m a (?) = (&) -- | lift value a to validation context and check if it is valid. -- infixl 5 ?: (?:) :: HasValid m => a -> Validator a -> m a (?:) = verify -- | Plan for i18n translate, now just for english. class Exception e => HasI18n e where toI18n :: e -> String toErr :: String -> e -> ValidatorErr toErr field e = let message = toI18n e exception = toException e in ValidatorErr{..} -- | Validation Error Message data ValidationException = ShouldBeFalse | ShouldBeTrue | ShouldNull | ShouldNotNull | InvalidSize Word64 Word64 | InvalidPositive | InvalidPositiveOrZero | InvalidNegative | InvalidNegativeOrZero | InvalidMax Integer | InvalidMin Integer | InvalidEmail | InvalidNotBlank | InvalidNotEmpty | InvalidPast | InvalidFuture | InvalidPastOrPresent | InvalidFutureOrPresent | InvalidDecimalMax Scientific | InvalidDecimalMin Scientific | InvalidDigits Word8 Word8 | InvalidPattern String deriving Show instance Exception ValidationException instance HasI18n ValidationException where toI18n ShouldBeTrue = "must be true" toI18n ShouldBeFalse = "must be false" toI18n ShouldNull = "must be null" toI18n ShouldNotNull = "must not be null" toI18n (InvalidSize a b) = "size must be between " ++ show a ++ " and " ++ show b toI18n InvalidPositive = "must be greater than 0" toI18n InvalidPositiveOrZero = "must be greater than or equal to 0" toI18n InvalidNegative = "must be less than 0" toI18n InvalidNegativeOrZero = "must be less than or equal to 0" toI18n InvalidEmail = "must be a well-formed email address" toI18n InvalidNotBlank = "must not be blank" toI18n InvalidNotEmpty = "must not be empty" toI18n InvalidPast = "must be a past date" toI18n InvalidFuture = "must be a future date" toI18n InvalidPastOrPresent = "must be a date in the past or in the present" toI18n InvalidFutureOrPresent = "must be a date in the present or in the future" toI18n (InvalidMax n) = "must be less than or equal to " ++ show n toI18n (InvalidMin n) = "must be greater than or equal to " ++ show n toI18n (InvalidDecimalMax d) = "must be less than " ++ show d toI18n (InvalidDecimalMin d) = "must be greater than " ++ show d toI18n (InvalidDigits i f) = "numeric value out of bounds (<" ++ show i ++ " digits>.<" ++ show f ++ " digits> expected)" toI18n (InvalidPattern r) = "must match " ++ r data ValidatorErr = ValidatorErr { exception :: SomeException , message :: String , field :: String } deriving Show -- | Define how invalid infomation passed to upper layer. class Monad m => HasValid m where invalid :: HasI18n a => a -> m b invalid = error . toI18n mark :: String -> m a -> m a mark _ = id instance HasValid (Either String) where invalid = Left . toI18n data VerifyResult a = Invalid [ValidatorErr] | Valid a deriving (Show, Functor) instance Applicative VerifyResult where pure = Valid (Invalid a) <*> (Invalid b) = Invalid (a ++ b) (Invalid a) <*> _ = Invalid a _ <*> (Invalid b) = Invalid b (Valid f) <*> (Valid b) = Valid (f b) instance Monad VerifyResult where return = Valid (Valid a) >>= f = f a (Invalid a) >>= _ = Invalid a instance HasValid VerifyResult where invalid e = Invalid [toErr "" e] mark name ma = let go err = if null (field err) then err { field = name } else err { field = field err ++ "." ++ name } in case ma of (Invalid x) -> Invalid $ go <$> x v -> v -- | Validator, use to define detailed validation check. type Validator a = forall m. HasValid m => m a -> m a type Validator' a = forall m. HasValid m => a -> m a vcvt :: Validator' a -> Validator a vcvt f = (>>= f) -- | Length checker bundle class HasValidSize a where -- | Size validation size :: (Word64, Word64) -> Validator a size (x,y) = vcvt $ \a -> do let la = getLength a if la < x || la > y then invalid $ InvalidSize x y else return a -- | Assert not empty notEmpty :: Validator a notEmpty = vcvt $ \a -> do if getLength a == 0 then invalid InvalidNotEmpty else return a -- | Assert not blank notBlank :: Validator a notBlank = vcvt $ \a -> do if getLength a == 0 then invalid InvalidNotBlank else return a -- | calculate length from value getLength :: a -> Word64 {-# MINIMAL getLength #-} instance HasValidSize TS.Text where getLength = fromIntegral . TS.length instance HasValidSize TL.Text where getLength = fromIntegral . TL.length instance HasValidSize [a] where getLength = fromIntegral . length -- | Regular expression validation pattern :: RegexLike Regex a => String -> Validator a pattern p = vcvt $ \a -> do if a =~ p then return a else invalid $ InvalidPattern p emailPattern :: String emailPattern = "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}$" -- | Email validation email :: RegexLike Regex a => Validator a email = vcvt $ \a -> do if a =~ emailPattern then return a else invalid InvalidEmail -- | Positive validation positive :: (Eq a, Num a) => Validator a positive = vcvt $ \a -> do if a /= 0 && abs a - a == 0 then return a else invalid InvalidPositive -- | Positive or zero validation positiveOrZero :: (Eq a, Num a) => Validator a positiveOrZero = vcvt $ \a -> do if abs a - a == 0 then return a else invalid InvalidPositiveOrZero -- | Negative validation negative :: (Eq a, Num a) => Validator a negative = vcvt $ \a -> do if a /= 0 && abs a + a == 0 then return a else invalid InvalidNegative -- | Negative or zero validation negativeOrZero :: (Eq a, Num a) => Validator a negativeOrZero = vcvt $ \a -> do if abs a + a == 0 then return a else invalid InvalidNegativeOrZero -- | Assert true assertTrue :: Validator Bool assertTrue = vcvt $ \a -> do if a then return a else invalid ShouldBeTrue -- | Assert false assertFalse :: Validator Bool assertFalse = vcvt $ \a -> do if not a then return a else invalid ShouldBeFalse -- | Assert not null notNull :: Validator (Maybe a) notNull = vcvt $ \a -> do case a of Just _ -> return a _ -> invalid ShouldNotNull -- | Assert null assertNull :: Validator (Maybe a) assertNull = vcvt $ \a -> do case a of Just _ -> invalid ShouldNull _ -> return a -- | Maximum int validation maxInt :: Integral a => a -> Validator a maxInt m = vcvt $ \a -> do if a > m then invalid (InvalidMax $ toInteger m) else return a -- | Minimum int validation minInt :: Integral a => a -> Validator a minInt m = vcvt $ \a -> do if a < m then invalid (InvalidMin $ toInteger m) else return a -- | Maximum decimal validation maxDecimal :: RealFloat a => a -> Validator a maxDecimal m = vcvt $ \a -> do if a > m then invalid (InvalidDecimalMax $ fromFloatDigits m) else return a -- | Minimum int validation minDecimal :: RealFloat a => a -> Validator a minDecimal m = vcvt $ \a -> do if a < m then invalid (InvalidDecimalMin $ fromFloatDigits m) else return a -- | lift value a to validation context and check if it is valid. verify :: HasValid m => a -> Validator a -> m a verify a f = f (return a) -- $use -- -- This library is designed for validate haskell records, such as in configurations or web request parameters. -- -- Usage: -- -- > {-# LANGUAGE RecordWildCards #-} -- > module Main where -- > import Data.Menshen -- > data Body = Body -- > { name :: String -- > , age :: Int -- > } deriving Show -- > -- > verifyBody :: Validator Body -- > verifyBody = vcvt $ \Body{..} -> Body -- > <$> name ?: mark "name" . pattern "^[a-z]{3,6}$" -- > <*> age ?: mark "age" . minInt 1 . maxInt 150 -- > -- > makeBody :: String -> Int -> Either String Body -- > makeBody name age = Body{..} ?: verifyBody -- > -- > main = do -- > print $ makeBody "daniel" 15 -- > -- -- Useage in Record parsing process: -- -- > instance HasValid Parser where -- > invalid = fail . toI18n -- > -- > instance FromJSON Body where -- > parseJSON = withObject "Body" $ \v -> Body -- > <$> v .: "name" ? pattern "^[a-z]{3,6}$" -- > <*> v .: "age" ? minInt 1 . maxInt 150 -- >