{-# LANGUAGE FlexibleInstances, GeneralizedNewtypeDeriving #-} {- | This module defines the 'Form' type, its instances, core manipulation functions, and a bunch of helper utilities. -} module Text.Reform.Core where import Control.Applicative (Applicative(pure, (<*>))) import Control.Applicative.Indexed (IndexedApplicative(ipure, (<<*>>)), IndexedFunctor (imap)) import Control.Arrow (first, second) import Control.Monad.Reader (MonadReader(ask), ReaderT, runReaderT) import Control.Monad.State (MonadState(get,put), StateT, evalStateT) import Control.Monad.Trans (lift) import Data.Monoid (Monoid(mempty, mappend)) import Text.Reform.Result (FormId(..), FormRange(..), Result(..), unitRange, zeroId) ------------------------------------------------------------------------------ -- * Proved ------------------------------------------------------------------------------ -- | Proved records a value, the location that value came from, and something that was proved about the value. data Proved proofs a = Proved { proofs :: proofs , pos :: FormRange , unProved :: a } instance Functor (Proved ()) where fmap f (Proved () pos a) = Proved () pos (f a) -- | Utility Function: trivially prove nothing about () unitProved :: FormId -> Proved () () unitProved formId = Proved { proofs = () , pos = unitRange formId , unProved = () } ------------------------------------------------------------------------------ -- * FormState ------------------------------------------------------------------------------ -- | inner state used by 'Form'. type FormState m input = ReaderT (Environment m input) (StateT FormRange m) -- | used to represent whether a value was found in the form -- submission data, missing from the form submission data, or expected -- that the default value should be used data Value a = Default | Missing | Found a -- | Utility function: Get the current input -- getFormInput :: Monad m => FormState m input (Value input) getFormInput = getFormId >>= getFormInput' -- | Utility function: Gets the input of an arbitrary 'FormId'. -- getFormInput' :: Monad m => FormId -> FormState m input (Value input) getFormInput' id' = do env <- ask case env of NoEnvironment -> return Default Environment f -> lift $ lift $ f id' -- | Utility function: Get the current range -- getFormRange :: Monad m => FormState m i FormRange getFormRange = get -- | The environment is where you get the actual input per form. -- -- The 'NoEnvironment' constructor is typically used when generating a -- view for a GET request, where no data has yet been submitted. This -- will cause the input elements to use their supplied default values. -- -- Note that 'NoEnviroment' is different than supplying an empty environment. data Environment m input = Environment (FormId -> m (Value input)) | NoEnvironment -- | Not quite sure when this is useful and so hard to say if the rules for combining things with Missing/Default are correct instance (Monoid input, Monad m) => Monoid (Environment m input) where mempty = NoEnvironment NoEnvironment `mappend` x = x x `mappend` NoEnvironment = x (Environment env1) `mappend` (Environment env2) = Environment $ \id' -> do r1 <- (env1 id') r2 <- (env2 id') case (r1, r2) of (Missing, Missing) -> return Missing (Default, Missing) -> return Default (Missing, Default) -> return Default (Found x, Found y) -> return $ Found (x `mappend` y) (Found x, _ ) -> return $ Found x (_ , Found y) -> return $ Found y -- | Utility function: returns the current 'FormId'. This will only make sense -- if the form is not composed -- getFormId :: Monad m => FormState m i FormId getFormId = do FormRange x _ <- get return x -- | Utility function: increment the current 'FormId'. incFormId :: Monad m => FormState m i () incFormId = do FormRange _ endF1 <- get put $ unitRange endF1 -- | A view represents a visual representation of a form. It is composed of a -- function which takes a list of all errors and then produces a new view -- newtype View error v = View { unView :: [(FormRange, error)] -> v } deriving (Monoid) instance Functor (View e) where fmap f (View g) = View $ f . g ------------------------------------------------------------------------------ -- * Form ------------------------------------------------------------------------------ -- | a 'Form' contains a 'View' combined with a validation function -- which will attempt to extract a value from submitted form data. -- -- It is highly parameterized, allowing it work in a wide variety of -- different configurations. You will likely want to make a type alias -- that is specific to your application to make type signatures more -- manageable. -- -- [@m@] A monad which can be used by the validator -- -- [@input@] A framework specific type for representing the raw key/value pairs from the form data -- -- [@error@] A application specific type for error messages -- -- [@view@] The type of data being generated for the view (HSP, Blaze Html, Heist, etc) -- -- [@proof@] A type which names what has been proved about the return value. @()@ means nothing has been proved. -- -- [@a@] Value return by form when it is successfully decoded, validated, etc. -- -- -- This type is very similar to the 'Form' type from -- @digestive-functors <= 0.2@. If @proof@ is @()@, then 'Form' is an -- applicative functor and can be used almost exactly like -- @digestive-functors <= 0.2@. newtype Form m input error view proof a = Form { unForm :: FormState m input (View error view, m (Result error (Proved proof a))) } instance (Monad m) => IndexedFunctor (Form m input view error) where imap f g (Form frm) = Form $ do (view, mval) <- frm val <- lift $ lift $ mval case val of (Ok (Proved p pos a)) -> return (view, return $ Ok (Proved (f p) pos (g a))) (Error errs) -> return (view, return $ Error errs) instance (Monoid view, Monad m) => IndexedApplicative (Form m input error view) where ipure p a = Form $ do i <- getFormId return (mempty, return $ Ok (Proved p (unitRange i) a)) (Form frmF) <<*>> (Form frmA) = Form $ do ((view1, mfok), (view2, maok)) <- bracketState $ do res1 <- frmF incFormId res2 <- frmA return (res1, res2) fok <- lift $ lift $ mfok aok <- lift $ lift $ maok case (fok, aok) of (Error errs1, Error errs2) -> return (view1 `mappend` view2, return $ Error $ errs1 ++ errs2) (Error errs1, _) -> return (view1 `mappend` view2, return $ Error $ errs1) (_ , Error errs2) -> return (view1 `mappend` view2, return $ Error $ errs2) (Ok (Proved p (FormRange x _) f), Ok (Proved q (FormRange _ y) a)) -> return (view1 `mappend` view2, return $ Ok $ Proved { proofs = p q , pos = FormRange x y , unProved = f a }) bracketState :: Monad m => FormState m input a -> FormState m input a bracketState k = do FormRange startF1 _ <- get res <- k FormRange _ endF2 <- get put $ FormRange startF1 endF2 return res instance (Functor m) => Functor (Form m input error view ()) where fmap f form = Form $ fmap (second (fmap (fmap (fmap f)))) (unForm form) instance (Functor m, Monoid view, Monad m) => Applicative (Form m input error view ()) where pure a = Form $ do i <- getFormId return (View $ const $ mempty, return $ Ok $ Proved { proofs = () , pos = FormRange i i , unProved = a }) -- this coud be defined in terms of <<*>> if we just changed the proof of frmF to (() -> ()) (Form frmF) <*> (Form frmA) = Form $ do ((view1, mfok), (view2, maok)) <- bracketState $ do res1 <- frmF incFormId res2 <- frmA return (res1, res2) fok <- lift $ lift $ mfok aok <- lift $ lift $ maok case (fok, aok) of (Error errs1, Error errs2) -> return (view1 `mappend` view2, return $ Error $ errs1 ++ errs2) (Error errs1, _) -> return (view1 `mappend` view2, return $ Error $ errs1) (_ , Error errs2) -> return (view1 `mappend` view2, return $ Error $ errs2) (Ok (Proved p (FormRange x _) f), Ok (Proved q (FormRange _ y) a)) -> return (view1 `mappend` view2, return $ Ok $ Proved { proofs = () , pos = FormRange x y , unProved = f a }) -- ** Ways to evaluate a Form -- | Run a form -- runForm :: (Monad m) => Environment m input -> String -> Form m input error view proof a -> m (View error view, m (Result error (Proved proof a))) runForm env prefix form = evalStateT (runReaderT (unForm form) env) (unitRange (zeroId prefix)) -- | Run a form -- runForm' :: (Monad m) => Environment m input -> String -> Form m input error view proof a -> m (view , Maybe a) runForm' env prefix form = do (view', mresult) <- runForm env prefix form result <- mresult return $ case result of Error e -> (unView view' e , Nothing) Ok x -> (unView view' [], Just (unProved x)) -- | Just evaluate the form to a view. This usually maps to a GET request in the -- browser. -- viewForm :: (Monad m) => String -- ^ form prefix -> Form m input error view proof a -- ^ form to view -> m view viewForm prefix form = do (v, _) <- runForm NoEnvironment prefix form return (unView v []) -- | Evaluate a form -- -- Returns: -- -- [@Left view@] on failure. The @view@ will have already been applied to the errors. -- -- [@Right a@] on success. -- eitherForm :: (Monad m) => Environment m input -- ^ Input environment -> String -- ^ Identifier for the form -> Form m input error view proof a -- ^ Form to run -> m (Either view a) -- ^ Result eitherForm env id' form = do (view', mresult) <- runForm env id' form result <- mresult return $ case result of Error e -> Left $ unView view' e Ok x -> Right (unProved x) -- | create a 'Form' from some @view@. -- -- This is typically used to turn markup like @\@ into a 'Form'. view :: (Monad m) => view -- ^ View to insert -> Form m input error view () () -- ^ Resulting form view view' = Form $ do i <- getFormId return ( View (const view') , return (Ok (Proved { proofs = () , pos = FormRange i i , unProved = () }))) -- | Append a unit form to the left. This is useful for adding labels or error -- fields. -- -- The 'Forms' on the left and right hand side will share the same -- 'FormId'. This is useful for elements like @\