mirror of
https://github.com/idris-lang/Idris2.git
synced 2024-12-25 20:51:43 +03:00
211 lines
7.4 KiB
Idris
211 lines
7.4 KiB
Idris
module Control.Validation
|
|
|
|
-- Main purpose of this module is verifying programmer's assumptions about
|
|
-- user input. On one hand we want to write precisely typed programs, including
|
|
-- assumptions about input expressed in types and prove correctness of these
|
|
-- programs. On the other we get an unstructured user input as a string or even
|
|
-- a raw sequence of bytes.
|
|
|
|
-- This module intends to provide a framework for verifying our assumptions
|
|
-- about user input and constructing proofs that input is indeed valid or
|
|
-- failing early with a nice error message if it isn't.
|
|
|
|
import Control.Monad.Identity
|
|
import Control.Monad.Error.Either
|
|
import Data.Nat
|
|
import Data.String
|
|
import Data.Vect
|
|
import Decidable.Equality
|
|
|
|
%default total
|
|
|
|
|
|
public export
|
|
Result : (Type -> Type) -> Type -> Type
|
|
Result m = EitherT String m
|
|
|
|
||| Validators in this module come in two flavours: Structural Validators and
|
|
||| Property Validators. They are both wrappers around functions which take
|
|
||| some input and confirm that it's valid (returning some witness of its
|
|
||| validity) or fail with an error described by a string.
|
|
export
|
|
data ValidatorT : (Type -> Type) -> Type -> Type -> Type where
|
|
MkValidator : (a -> Result m b) -> ValidatorT m a b
|
|
|
|
||| A type of validator trying to prove properties of values. It's type is
|
|
||| significantly different than that of an ordinary validator and cannot be
|
|
||| made an instance of Monad interface, because it's last parameter is
|
|
||| (t -> Type) instead of just Type. Therefore it must be explicitly turned
|
|
||| into an ordinary validator using the prop function below.
|
|
data PropValidator : (Type -> Type) -> (t : Type) -> (t -> Type) -> Type where
|
|
MkPropValidator : ((x : t) -> Result m (p x)) -> PropValidator m t p
|
|
|
|
public export
|
|
Validator : Type -> Type -> Type
|
|
Validator = ValidatorT Identity
|
|
|
|
|
|
||| Run validation on given input, returning (Right refinedInput) if everything
|
|
||| is all right or (Left errorMessage) if it's not.
|
|
export
|
|
validateT : ValidatorT m a b -> a -> Result m b
|
|
validateT (MkValidator v) = v
|
|
|
|
||| Run validation within the Identity monad and unwrap result immediately.
|
|
export
|
|
validate : Validator a b -> a -> Either String b
|
|
validate v = runIdentity . runEitherT . validateT v
|
|
|
|
||| Given a function from input to Either String output, make a validator.
|
|
export
|
|
validator : (a -> Result m b) -> ValidatorT m a b
|
|
validator = MkValidator
|
|
|
|
export
|
|
Functor m => Functor (ValidatorT m a) where
|
|
map f v = MkValidator (map f . validateT v)
|
|
|
|
export
|
|
Monad m => Applicative (ValidatorT m a) where
|
|
pure a = MkValidator (const $ pure a)
|
|
f <*> a = MkValidator (\x => validateT f x <*> validateT a x)
|
|
|
|
export
|
|
Monad m => Monad (ValidatorT m a) where
|
|
v >>= f = MkValidator $ \x => do
|
|
r <- validateT v x
|
|
validateT (f r) x
|
|
|
|
||| Plug a property validator into the chain of other validators. The value
|
|
||| under validation will be ignored and the value whose property is going to
|
|
||| be checked must be supplied manually. Otherwise Idris won;t be able to
|
|
||| unify.
|
|
prop : PropValidator m t p -> (x : t) -> ValidatorT m a (p x)
|
|
prop (MkPropValidator v) x = MkValidator (const $ v x)
|
|
|
|
replaceError : Monad m => String -> Result m a -> Result m a
|
|
replaceError e = bimapEitherT (const e) id
|
|
|
|
||| Replace validator's default error message.
|
|
export
|
|
withError : Monad m => String -> ValidatorT m a b -> ValidatorT m a b
|
|
withError e (MkValidator f) = MkValidator (replaceError e . f)
|
|
|
|
||| A validator which always fails with a given message.
|
|
export
|
|
fail : Applicative m => String -> ValidatorT m a b
|
|
fail s = MkValidator $ \_ => left s
|
|
|
|
infixl 2 >>>
|
|
|
|
||| Compose two validators so that the second validates the output of the first.
|
|
export
|
|
(>>>) : Monad m => ValidatorT m a b -> ValidatorT m b c -> ValidatorT m a c
|
|
left >>> right = MkValidator (validateT left >=> validateT right)
|
|
|
|
Monad m => Alternative (ValidatorT m a) where
|
|
left <|> right = MkValidator $ \x => MkEitherT $ do
|
|
case !(runEitherT $ validateT left x) of
|
|
(Right r) => pure $ Right r
|
|
(Left e) => case !(runEitherT $ validateT right x) of
|
|
(Right r) => pure $ Right r
|
|
(Left e') => pure $ Left (e <+> " / " <+> e')
|
|
|
|
empty = MkValidator $ \x => MkEitherT $ pure (Left "invalid")
|
|
|
|
||| Alter the input before validation using given function.
|
|
export
|
|
contramap : (a -> b) -> ValidatorT m b c -> ValidatorT m a c
|
|
contramap f v = MkValidator (validateT v . f)
|
|
|
|
|
|
||| Given a value x and a decision procedure for property p, validateT if p x
|
|
||| holds, returning a proof if it does. The procedure also has access to the
|
|
||| raw input in case it was helpful.
|
|
export
|
|
decide : Monad m => (t -> String) -> ((x : t) -> Dec (p x)) -> PropValidator m t p
|
|
decide msg dec = MkPropValidator $ \x => case dec x of
|
|
Yes prf => pure prf
|
|
No _ => left (msg x)
|
|
|
|
||| Given a function converting a into Maybe b, build a Validator of a
|
|
||| converting it into b.
|
|
export
|
|
fromMaybe : Monad m => (a -> String) -> (a -> Maybe b) -> ValidatorT m a b
|
|
fromMaybe e f = MkValidator $ \a => case f a of
|
|
Nothing => left $ e a
|
|
Just b => pure b
|
|
|
|
||| Verify whether a String represents a natural number.
|
|
export
|
|
natural : Monad m => ValidatorT m String Nat
|
|
natural = fromMaybe mkError parsePositive
|
|
where
|
|
mkError : String -> String
|
|
mkError s = "'" <+> s <+> "' is not a natural number."
|
|
|
|
||| Verify whether a String represents an Integer
|
|
export
|
|
integral : Num a => Neg a => Monad m => ValidatorT m String a
|
|
integral = fromMaybe mkError parseInteger
|
|
where
|
|
mkError : String -> String
|
|
mkError s = "'" <+> s <+> "' is not an integer."
|
|
|
|
||| Verify that a string represents a decimal fraction.
|
|
export
|
|
double : Monad m => ValidatorT m String Double
|
|
double = fromMaybe mkError parseDouble
|
|
where
|
|
mkError : String -> String
|
|
mkError s = "'" <+> s <+> "is not a decimal."
|
|
|
|
|
|
||| Verify whether a list has a desired length.
|
|
export
|
|
length : Monad m => (l : Nat) -> ValidatorT m (List a) (Vect l a)
|
|
length l = MkValidator (validateVector l)
|
|
where
|
|
validateVector : (l : Nat) -> List a -> Result m (Vect l a)
|
|
validateVector Z [] = pure []
|
|
validateVector (S _) [] = left "Missing list element."
|
|
validateVector Z (_ :: _) = left "Excessive list element."
|
|
validateVector (S k) (x :: xs) = do
|
|
ys <- validateVector k xs
|
|
pure (x :: ys)
|
|
|
|
||| Verify that certain values are equal.
|
|
export
|
|
equal : Monad m => DecEq t => (a : t) -> PropValidator m t (\b => a = b)
|
|
equal a = MkPropValidator $ \b => case decEq a b of
|
|
Yes prf => pure prf
|
|
No _ => left "Values are not equal."
|
|
|
|
||| Verify that a Nat is less than or equal to certain bound.
|
|
export
|
|
lteNat : Monad m => (bound : Nat) -> PropValidator m Nat (flip LTE bound)
|
|
lteNat bound = decide
|
|
(\n => show n <+> " is not lower or equal to " <+> show bound)
|
|
(\n => isLTE n bound)
|
|
|
|
||| Verify that a Nat is greater than or equal to certain bound.
|
|
export
|
|
gteNat : Monad m => (bound : Nat) -> PropValidator m Nat (flip GTE bound)
|
|
gteNat bound = decide
|
|
(\n => show n <+> " is not greater or equal to " <+> show bound)
|
|
(isLTE bound)
|
|
|
|
||| Verify that a Nat is strictly less than a certain bound.
|
|
export
|
|
ltNat : Monad m => (bound : Nat) -> PropValidator m Nat (flip LT bound)
|
|
ltNat bound = decide
|
|
(\n => show n <+> " is not strictly lower than " <+> show bound)
|
|
(\n => isLTE (S n) bound)
|
|
|
|
||| Verify that a Nat is strictly greate than a certain bound.
|
|
export
|
|
gtNat : Monad m => (bound : Nat) -> PropValidator m Nat (flip GT bound)
|
|
gtNat bound = decide
|
|
(\n => show n <+> " is not strictly greater than " <+> show bound)
|
|
(isLTE (S bound))
|