yesod/Yesod/Form.hs
2010-06-30 22:41:47 +03:00

364 lines
11 KiB
Haskell

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-- | Parse forms (and query strings).
module Yesod.Form
( -- * Data types
Form (..)
, Formlet
, FormResult (..)
-- * Unwrapping functions
, runFormGet
, runFormPost
, runFormGet'
, runFormPost'
-- * Create your own formlets
, incr
, input
, check
-- * Error display
, wrapperRow
, sealFormlet
, sealForm
, sealRow
-- * Formable
, Formable (..)
, deriveFormable
, share2
-- * Pre-built formlets
) where
import Text.Hamlet
import Yesod.Request
import Yesod.Handler
import Control.Applicative hiding (optional)
import Data.Time (Day)
import Data.Maybe (fromMaybe)
import "transformers" Control.Monad.IO.Class
import Yesod.Internal
import Control.Monad.Attempt
import Control.Monad ((<=<), liftM, join)
import Data.Monoid (mempty, mappend)
import Control.Monad.Trans.State
import Control.Arrow (first)
import Language.Haskell.TH.Syntax
import Database.Persist.Base (PersistField, EntityDef (..))
import Data.Char (isAlphaNum, toUpper, isUpper)
import Data.Maybe (fromMaybe, isJust)
import Web.Routes.Quasi (SinglePiece)
import Data.Int (Int64)
import qualified Data.ByteString.Lazy.UTF8
noParamNameError :: String
noParamNameError = "No param name (miscalling of Yesod.Form library)"
data FormResult a = FormMissing
| FormFailure [String]
| FormSuccess a
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
newtype Form sub y a = Form
{ deform :: Env -> Incr (GHandler sub y) (FormResult a, Hamlet (Routes y))
}
type Formlet sub y a = Maybe a -> Form sub y a
type Env = [(String, String)]
instance Functor (Form sub url) where
fmap f (Form g) = Form $ \env -> liftM (first $ fmap f) (g env)
instance Applicative (Form sub url) where
pure a = Form $ const $ return (pure a, mempty)
(Form f) <*> (Form g) = Form $ \env -> do
(f1, f2) <- f env
(g1, g2) <- g env
return (f1 <*> g1, f2 `mappend` g2)
runFormGeneric :: Env
-> Form sub y a
-> GHandler sub y (FormResult a, Hamlet (Routes y))
runFormGeneric env f = evalStateT (deform f env) 1
-- | Run a form against POST parameters.
runFormPost :: Form sub y a
-> GHandler sub y (FormResult a, Hamlet (Routes y))
runFormPost f = do
rr <- getRequest
(pp, _) <- liftIO $ reqRequestBody rr
runFormGeneric pp f
-- | Run a form against POST parameters, disregarding the resulting HTML and
-- returning an error response on invalid input.
runFormPost' :: Form sub y a -> GHandler sub y a
runFormPost' = helper <=< runFormPost
-- | Run a form against GET parameters, disregarding the resulting HTML and
-- returning an error response on invalid input.
runFormGet' :: Form sub y a -> GHandler sub y a
runFormGet' = helper <=< runFormGet
helper :: (FormResult a, Hamlet (Routes y)) -> GHandler sub y a
helper (FormSuccess a, _) = return a
helper (FormFailure e, _) = invalidArgs e
helper (FormMissing, _) = invalidArgs ["No input found"]
-- | Run a form against GET parameters.
runFormGet :: Form sub y a
-> GHandler sub y (FormResult a, Hamlet (Routes y))
runFormGet f = do
gets <- reqGetParams `fmap` getRequest
runFormGeneric gets f
type Incr = StateT Int
incr :: Monad m => Incr m Int
incr = do
i <- get
let i' = i + 1
put i'
return i'
input :: (String -> String -> Hamlet (Routes y))
-> Maybe String
-> Form sub y String
input mkXml val = Form $ \env -> do
i <- incr
let i' = show i
let param = lookup i' env
let xml = mkXml i' $ fromMaybe (fromMaybe "" val) param
return (maybe FormMissing FormSuccess param, xml)
check :: Form sub url a -> (a -> Either [String] b) -> Form sub url b
check (Form form) f = Form $ \env -> liftM (first go) (form env)
where
go FormMissing = FormMissing
go (FormFailure x) = FormFailure x
go (FormSuccess a) =
case f a of
Left errs -> FormFailure errs
Right b -> FormSuccess b
wrapperRow :: String -> [String] -> Hamlet url -> Hamlet url
wrapperRow label errs control = [$hamlet|
%tr
%th $string.label$
%td ^control^
$if not.null.errs
%td.errors
%ul
$forall errs err
%li $string.err$
|]
sealRow :: Formable b => String -> (a -> b) -> Maybe a -> Form sub master b
sealRow label getVal val =
sealForm (wrapperRow label) $ formable $ fmap getVal val
sealForm :: ([String] -> Hamlet (Routes y) -> Hamlet (Routes y))
-> Form sub y a -> Form sub y a
sealForm wrapper (Form form) = Form $ \env -> liftM go (form env)
where
go (res, xml) = (res, wrapper (toList res) xml)
toList (FormFailure errs) = errs
toList _ = []
sealFormlet :: ([String] -> Hamlet (Routes y) -> Hamlet (Routes y))
-> Formlet sub y a -> Formlet sub y a
sealFormlet wrapper formlet initVal = sealForm wrapper $ formlet initVal
class Formable a where
formable :: Formlet sub master a
--------------- Formable instances
instance Formable String where
formable x = input go x `check` notEmpty
where
go name val = [$hamlet|
%input!type=text!name=$string.name$!value=$string.val$
|]
notEmpty s
| null s = Left ["Value required"]
| otherwise = Right s
instance Formable (Maybe String) where
formable x = input go (join x) `check` isEmpty
where
go name val = [$hamlet|
%input!type=text!name=$string.name$!value=$string.val$
|]
isEmpty s
| null s = Right Nothing
| otherwise = Right $ Just s
instance Formable (Html ()) where
formable = fmap preEscapedString
. input go
. fmap (Data.ByteString.Lazy.UTF8.toString . renderHtml)
where
go name val = [$hamlet|%textarea!name=$string.name$ $string.val$|]
instance Formable Day where
formable x = input go (fmap show x) `check` asDay
where
go name val = [$hamlet|
%input!type=date!name=$string.name$!value=$string.val$
|]
asDay s = case reads s of
(y, _):_ -> Right y
[] -> Left ["Invalid day"]
instance Formable Int64 where
formable x = input go (fmap show x) `check` asInt
where
go name val = [$hamlet|
%input!type=number!name=$string.name$!value=$string.val$
|]
asInt s = case reads s of
(y, _):_ -> Right y
[] -> Left ["Invalid integer"]
instance Formable Double where
formable x = input go (fmap numstring x) `check` asDouble
where
go name val = [$hamlet|
%input!type=number!name=$string.name$!value=$string.val$
|]
asDouble s = case reads s of
(y, _):_ -> Right y
[] -> Left ["Invalid double"]
numstring d =
let s = show d
in case reverse s of
'0':'.':y -> reverse y
_ -> s
instance Formable (Maybe Day) where
formable x = input go (fmap show $ join x) `check` asDay
where
go name val = [$hamlet|
%input!type=date!name=$string.name$!value=$string.val$
|]
asDay "" = Right Nothing
asDay s = case reads s of
(y, _):_ -> Right $ Just y
[] -> Left ["Invalid day"]
instance Formable (Maybe Int) where
formable x = input go (fmap show $ join x) `check` asInt
where
go name val = [$hamlet|
%input!type=number!name=$string.name$!value=$string.val$
|]
asInt "" = Right Nothing
asInt s = case reads s of
(y, _):_ -> Right $ Just y
[] -> Left ["Invalid integer"]
instance Formable (Maybe Int64) where
formable x = input go (fmap show $ join x) `check` asInt
where
go name val = [$hamlet|
%input!type=number!name=$string.name$!value=$string.val$
|]
asInt "" = Right Nothing
asInt s = case reads s of
(y, _):_ -> Right $ Just y
[] -> Left ["Invalid integer"]
instance Formable Bool where
formable x = Form $ \env -> do
i <- incr
let i' = show i
let param = lookup i' env
let def = if null env then fromMaybe False x else isJust param
return (FormSuccess $ isJust param, go i' def)
where
go name val = [$hamlet|
%input!type=checkbox!name=$string.name$!:val:checked
|]
instance Formable Int where
formable x = input go (fmap show x) `check` asInt
where
go name val = [$hamlet|
%input!type=number!name=$string.name$!value=$string.val$
|]
asInt s = case reads s of
(y, _):_ -> Right y
[] -> Left ["Invalid integer"]
newtype Slug = Slug { unSlug :: String }
deriving (Read, Eq, Show, SinglePiece, PersistField)
instance Formable Slug where
formable x = input go (fmap unSlug x) `check` asSlug
where
go name val = [$hamlet|
%input!type=text!name=$string.name$!value=$string.val$
|]
asSlug [] = Left ["Slug must be non-empty"]
asSlug x'
| all (\c -> c `elem` "-_" || isAlphaNum c) x' =
Right $ Slug x'
| otherwise = Left ["Slug must be alphanumeric, - and _"]
share2 :: Monad m => (a -> m [b]) -> (a -> m [b]) -> a -> m [b]
share2 f g a = do
f' <- f a
g' <- g a
return $ f' ++ g'
deriveFormable :: [EntityDef] -> Q [Dec]
deriveFormable = mapM derive
where
derive :: EntityDef -> Q Dec
derive t = do
let fst3 (x, _, _) = x
let cols = map (toLabel . fst3) $ entityColumns t
ap <- [|(<*>)|]
just <- [|pure|]
nothing <- [|Nothing|]
let just' = just `AppE` ConE (mkName $ entityName t)
let c1 = Clause [ ConP (mkName "Nothing") []
]
(NormalB $ go ap just' $ zip cols $ map (const nothing) cols)
[]
xs <- mapM (const $ newName "x") cols
let xs' = map (AppE just . VarE) xs
let c2 = Clause [ ConP (mkName "Just") [ConP (mkName $ entityName t)
$ map VarP xs]]
(NormalB $ go ap just' $ zip cols xs')
[]
return $ InstanceD [] (ConT ''Formable
`AppT` ConT (mkName $ entityName t))
[FunD (mkName "formable") [c1, c2]]
go ap just' = foldl (ap' ap) just' . map go'
go' (label, ex) =
VarE (mkName "sealForm") `AppE`
(VarE (mkName "wrapperRow") `AppE` LitE (StringL label)) `AppE`
(VarE (mkName "formable") `AppE` ex)
ap' ap x y = InfixE (Just x) ap (Just y)
toLabel :: String -> String
toLabel "" = ""
toLabel (x:rest) = toUpper x : go rest
where
go "" = ""
go (c:cs)
| isUpper c = ' ' : c : go cs
| otherwise = c : go cs