packages feed

validity-0.12.0.2: src/Data/Validity.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}

-- |
--
--    @Validity@ is used to specify additional invariants upon values that are not
--    enforced by the type system.
--
--    Let's take an example.
--    Suppose we were to implement a type @Prime@ that represents prime integers.
--
--    If you were to completely enforce the invariant that the represented number is
--    a prime, then we could use 'Natural' and only store the index of the
--    given prime in the infinite sequence of prime numbers.
--    This is very safe but also very expensive if we ever want to use the number,
--    because we would have to calculcate all the prime numbers until that index.
--
--    Instead we choose to implement @Prime@ by a @newtype Prime = Prime Int@.
--    Now we have to maintain the invariant that the @Int@ that we use to represent
--    the prime is in fact positive and a prime.
--
--    The @Validity@ typeclass allows us to specify this invariant (and enables
--    testing via the @genvalidity@ libraries:
--    https://hackage.haskell.org/package/genvalidity ):
--
--    > instance Validity Prime where
--    >     validate (Prime n) = check (isPrime n) "The 'Int' is prime."
--
--    If certain typeclass invariants exist, you can make these explicit in the
--    validity instance as well.
--    For example, 'Fixed a' is only valid if 'a' has an 'HasResolution' instance,
--    so the correct validity instance is @HasResolution a => Validity (Fixed a)@.
module Data.Validity
  ( Validity (..),

    -- * Helper functions to define 'validate'
    trivialValidation,
    genericValidate,
    check,
    declare,
    annotate,
    delve,
    decorate,
    decorateList,
    decorateString,
    invalid,
    valid,

    -- ** Helpers for specific types

    -- *** Char
    validateCharNotUtf16SurrogateCodePoint,
    isUtf16SurrogateCodePoint,
    validateCharNotLineSeparator,
    isLineSeparator,
    validateStringSingleLine,
    isSingleLine,

    -- *** RealFloat (Double)
    validateNotNaN,
    validateNotInfinite,

    -- *** Ratio
    validateRatioNotNaN,
    validateRatioNotInfinite,
    validateRatioNormalised,

    -- * Utilities

    -- ** Utilities for validity checking
    isValid,
    isInvalid,
    constructValid,
    constructValidUnsafe,

    -- ** Utilities for validation
    Validation (..),
    ValidationChain (..),
    checkValidity,
    validationIsValid,
    prettyValidate,
    prettyValidation,

    -- * Re-exports
    Monoid (..),
    Semigroup (..),
  )
where

import Data.Bits ((.&.))
import Data.Char (ord)
import Data.Either (isRight)
import Data.Fixed (Fixed (MkFixed), HasResolution)
import Data.Int (Int64)
import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Maybe (fromMaybe)
#if MIN_VERSION_base(4,16,0)
import GHC.Exts (Char (..), isTrue#, ord#, (<=#), (>=#))
#else
import GHC.Exts (Char (..), isTrue#, leWord#, ord#, (<=#), (>=#))
#endif
import GHC.Generics
import GHC.Int (Int16 (..), Int32 (..), Int8 (..))
import GHC.Natural
import GHC.Real (Ratio (..))
import GHC.Word (Word16 (..), Word32 (..), Word64 (..), Word8 (..))

-- | A class of types that have additional invariants defined upon them

--
-- === 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.
-- It is a helper function that checks that 'validate' says that there are
-- no reasons why the value is invalid.
--
-- === Instantiating 'Validity'
--
-- To instantiate 'Validity', one has to implement only 'validate'.
-- Use the helper functions below to define all the reasons why a given
-- value would be a valid value of its type.
--
-- Example:
--
-- > newtype Even = Even Int
-- >
-- > instance Validity Even
-- >     validate (Event i)
-- >       even i <?@> "The contained 'Int' is even."
--
-- === Semantics
--
-- 'validate' should be an underapproximation of actual validity.
--
-- This means that if 'isValid' is not a perfect representation of actual
-- validity, for safety reasons, it should never return 'True' for invalid
-- values, but it may return 'False' for valid values.
--
-- For example:
--
-- > validate = const $ invalid "always"
--
-- is a valid implementation for any type, because now 'isValid' never returns
-- 'True' for invalid values.
--
-- > validate (Even i) = declare "The integer is equal to two" $ i == 2
--
-- is a valid implementation for @newtype Even = Even Int@, but
--
-- > validate (Even i) = declare "The integer is even or equal to one" $ even i || i == 1
--
-- is not because then `isValid` 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 'valid' 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
-- >     validate (MyType d s)
-- >         = annotate d "myDouble"
-- >        <> annotate s "myString"
class Validity a where
  validate :: a -> Validation
  default validate ::
    (Generic a, GValidity (Rep a)) =>
    a ->
    Validation
  validate = genericValidate

genericValidate :: (Generic a, GValidity (Rep a)) => a -> Validation
genericValidate = gValidate . from

data ValidationChain
  = Violated String
  | Location
      String
      ValidationChain
  deriving (Show, Eq, Generic)

instance Validity ValidationChain

-- | The result of validating a value.
--
-- `mempty` means the value was valid.
--
-- This type intentionally doesn't have a `Validity` instance to make sure
-- you can never accidentally use `annotate` or `delve` twice.
newtype Validation = Validation
  { unValidation :: [ValidationChain]
  }
  deriving (Show, Eq, Generic)

instance Semigroup Validation where
  (Validation v1) <> (Validation v2) = Validation $ v1 ++ v2

instance Monoid Validation where
  mempty = Validation []
  mappend = (<>)

-- | Declare any value to be valid in validation
--
-- > trivialValidation a = seq a mempty
trivialValidation :: a -> Validation
trivialValidation a = seq a mempty

-- | 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]

-- | 'check', but with the arguments flipped
declare :: String -> Bool -> Validation
declare = flip check

-- | 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

-- | 'annotate', but with the arguments flipped.
delve :: Validity a => String -> a -> Validation
delve = flip annotate

-- | Decorate a validation with a location
decorate :: String -> Validation -> Validation
decorate = flip annotateValidation

-- | Decorate a piecewise validation of a list with their location in the list
decorateList :: [a] -> (a -> Validation) -> Validation
decorateList as func = mconcat $
  flip map (zip [0 ..] as) $ \(i, a) ->
    decorate (unwords ["The element at index", show (i :: Integer), "in the list"]) $
      func a

-- | 'decorateList', but specifically for 'String's
--
-- > decorateString = decorateList
decorateString :: String -> (Char -> Validation) -> Validation
decorateString = decorateList

-- | Construct a trivially invalid 'Validation'
--
-- Example:
--
-- > data Wrong
-- >     = Wrong
-- >     | Fine
-- >     deriving (Show, Eq)
-- >
-- > instance Validity Wrong where
-- >     validate w =
-- >         case w of
-- >             Wrong -> invalid "Wrong"
-- >             Fine -> valid
invalid :: String -> Validation
invalid = check False

valid :: Validation
valid = mempty

-- | Any tuple of things is valid if both of its elements are valid
instance (Validity a, Validity b) => Validity (a, b) where
  validate (a, b) =
    mconcat
      [ annotate a "The first element of the tuple",
        annotate 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
  validate (Left a) = annotate a "The 'Left'"
  validate (Right b) = annotate b "The 'Right'"

-- | 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
  validate (a, b, c) =
    mconcat
      [ annotate a "The first element of the triple",
        annotate b "The second element of the triple",
        annotate 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
  validate (a, b, c, d) =
    mconcat
      [ annotate a "The first element of the quadruple",
        annotate b "The second element of the quadruple",
        annotate c "The third element of the quadruple",
        annotate 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
  validate (a, b, c, d, e) =
    mconcat
      [ annotate a "The first element of the quintuple",
        annotate b "The second element of the quintuple",
        annotate c "The third element of the quintuple",
        annotate d "The fourth element of the quintuple",
        annotate 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
  validate (a, b, c, d, e, f) =
    mconcat
      [ annotate a "The first element of the sextuple",
        annotate b "The second element of the sextuple",
        annotate c "The third element of the sextuple",
        annotate d "The fourth element of the sextuple",
        annotate e "The fifth element of the sextuple",
        annotate 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
  validate = flip decorateList validate

-- | A nonempty list is valid if all the elements are valid.
--
-- See the instance for 'Validity [a]' for more information.
instance Validity a => Validity (NonEmpty a) where
  validate (e :| es) =
    mconcat
      [ annotate e "The first element of the nonempty list",
        annotate es "The rest of the elements of the nonempty list"
      ]

-- | 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
  validate Nothing = mempty
  validate (Just a) = annotate a "The 'Just'"

-- | Trivially valid
instance Validity () where
  validate = trivialValidation

-- | Trivially valid
instance Validity Bool where
  validate = trivialValidation

-- | Trivially valid
instance Validity Ordering where
  validate = trivialValidation

-- | Trivially valid
instance Validity Char where
  validate (C# c#) =
    mconcat
      [ declare "The contained value is positive" $ isTrue# (ord# c# >=# 0#),
        declare "The contained value is smaller than 0x10FFFF = 1114111" $ isTrue# (ord# c# <=# 1114111#)
      ]

validateCharNotUtf16SurrogateCodePoint :: Char -> Validation
validateCharNotUtf16SurrogateCodePoint c =
  declare "The character is not a UTF16 surrogate codepoint" $ not $ isUtf16SurrogateCodePoint c

isUtf16SurrogateCodePoint :: Char -> Bool
isUtf16SurrogateCodePoint c = ord c .&. 0x1ff800 == 0xd800

validateCharNotLineSeparator :: Char -> Validation
validateCharNotLineSeparator c =
  declare "The character is not a line separator" $ not $ isLineSeparator c

isLineSeparator :: Char -> Bool
isLineSeparator c = case c of
  '\n' -> True
  '\r' -> True
  _ -> False

validateStringSingleLine :: String -> Validation
validateStringSingleLine s = decorateList s validateCharNotLineSeparator

isSingleLine :: String -> Bool
isSingleLine = not . any isLineSeparator

-- | Trivially valid
instance Validity Int where
  validate = trivialValidation

#if MIN_VERSION_base(4,16,0)
instance Validity Int8 where validate = trivialValidation
instance Validity Int16 where validate = trivialValidation
instance Validity Int32 where validate = trivialValidation
#else
-- | NOT trivially valid on GHC because small number types are represented using a 64bit structure underneath.
instance Validity Int8 where
  validate (I8# i#) =
    mconcat
      [ declare "The contained integer is smaller than 2^7 = 128" $ isTrue# (i# <=# 127#),
        declare "The contained integer is greater than or equal to -2^7 = -128" $ isTrue# (i# >=# -128#)
      ]

-- | NOT trivially valid on GHC because small number types are represented using a 64bit structure underneath.
instance Validity Int16 where
  validate (I16# i#) =
    mconcat
      [ declare "The contained integer is smaller than 2^15 = 32768" $ isTrue# (i# <=# 32767#),
        declare "The contained integer is greater than or equal to -2^15 = -32768" $ isTrue# (i# >=# -32768#)
      ]

-- | NOT trivially valid on GHC because small number types are represented using a 64bit structure underneath.
instance Validity Int32 where
  validate (I32# i#) =
    mconcat
      [ declare "The contained integer is smaller than 2^31 = 2147483648" $ isTrue# (i# <=# 2147483647#),
        declare "The contained integer is greater than or equal to -2^31 = -2147483648" $ isTrue# (i# >=# -2147483648#)
      ]
#endif

-- | Trivially valid
instance Validity Int64 where
  validate = trivialValidation

-- | Trivially valid
instance Validity Word where
  validate = trivialValidation

#if MIN_VERSION_base(4,16,0)
instance Validity Word8 where validate = trivialValidation
instance Validity Word16 where validate = trivialValidation
instance Validity Word32 where validate = trivialValidation
#else
-- | NOT trivially valid on GHC because small number types are represented using a 64bit structure underneath.
instance Validity Word8 where
  validate (W8# w#) =
    declare "The contained integer is smaller than 2^8 = 256" $ isTrue# (w# `leWord#` 255##)

-- | NOT trivially valid on GHC because small number types are represented using a 64bit structure underneath.
instance Validity Word16 where
  validate (W16# w#) =
    declare "The contained integer is smaller than 2^16 = 65536" $ isTrue# (w# `leWord#` 65535##)

-- | NOT trivially valid on GHC because small number types are represented using a 64bit structure underneath.
instance Validity Word32 where
  validate (W32# w#) =
    declare "The contained integer is smaller than 2^32 = 4294967296" $ isTrue# (w# `leWord#` 4294967295##)
#endif

-- | Trivially valid
instance Validity Word64 where
  validate = trivialValidation

-- | Trivially valid:
instance Validity Float where
  validate = trivialValidation

-- | Trivially valid:
instance Validity Double where
  validate = trivialValidation

validateNotNaN :: RealFloat a => a -> Validation
validateNotNaN d = declare "The RealFloat is not NaN." $ not (isNaN d)

validateNotInfinite :: RealFloat a => a -> Validation
validateNotInfinite d = declare "The RealFloat is not infinite." $ not (isInfinite d)

validateRatioNotNaN :: Integral a => Ratio a -> Validation
validateRatioNotNaN r = declare "The Ratio is not NaN." $
  case r of
    (0 :% 0) -> False
    _ -> True

validateRatioNotInfinite :: Integral a => Ratio a -> Validation
validateRatioNotInfinite r = declare "The Ratio is not infinite." $
  case r of
    (1 :% 0) -> False
    ((-1) :% 0) -> False
    _ -> True

validateRatioNormalised :: Integral a => Ratio a -> Validation
validateRatioNormalised (n :% d) = declare "The Ratio is normalised." $
  case d of
    0 -> False
    _ ->
      let g = gcd n d
          gcdOverflows = g < 0
          n' :% d' = (n `quot` g) :% (d `quot` g)
          valueIsNormalised = n' :% d' == n :% d
       in not gcdOverflows && valueIsNormalised

-- | Trivially valid
--
-- Integer is not trivially valid under the hood, but instantiating
-- 'Validity' correctly would force validity to depend on a specific
-- (big integer library @integer-gmp@ versus @integer-simple@).
-- This is rather impractical so for the time being we have opted for
-- assuming that an 'Integer' is always valid.
-- Even though this is not technically sound, it is good enough for now.
instance Validity Integer where
  validate = trivialValidation

-- | Valid according to 'isValidNatural'
instance Validity Natural where
  validate = declare "The Natural is valid." . isValidNatural

-- | Valid if the contained numbers are valid and the denominator is
-- strictly positive.
instance (Validity a, Ord a, Num a, Integral a) => Validity (Ratio a) where
  validate r@(n :% d) =
    mconcat
      [ annotate n "The numerator",
        annotate d "The denominator",
        declare "The denominator is strictly positive." $ d > 0,
        validateRatioNormalised r
      ]

-- | Valid according to the contained 'Integer'.
instance HasResolution a => Validity (Fixed a) where
  validate (MkFixed i) = validate i

annotateValidation :: Validation -> String -> Validation
annotateValidation val s =
  case val of
    Validation errs -> Validation $ map (Location s) errs

class GValidity f where
  gValidate :: f a -> Validation

instance GValidity U1 where
  gValidate = trivialValidation

instance GValidity V1 where
  gValidate = trivialValidation

instance (GValidity a, GValidity b) => GValidity (a :*: b) where
  gValidate (a :*: b) = gValidate a `mappend` gValidate b

instance (GValidity a, GValidity b) => GValidity (a :+: b) where
  gValidate (L1 x) = gValidate x
  gValidate (R1 x) = gValidate x

instance (GValidity a, Datatype c) => GValidity (M1 D c a) where
  gValidate m1 = gValidate (unM1 m1)

instance (GValidity a, Constructor c) => GValidity (M1 C c a) where
  gValidate m1 = gValidate (unM1 m1) `annotateValidation` conName m1

instance (GValidity a, Selector c) => GValidity (M1 S c a) where
  gValidate m1 = gValidate (unM1 m1) `annotateValidation` selName m1

instance (Validity a) => GValidity (K1 R a) where
  gValidate (K1 x) = validate x

-- | Check whether a value is valid.
isValid :: Validity a => a -> Bool
isValid = isRight . checkValidity

-- | Check whether a value 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 may 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

-- | Check if a 'Validation' concerns a valid value.
validationIsValid :: Validation -> Bool
validationIsValid v = case v of
  Validation [] -> True
  _ -> False

-- | Validate a given value
--
-- This function will return a nice error if the value is invalid.
-- It will return the original value in 'Right' if it was valid,
-- as evidence that it has been validated.
prettyValidate :: Validity a => a -> Either String a
prettyValidate a = case prettyValidation $ validate a of
  Just e -> Left e
  Nothing -> Right a

-- | Render a `Validation` in a somewhat pretty way.
--
-- This function will return 'Nothing' if the 'Validation' concerned a valid value.
prettyValidation :: Validation -> Maybe String
prettyValidation v =
  case v of
    Validation [] -> Nothing
    Validation errs -> Just $ 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