packages feed

validity 0.3.3.0 → 0.4.0.0

raw patch · 2 files changed

+429/−62 lines, 2 filesdep ~base

Dependency ranges changed: base

Files

src/Data/Validity.hs view
@@ -1,8 +1,9 @@ {-# LANGUAGE CPP #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeOperators #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE DefaultSignatures #-}  {-| @@ -27,6 +28,7 @@     https://hackage.haskell.org/package/genvalidity ):      > instance Validity Prime where+    >     validate (Prime n) = isPrime n <?@> "The 'Int' is prime."     >     isValid (Prime n) = isPrime n      If certain typeclass invariants exist, you can make these explicit in the@@ -36,14 +38,40 @@     -} module Data.Validity     ( Validity(..)+    -- * Helper functions to define 'isValid'+    , triviallyValid+    -- * Helper functions to define 'validate'+    , trivialValidation+    , isValidByValidating+    , check+    , (<?!>)+    , annotate+    , (<?@>)+    , validateByChecking+    , validateByCheckingName+    , validateByCheckingDefault+    -- * Utilities+    -- ** Utilities for validity checking     , isInvalid     , constructValid     , constructValidUnsafe+    -- ** Utilities for validation+    , Validation(..)+    , ValidationChain(..)+    , checkValidity+    , prettyValidation+    -- * Re-exports+    , Monoid(..)     ) where +import Debug.Trace++import Data.Either (isRight) import Data.Fixed (Fixed(MkFixed), HasResolution)+import Data.List (intercalate) import Data.Maybe (Maybe, fromMaybe)-import Data.Word (Word, Word8, Word16)+import Data.Monoid+import Data.Word (Word, Word16, Word32, Word64, Word8) import GHC.Generics #if MIN_VERSION_base(4,8,0) import GHC.Natural (Natural, isValidNatural)@@ -53,6 +81,46 @@ -- | A class of types that have additional invariants defined upon them -- that aren't enforced by the type system --+-- === Purpose+--+-- 'validate' checks whether a given value is a valid value and reports all+-- reasons why the given value is not valid if that is the case.+--+-- 'isValid' only checks whether a given value is a valid value of its type.+--+-- === Instantiating 'Validity'+--+-- To instantiate 'Validity', one has to implement both 'isValid' and+-- 'validate'.+-- Start by implementing 'validate'. Use the helper functions below to define+-- all the reasons why a given value would be a valid value of its type.+-- Then define `isValid = isValidbyValidating' for now.+--+-- Example:+--+-- > newtype Even = Even Int+-- >+-- > instance Validity Even+-- >     validate (Event i)+-- >       even i <?@> "The contained 'Int' is even."+-- >     isValid = isValidByValidating+--+-- If it turns out that, at this point, 'isValid' is too slow for your taste,+-- you can replace the implementation of 'isValid' by a custom implementation.+-- However, it is important that this 'isValid' implementation has exactly+-- the same semantics as 'isValidbyValidating'.+--+-- Example:+--+-- > newtype Even = Even Int+-- >+-- > instance Validity Even+-- >     validate (Event i)+-- >       even i <?@> "The contained 'Int' is even."+-- >     isValid (Event i) = even i+--+-- === Semantics+-- -- 'isValid' should be an underapproximation of actual validity. -- -- This means that if 'isValid' is not a perfect representation of actual@@ -73,89 +141,310 @@ -- > isValid (Even i) = even i || i == 1 -- -- is not because it returns 'True' for an invalid value: '1'.+--+-- === Automatic instances with 'Generic'+--+-- An instance of this class can be made automatically if the type in question+-- has a 'Generic' instance. This instance will try to use 'isValid' to+-- on all structural sub-parts of the value that is being checked for validity.+--+-- Example:+--+-- > {-# LANGUAGE DeriveGeneric #-}+-- >+-- > data MyType = MyType+-- >     { myDouble :: Double+-- >     { myString :: String+-- >     } deriving (Show, Eq, Generic)+-- >+-- > instance Validity MyType+--+-- generates something like:+--+-- > instance Validity MyType where+-- >     isValid (MyType d s)+-- >         = isValid d && isValid s+-- >     validate (MyType d s)+-- >         = d <?!> "myDouble"+-- >        <> s <?!> "myString" class Validity a where+    validate :: a -> Validation+    default validate :: (Generic a, GValidity (Rep a)) =>+        a -> Validation+    validate = gValidate . from     isValid :: a -> Bool     default isValid :: (Generic a, GValidity (Rep a)) =>         a -> Bool     isValid = gIsValid . from -isInvalid-    :: Validity a-    => a -> Bool-isInvalid = not . isValid+data ValidationChain+    = Violated String+    | Location String+               ValidationChain+    deriving (Show, Eq, Generic) +instance Validity ValidationChain++data Validation = Validation+    { unValidation :: [ValidationChain]+    } deriving (Show, Eq, Generic)++instance Validity Validation++instance Monoid Validation where+    mempty = Validation []+    mappend (Validation v1) (Validation v2) = Validation $ v1 ++ v2++-- | Declare any value to be valid.+--+-- > triviallyValid a = seq a True+triviallyValid :: a -> Bool+triviallyValid a = seq a True++-- | Implement 'isValid' by using 'validate' and checking that there are no+-- reasons that the value is invalid.+isValidByValidating :: Validity a => a -> Bool+isValidByValidating = isRight . checkValidity++-- | Declare any value to be valid in validation+--+-- > trivialValidation a = seq a mempty+trivialValidation :: a -> Validation+trivialValidation a = seq a mempty++-- | Implement 'validate' by using 'isValid' and using the given string as the+-- reason if the value is invalid.+validateByChecking :: Validity a => String -> a -> Validation+validateByChecking s a = isValid a <?@> s++-- | Implement 'validate' by using 'isValid' and using the given name to define+-- the reason if the value is invalid.+--+-- > validateByCheckingName name = validateByChecking $ unwords ["The", name, "valid."]+validateByCheckingName :: Validity a => String -> a -> Validation+validateByCheckingName name =+    validateByChecking $ unwords ["The", name, "valid."]++-- | Implement 'validate' by using 'isValid' and using a default reason if the+-- value is invalid.+--+-- > validateByCheckingDefault = validateByChecking "The value is valid."+validateByCheckingDefault :: Validity a => a -> Validation+validateByCheckingDefault = validateByChecking "The value is valid."++-- | Check that a given invariant holds.+--+-- The given string should describe the invariant, not the violation.+--+-- Example:+--+-- > check (x < 5) "x is strictly smaller than 5"+--+-- instead of+--+-- > check (x < 5) "x is greater than 5"+check :: Bool -> String -> Validation+check b err =+    if b+        then mempty+        else Validation [Violated err]++-- | Infix operator for 'check'+--+-- Example:+--+-- > x < 5 <?@> "x is strictly smaller than 5"+(<?@>) :: Bool -> String -> Validation+(<?@>) = check++infixr 0 <?@>++-- | Declare a sub-part as a necessary part for validation, and annotate it with a name.+--+-- Example:+--+-- > validate (a, b) =+-- >     mconcat+-- >         [ annotate a "The first element of the tuple"+-- >         , annotate b "The second element of the tuple"+-- >         ]+annotate :: Validity a => a -> String -> Validation+annotate = annotateValidation . validate++-- | Infix operator for 'annotate'+--+-- Example:+--+-- > validate (a, b) =+-- >     mconcat+-- >         [ a <?!> "The first element of the tuple"+-- >         , b <?!> "The second element of the tuple"+-- >         ]+(<?!>) :: Validity a => a -> String -> Validation+(<?!>) = annotate++infixr 0 <?!>+ -- | Any tuple of things is valid if both of its elements are valid-instance (Validity a, Validity b) =>-         Validity (a, b) where+instance (Validity a, Validity b) => Validity (a, b) where     isValid (a, b) = isValid a && isValid b+    validate (a, b) =+        mconcat+            [ a <?!> "The first element of the tuple"+            , b <?!> "The second element of the tuple"+            ]  -- | Any Either of things is valid if the contents are valid in either of the cases.-instance (Validity a, Validity b) =>-         Validity (Either a b) where+instance (Validity a, Validity b) => Validity (Either a b) where     isValid (Left a) = isValid a     isValid (Right b) = isValid b+    validate (Left a) = a <?!> "The 'Left'"+    validate (Right b) = b <?!> "The 'Right'" --- | Any tuple of things is valid if all three of its elements are valid-instance (Validity a, Validity b, Validity c) =>-         Validity (a, b, c) where+-- | Any triple of things is valid if all three of its elements are valid+instance (Validity a, Validity b, Validity c) => Validity (a, b, c) where     isValid (a, b, c) = isValid a && isValid b && isValid c+    validate (a, b, c) =+        mconcat+            [ a <?!> "The first element of the triple"+            , b <?!> "The second element of the triple"+            , c <?!> "The third element of the triple"+            ] +-- | Any quadruple of things is valid if all four of its elements are valid+instance (Validity a, Validity b, Validity c, Validity d) =>+         Validity (a, b, c, d) where+    isValid (a, b, c, d) = isValid a && isValid b && isValid c && isValid d+    validate (a, b, c, d) =+        mconcat+            [ a <?!> "The first element of the quadruple"+            , b <?!> "The second element of the quadruple"+            , c <?!> "The third element of the quadruple"+            , d <?!> "The fourth element of the quadruple"+            ]++-- | Any quintuple of things is valid if all five of its elements are valid+instance (Validity a, Validity b, Validity c, Validity d, Validity e) =>+         Validity (a, b, c, d, e) where+    isValid (a, b, c, d, e) =+        isValid a && isValid b && isValid c && isValid d && isValid e+    validate (a, b, c, d, e) =+        mconcat+            [ a <?!> "The first element of the quintuple"+            , b <?!> "The second element of the quintuple"+            , c <?!> "The third element of the quintuple"+            , d <?!> "The fourth element of the quintuple"+            , e <?!> "The fifth element of the quintuple"+            ]++-- | Any sextuple of things is valid if all six of its elements are valid+instance ( Validity a+         , Validity b+         , Validity c+         , Validity d+         , Validity e+         , Validity f+         ) =>+         Validity (a, b, c, d, e, f) where+    isValid (a, b, c, d, e, f) =+        isValid a &&+        isValid b && isValid c && isValid d && isValid e && isValid f+    validate (a, b, c, d, e, f) =+        mconcat+            [ a <?!> "The first element of the sextuple"+            , b <?!> "The second element of the sextuple"+            , c <?!> "The third element of the sextuple"+            , d <?!> "The fourth element of the sextuple"+            , e <?!> "The fifth element of the sextuple"+            , f <?!> "The sixth element of the sextuple"+            ]+ -- | A list of things is valid if all of the things are valid. -- -- This means that the empty list is considered valid. -- If the empty list should not be considered valid as part of your custom data -- type, make sure to write a custom @Validity instance@-instance Validity a =>-         Validity [a] where+instance Validity a => Validity [a] where     isValid = all isValid+    validate =+        mconcat .+        map+            (\(ix, e) ->+                 e <?!>+                 (unwords ["The element at index", show ix, "in the list"])) .+        zip [0 ..]  -- | A Maybe thing is valid if the thing inside is valid or it's nothing -- It makes sense to assume that 'Nothing' is valid. -- If Nothing wasn't valid, you wouldn't have used a Maybe -- in the datastructure.-instance Validity a =>-         Validity (Maybe a) where+instance Validity a => Validity (Maybe a) where     isValid Nothing = True     isValid (Just a) = isValid a+    validate Nothing = mempty+    validate (Just a) = a <?!> "The 'Just'"  -- | Trivially valid instance Validity () where-    isValid = const True+    isValid = triviallyValid+    validate = validateByCheckingName "()"  -- | Trivially valid instance Validity Bool where-    isValid = const True+    isValid = triviallyValid+    validate = validateByCheckingName "Bool"  -- | Trivially valid instance Validity Ordering where-    isValid = const True+    isValid = triviallyValid+    validate = validateByCheckingName "Ordering"  -- | Trivially valid instance Validity Char where-    isValid = const True+    isValid = triviallyValid+    validate = validateByCheckingName "Char"  -- | Trivially valid instance Validity Int where-    isValid = const True+    isValid = triviallyValid+    validate = validateByCheckingName "Int"  -- | Trivially valid instance Validity Word where-    isValid = const True+    isValid = triviallyValid+    validate = validateByCheckingName "Word"  -- | Trivially valid instance Validity Word8 where-    isValid = const True+    isValid = triviallyValid+    validate = validateByCheckingName "Word8"  -- | Trivially valid instance Validity Word16 where-    isValid = const True+    isValid = triviallyValid+    validate = validateByCheckingName "Word16" +-- | Trivially valid+instance Validity Word32 where+    isValid = triviallyValid+    validate = validateByCheckingName "Word34"++-- | Trivially valid+instance Validity Word64 where+    isValid = triviallyValid+    validate = validateByCheckingName "Word64"+ -- | NOT trivially valid: -- -- * NaN is not valid. -- * Infinite values are not valid. instance Validity Float where-    isValid d = not (isNaN d) && not (isInfinite d)+    isValid f = not (isNaN f) && not (isInfinite f)+    validate f =+        mconcat+            [ not (isNaN f) <?@> "The Float is not Nan."+            , not (isInfinite f) <?@> "The Float is not infinite."+            ]  -- | NOT trivially valid: --@@ -163,6 +452,11 @@ -- * Infinite values are not valid. instance Validity Double where     isValid d = not (isNaN d) && not (isInfinite d)+    validate d =+        mconcat+            [ not (isNaN d) <?@> "The Double is not NaN."+            , not (isInfinite d) <?@> "The Double is not infinite."+            ]  -- | Trivially valid --@@ -173,60 +467,120 @@ -- assuming that an 'Integer' is always valid. -- Even though this is not technically sound, it is good enough for now. instance Validity Integer where-    isValid = const True+    isValid = triviallyValid+    validate = validateByCheckingName "Integer" #if MIN_VERSION_base(4,8,0) -- | Valid according to 'isValidNatural' -- -- Only available with @base >= 4.8@. instance Validity Natural where     isValid = isValidNatural+    validate = validateByChecking "Natural" #endif -- | Valid if the contained 'Integer's are valid and the denominator is -- strictly positive. instance Validity Rational where-    isValid (d :% n) = and [isValid n, isValid d, d > 0]+    isValid (d :% n) = isValid n && isValid d && d > 0+    validate (d :% n) =+        mconcat+            [ d <?!> "The numerator"+            , n <?!> "The denominator"+            , (d > 0) <?@> "The denominator is strictly positive."+            ]  -- | Valid according to the contained 'Integer'.-instance HasResolution a =>-         Validity (Fixed a) where+instance HasResolution a => Validity (Fixed a) where     isValid (MkFixed i) = isValid i---- | Construct a valid element from an unchecked element-constructValid-    :: Validity a-    => a -> Maybe a-constructValid p =-    if isValid p-        then Just p-        else Nothing+    validate (MkFixed i) = validate i --- | Construct a valid element from an unchecked element, throwing 'error'--- on invalid elements.-constructValidUnsafe-    :: (Show a, Validity a)-    => a -> a-constructValidUnsafe p =-    fromMaybe (error $ show p ++ " is not valid") $ constructValid p+annotateValidation :: Validation -> String -> Validation+annotateValidation val s =+    case val of+        Validation errs -> Validation $ map (Location s) errs  class GValidity f where     gIsValid :: f a -> Bool+    gValidate :: f a -> Validation  instance GValidity U1 where-    gIsValid U1 = True+    gIsValid = triviallyValid+    gValidate = trivialValidation -instance (GValidity a, GValidity b) =>-         GValidity (a :*: b) where+instance GValidity V1 where+    gIsValid = triviallyValid+    gValidate = trivialValidation++instance (GValidity a, GValidity b) => GValidity (a :*: b) where     gIsValid (a :*: b) = gIsValid a && gIsValid b+    gValidate (a :*: b) = gValidate a `mappend` gValidate b -instance (GValidity a, GValidity b) =>-         GValidity (a :+: b) where+instance (GValidity a, GValidity b) => GValidity (a :+: b) where     gIsValid (L1 x) = gIsValid x     gIsValid (R1 x) = gIsValid x+    gValidate (L1 x) = gValidate x+    gValidate (R1 x) = gValidate x -instance (GValidity a) =>-         GValidity (M1 i c a) where+instance (GValidity a, Datatype c) => GValidity (M1 D c a) where     gIsValid (M1 x) = gIsValid x+    gValidate m1 = gValidate (unM1 m1) -instance (Validity a) =>-         GValidity (K1 i a) where+instance (GValidity a, Constructor c) => GValidity (M1 C c a) where+    gIsValid (M1 x) = gIsValid x+    gValidate m1 = gValidate (unM1 m1) `annotateValidation` conName m1++instance (GValidity a, Selector c) => GValidity (M1 S c a) where+    gIsValid (M1 x) = gIsValid x+    gValidate m1 = gValidate (unM1 m1) `annotateValidation` selName m1++instance (Validity a) => GValidity (K1 R a) where     gIsValid (K1 x) = isValid x+    gValidate (K1 x) = validate x++-- | Check whether 'isInvalid' is not valid.+--+-- > isInvalid = not . isValid+isInvalid :: Validity a => a -> Bool+isInvalid = not . isValid++-- | Construct a valid element from an unchecked element+constructValid :: Validity a => a -> Maybe a+constructValid p =+    if isValid p+        then Just p+        else Nothing++-- | Construct a valid element from an unchecked element, throwing 'error'+-- on invalid elements.+constructValidUnsafe :: (Show a, Validity a) => a -> a+constructValidUnsafe p =+    fromMaybe (error $ show p ++ " is not valid") $ constructValid p++-- | validate a given value.+--+-- This function returns either all the reasons why the given value is invalid,+-- in the form of a list of 'ValidationChain's, or it returns 'Right' with the+-- input value, as evidence that it is valid.+--+-- Note: You map want to use 'prettyValidation' instead, if you want to+-- display these 'ValidationChain's to a user.+checkValidity :: Validity a => a -> Either [ValidationChain] a+checkValidity a =+    case validate a of+        Validation [] -> Right a+        Validation errs -> Left errs++-- | validate a given value, and return a nice error if the value is invalid.+prettyValidation :: Validity a => a -> Either String a+prettyValidation a =+    case checkValidity a of+        Right a -> Right a+        Left errs -> Left $ intercalate "\n" $ map (errCascade . toStrings) errs+  where+    toStrings (Violated s) = ["Violated: " ++ s]+    toStrings (Location s vc) = s : toStrings vc+    errCascade errList =+        intercalate "\n" $+        flip map (zip [0 ..] errList) $ \(i, segment) ->+            case i of+                0 -> segment+                _ -> replicate i ' ' ++ "\\ " ++ segment
validity.cabal view
@@ -1,5 +1,5 @@ name: validity-version: 0.3.3.0+version: 0.4.0.0 cabal-version: >=1.10 build-type: Simple license: MIT@@ -13,15 +13,26 @@     .     Note: There are companion instance packages for this library:     .-    * <https://hackage.haskell.org/package/validity-text validity-text>+    * <https://hackage.haskell.org/package/validity-aeson validity-aeson>     .+    * <https://hackage.haskell.org/package/validity-bytestring validity-bytestring>+    .+    * <https://hackage.haskell.org/package/validity-containers validity-containers>+    .     * <https://hackage.haskell.org/package/validity-path validity-path>     .+    * <https://hackage.haskell.org/package/validity-scientific validity-scientific>+    .+    * <https://hackage.haskell.org/package/validity-text validity-text>+    .     * <https://hackage.haskell.org/package/validity-time validity-time>     .-    * <https://hackage.haskell.org/package/validity-containers validity-containers>+    * <https://hackage.haskell.org/package/validity-unordered-containers validity-unordered-containers>     .-    * <https://hackage.haskell.org/package/validity-bytestring validity-bytestring>+    * <https://hackage.haskell.org/package/validity-uuid validity-uuid>+    .+    * <https://hackage.haskell.org/package/validity-vector validity-vector>+    . category: Validity author: Tom Sydney Kerckhove @@ -30,6 +41,9 @@     location: https://github.com/NorfairKing/validity  library+    +    if impl(ghc >=8.0.0)+        ghc-options: -Wno-redundant-constraints     exposed-modules:         Data.RelativeValidity         Data.Validity@@ -37,5 +51,4 @@         base >=4.7 && <5     default-language: Haskell2010     hs-source-dirs: src-    if impl(ghc >= 8.0.0)-        ghc-options: -Wno-redundant-constraints+