yesod/Yesod/Form/Core.hs
2010-10-26 15:57:04 +02:00

370 lines
12 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
-- | Users of the forms library should not need to use this module in general.
-- It is intended only for writing custom forms and form fields.
module Yesod.Form.Core
( FormResult (..)
, GForm (..)
, newFormIdent
, deeperFormIdent
, shallowerFormIdent
, Env
, FileEnv
, Enctype (..)
, Ints (..)
, requiredFieldHelper
, optionalFieldHelper
, fieldsToInput
, mapFormXml
, checkForm
, checkField
, askParams
, askFiles
, liftForm
, IsForm (..)
, RunForm (..)
, GFormMonad
-- * Data types
, FieldInfo (..)
, FormFieldSettings (..)
, FieldProfile (..)
-- * Type synonyms
, Form
, Formlet
, FormField
, FormletField
, FormInput
) where
import Control.Monad.Trans.State
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Writer
import Control.Monad.Trans.Class (lift)
import Yesod.Handler
import Yesod.Widget
import Data.Monoid (Monoid (..))
import Control.Applicative
import Yesod.Request
import Control.Monad (liftM)
import Text.Hamlet
import Data.String
import Control.Monad (join)
-- | A form can produce three different results: there was no data available,
-- the data was invalid, or there was a successful parse.
--
-- The 'Applicative' instance will concatenate the failure messages in two
-- 'FormResult's.
data FormResult a = FormMissing
| FormFailure [String]
| FormSuccess a
deriving Show
instance Functor FormResult where
fmap _ FormMissing = FormMissing
fmap _ (FormFailure errs) = FormFailure errs
fmap f (FormSuccess a) = FormSuccess $ f a
instance Applicative FormResult where
pure = FormSuccess
(FormSuccess f) <*> (FormSuccess g) = FormSuccess $ f g
(FormFailure x) <*> (FormFailure y) = FormFailure $ x ++ y
(FormFailure x) <*> _ = FormFailure x
_ <*> (FormFailure y) = FormFailure y
_ <*> _ = FormMissing
instance Monoid m => Monoid (FormResult m) where
mempty = pure mempty
mappend x y = mappend <$> x <*> y
-- | The encoding type required by a form. The 'Show' instance produces values
-- that can be inserted directly into HTML.
data Enctype = UrlEncoded | Multipart
deriving (Eq, Enum, Bounded)
instance ToHtml Enctype where
toHtml UrlEncoded = unsafeByteString "application/x-www-form-urlencoded"
toHtml Multipart = unsafeByteString "multipart/form-data"
instance Monoid Enctype where
mempty = UrlEncoded
mappend UrlEncoded UrlEncoded = UrlEncoded
mappend _ _ = Multipart
data Ints = IntCons Int Ints | IntSingle Int
instance Show Ints where
show (IntSingle i) = show i
show (IntCons i is) = show i ++ '-' : show is
incrInts :: Ints -> Ints
incrInts (IntSingle i) = IntSingle $ i + 1
incrInts (IntCons i is) = (i + 1) `IntCons` is
-- | A generic form, allowing you to specifying the subsite datatype, master
-- site datatype, a datatype for the form XML and the return type.
newtype GForm s m xml a = GForm
{ deform :: FormInner s m (FormResult a, xml, Enctype)
}
type GFormMonad s m a = WriterT Enctype (FormInner s m) a
type FormInner s m =
StateT Ints (
ReaderT Env (
ReaderT FileEnv (
GHandler s m
)))
type Env = [(String, String)]
type FileEnv = [(String, FileInfo)]
-- | Get a unique identifier.
newFormIdent :: Monad m => StateT Ints m String
newFormIdent = do
i <- get
let i' = incrInts i
put i'
return $ 'f' : show i'
deeperFormIdent :: Monad m => StateT Ints m ()
deeperFormIdent = do
i <- get
let i' = 1 `IntCons` incrInts i
put i'
shallowerFormIdent :: Monad m => StateT Ints m ()
shallowerFormIdent = do
IntCons _ i <- get
put i
instance Monoid xml => Functor (GForm sub url xml) where
fmap f (GForm g) =
GForm $ liftM (first3 $ fmap f) g
where
first3 f' (x, y, z) = (f' x, y, z)
instance Monoid xml => Applicative (GForm sub url xml) where
pure a = GForm $ return (pure a, mempty, mempty)
(GForm f) <*> (GForm g) = GForm $ do
(f1, f2, f3) <- f
(g1, g2, g3) <- g
return (f1 <*> g1, f2 `mappend` g2, f3 `mappend` g3)
-- | Create a required field (ie, one that cannot be blank) from a
-- 'FieldProfile'.
requiredFieldHelper
:: IsForm f
=> FieldProfile (FormSub f) (FormMaster f) (FormType f)
-> FormFieldSettings
-> Maybe (FormType f)
-> f
requiredFieldHelper (FieldProfile parse render mkWidget) ffs orig = toForm $ do
env <- lift ask
let (FormFieldSettings label tooltip theId' name') = ffs
name <- maybe newFormIdent return name'
theId <- maybe newFormIdent return theId'
let (res, val) =
if null env
then (FormMissing, maybe "" render orig)
else case lookup name env of
Nothing -> (FormMissing, "")
Just "" -> (FormFailure ["Value is required"], "")
Just x ->
case parse x of
Left e -> (FormFailure [e], x)
Right y -> (FormSuccess y, x)
let fi = FieldInfo
{ fiLabel = string label
, fiTooltip = tooltip
, fiIdent = theId
, fiInput = mkWidget theId name val True
, fiErrors = case res of
FormFailure [x] -> Just $ string x
_ -> Nothing
, fiRequired = True
}
let res' = case res of
FormFailure [e] -> FormFailure [label ++ ": " ++ e]
_ -> res
return (res', fi, UrlEncoded)
class IsForm f where
type FormSub f
type FormMaster f
type FormType f
toForm :: FormInner
(FormSub f)
(FormMaster f)
(FormResult (FormType f),
FieldInfo (FormSub f) (FormMaster f),
Enctype) -> f
instance IsForm (FormField s m a) where
type FormSub (FormField s m a) = s
type FormMaster (FormField s m a) = m
type FormType (FormField s m a) = a
toForm x = GForm $ do
(a, b, c) <- x
return (a, [b], c)
instance IsForm (GFormMonad s m (FormResult a, FieldInfo s m)) where
type FormSub (GFormMonad s m (FormResult a, FieldInfo s m)) = s
type FormMaster (GFormMonad s m (FormResult a, FieldInfo s m)) = m
type FormType (GFormMonad s m (FormResult a, FieldInfo s m)) = a
toForm x = do
(res, fi, enctype) <- lift x
tell enctype
return (res, fi)
class RunForm f where
type RunFormSub f
type RunFormMaster f
type RunFormType f
runFormGeneric :: Env -> FileEnv -> f
-> GHandler (RunFormSub f)
(RunFormMaster f)
(RunFormType f)
instance RunForm (GForm s m xml a) where
type RunFormSub (GForm s m xml a) = s
type RunFormMaster (GForm s m xml a) = m
type RunFormType (GForm s m xml a) =
(FormResult a, xml, Enctype)
runFormGeneric env fe (GForm f) =
runReaderT (runReaderT (evalStateT f $ IntSingle 1) env) fe
instance RunForm (GFormMonad s m a) where
type RunFormSub (GFormMonad s m a) = s
type RunFormMaster (GFormMonad s m a) = m
type RunFormType (GFormMonad s m a) = (a, Enctype)
runFormGeneric e fe f =
runReaderT (runReaderT (evalStateT (runWriterT f) $ IntSingle 1) e) fe
-- | Create an optional field (ie, one that can be blank) from a
-- 'FieldProfile'.
optionalFieldHelper
:: (IsForm f, Maybe b ~ FormType f)
=> FieldProfile (FormSub f) (FormMaster f) b
-> FormFieldSettings
-> Maybe (Maybe b)
-> f
optionalFieldHelper (FieldProfile parse render mkWidget) ffs orig' = toForm $ do
env <- lift ask
let (FormFieldSettings label tooltip theId' name') = ffs
let orig = join orig'
name <- maybe newFormIdent return name'
theId <- maybe newFormIdent return theId'
let (res, val) =
if null env
then (FormSuccess Nothing, maybe "" render orig)
else case lookup name env of
Nothing -> (FormSuccess Nothing, "")
Just "" -> (FormSuccess Nothing, "")
Just x ->
case parse x of
Left e -> (FormFailure [e], x)
Right y -> (FormSuccess $ Just y, x)
let fi = FieldInfo
{ fiLabel = string label
, fiTooltip = tooltip
, fiIdent = theId
, fiInput = mkWidget theId name val False
, fiErrors = case res of
FormFailure x -> Just $ string $ unlines x
_ -> Nothing
, fiRequired = False
}
let res' = case res of
FormFailure [e] -> FormFailure [label ++ ": " ++ e]
_ -> res
return (res', fi, UrlEncoded)
fieldsToInput :: [FieldInfo sub y] -> [GWidget sub y ()]
fieldsToInput = map fiInput
-- | Convert the XML in a 'GForm'.
mapFormXml :: (xml1 -> xml2) -> GForm s y xml1 a -> GForm s y xml2 a
mapFormXml f (GForm g) = GForm $ do
(res, xml, enc) <- g
return (res, f xml, enc)
-- | Using this as the intermediate XML representation for fields allows us to
-- write generic field functions and then different functions for producing
-- actual HTML. See, for example, 'fieldsToTable' and 'fieldsToPlain'.
data FieldInfo sub y = FieldInfo
{ fiLabel :: Html
, fiTooltip :: Html
, fiIdent :: String
, fiInput :: GWidget sub y ()
, fiErrors :: Maybe Html
, fiRequired :: Bool
}
data FormFieldSettings = FormFieldSettings
{ ffsLabel :: String
, ffsTooltip :: Html
, ffsId :: Maybe String
, ffsName :: Maybe String
}
instance IsString FormFieldSettings where
fromString s = FormFieldSettings s mempty Nothing Nothing
-- | A generic definition of a form field that can be used for generating both
-- required and optional fields. See 'requiredFieldHelper and
-- 'optionalFieldHelper'.
data FieldProfile sub y a = FieldProfile
{ fpParse :: String -> Either String a
, fpRender :: a -> String
-- | ID, name, value, required
, fpWidget :: String -> String -> String -> Bool -> GWidget sub y ()
}
type Form sub y = GForm sub y (GWidget sub y ())
type Formlet sub y a = Maybe a -> Form sub y a
type FormField sub y = GForm sub y [FieldInfo sub y]
type FormletField sub y a = Maybe a -> FormField sub y a
type FormInput sub y = GForm sub y [GWidget sub y ()]
-- | Add a validation check to a form.
--
-- Note that if there is a validation error, this message will /not/
-- automatically appear on the form; for that, you need to use 'checkField'.
checkForm :: (a -> FormResult b) -> GForm s m x a -> GForm s m x b
checkForm f (GForm form) = GForm $ do
(res, xml, enc) <- form
let res' = case res of
FormSuccess a -> f a
FormFailure e -> FormFailure e
FormMissing -> FormMissing
return (res', xml, enc)
-- | Add a validation check to a 'FormField'.
--
-- Unlike 'checkForm', the validation error will appear in the generated HTML
-- of the form.
checkField :: (a -> Either String b) -> FormField s m a -> FormField s m b
checkField f (GForm form) = GForm $ do
(res, xml, enc) <- form
let (res', merr) =
case res of
FormSuccess a ->
case f a of
Left e -> (FormFailure [e], Just e)
Right x -> (FormSuccess x, Nothing)
FormFailure e -> (FormFailure e, Nothing)
FormMissing -> (FormMissing, Nothing)
let xml' =
case merr of
Nothing -> xml
Just err -> flip map xml $ \fi -> fi
{ fiErrors = Just $
case fiErrors fi of
Nothing -> string err
Just x -> x
}
return (res', xml', enc)
askParams :: Monad m => StateT Ints (ReaderT Env m) Env
askParams = lift ask
askFiles :: Monad m => StateT Ints (ReaderT Env (ReaderT FileEnv m)) FileEnv
askFiles = lift $ lift ask
liftForm :: Monad m => m a -> StateT Ints (ReaderT Env (ReaderT FileEnv m)) a
liftForm = lift . lift . lift