{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-} {-| WebConts allow simple continuation based behavior in Happstack applications through the use of cookies and an IntMap on the server mapping cookie names to continuations. It is based on the paste . For the arc challenge: > arc = do > name <- doform' frm > display $ samelink "click here" > display $ "you said" ++ name A more complicated example, creating a User datatype with a different page for each field: > data User = User {age:: Int, name::String, dead::Bool} deriving (Show) > makeUser = liftM3 User age name dead >>= (display . show) where > age = doform' $ label "Age: " *> frm > name = doform' $ label "Name: " *> frm > dead = doform' $ label "Dead?: " *> frm To run the continuations in Happstack, use 'runStateless' or 'runWithState', giving each continuation in a list as a parameter, depending on whether you want MACID support enabled: > runStateless nullConf [arc, makeUser] [] -} module WebCont ( doform, doform', runWithState, runStateless, frm, display, samelink, button, link, WebCont(..), Result(..), ContForm, DefaultForm, nullConf, ) where import Happstack.Server.SimpleHTTP hiding (method) import Happstack.State hiding (dup) import Happstack.Util.Cron import Control.Monad import Control.Monad.Identity (Identity (..)) import Control.Monad.Trans import Control.Monad.Reader import Control.Concurrent import Control.Exception hiding (catch) import Control.Concatenative import Control.Applicative import Control.Applicative.Error (Failing (..)) import Text.Formlets import Text.XHtml.Strict.Formlets hiding (label) import Text.XHtml.Strict hiding (input, checkbox, button) import qualified Data.IntMap as Map import Data.IntMap (IntMap) import Data.Monoid import Data.ByteString.Lazy.UTF8 (toString) import qualified Happstack.Server.SimpleHTTP as H (ContentType(..)) import qualified Text.Formlets as F (ContentType(..)) data Result r a = Done a | Via r (WebCont r a) deriving Show -- | A WebCont is a function from 'Env' to a value of a, -- displaying a response of r. Both r and a must be -- Html for runnable continuations. newtype WebCont r a = WebCont {runWeb :: Env -> Result r a} deriving Show runWeb' :: Env -> (MVar Int, MVar ContTable) -> WebCont Html () -> ServerPartT IO Response runWeb' e t w = case runWeb w e of Done _ -> expireCookie "cont" >> mzero Via r w' -> serializeCont t w' >> ok (toResponse r) type ContForm a = XHtmlForm Identity a instance Monad (WebCont r) where return = WebCont . const . Done w >>= f = WebCont $ \e -> case (runWeb w e) of Done a -> runWeb (f a) e Via r w' -> Via r (w' >>= f) instance (Monoid a) => Monoid (WebCont a a) where (WebCont f) `mappend` (WebCont g) = WebCont $ f &&. g >>@ c where (Done h1) `c` (Via h2 a) = Via (h1 `mappend` h2) a (Via h1 a) `c` (Done h2) = Via (h1 `mappend` h2) a (Via v a) `c` (Via t b) = Via (v `mappend` t) (a `mappend` b) mempty = WebCont . const . Done $ mempty instance (Monoid r, Monoid a) => Monoid (WebCont r a) where (WebCont f) `mappend` (WebCont g) = WebCont $ f &&. g >>@ c where (Done _) `c` x@(Via _ _) = x x@(Via _ _) `c` (Done _) = x (Via v a) `c` (Via t b) = Via (v `mappend` t) (a `mappend` b) mempty = WebCont . const . Done $ mempty -- | Lift a form into the WebCont monad using a function --- to modify its html, and a function to display errors doform :: (Html -> r) -> ([String] -> Html -> r) -> ContForm a -> WebCont r a doform l err f = WebCont . const $ Via (l (createForm [] f)) succPage where succPage = WebCont $ dup >>. first parse >>@ formResult parse env = x where (Identity x, _, _) = runFormState env f formResult (Success a) _ = Done a formResult (Failure faults) e = Via (err faults (createForm e f)) succPage makeForm frm = form ! [method "POST"] << (frm +++ submit "submit" "submit") createForm :: Env -> ContForm a -> Html createForm env frm = form ! [method "POST"] << (xml +++ submit "submit" "Submit") where (extractor, xml, endState) = runFormState env frm -- | Lift a form into the WebCont monad without embellishment, displaying errors inline doform' :: ContForm a -> WebCont Html a doform' = doform id $ curry (unlines **. id >>@ (+++)) -- | Display a page, ignoring the value display :: HTML a => a -> WebCont Html () display h = WebCont $ const $ Via (toHtml h) (return ()) -- | Button with an associated value button :: String -> a -> WebCont Html a button s a = display (form ! [method "POST"] << (submit "submit" s)) >> return a -- | Link with an associated value link :: HTML l => l -> a -> WebCont Html a link l a = display (samelink l) >> return a -- | Links to the same page, leading to the next step in a continuation samelink :: HTML a => a -> HotLink samelink a = hotlink "" (toHtml a) type ContTable = IntMap (WebCont Html ()) runConts :: (MVar Int, MVar ContTable) -> ServerPartT IO Response runConts t = withDataFn (lookCookieValue "cont") $ \c-> do w <- liftIO $ deserializeCont t c case w of Just w' -> do d <- lookInputPairs clearCont t c runWeb' (maybe [] id d) t w' Nothing -> mzero lookInputPairs :: ServerPartT IO (Maybe Env) lookInputPairs = getDataFn $ asks fst >>= (return . map f) where f (k,Input v Nothing _) = (k,Left (toString v)) f (k,Input v (Just f) (H.ContentType t s p)) = (k,Right (File v f (F.ContentType t s p))) deserializeCont :: (MVar Int, MVar ContTable) -> String -> IO (Maybe (WebCont Html ())) deserializeCont (_,t) s = do e <- readMVar t let p = Map.lookup (read s :: Int) e return p clearCont :: (MVar Int, MVar ContTable) -> String -> ServerPartT IO () clearCont (_,t) s = liftIO (takeMVar t >>= (putMVar t . Map.delete (read s))) serializeCont :: (MVar Int, MVar ContTable) -> (WebCont Html ()) -> ServerPartT IO () serializeCont (i,t) f = do k <- liftIO $ takeMVar i liftIO $ putMVar i (k+1) liftIO $ takeMVar t >>= (putMVar t . Map.insert k f) addCookie 600 (mkCookie "cont" (show k)) -- | Entry point for the continuation server: sets up continuation table, MACID state, -- and a chron job to add a checkpoint daily runWithState :: (Methods a, Component a) => Conf -> Proxy a -> [WebCont Html ()] -> [ServerPartT IO Response] -> IO () runWithState c p conts controller = bracket (startSystemState p) (biM_ createCheckpoint shutdownSystem) $ \control-> do withThread (runStateless c conts controller) $ do withThread (cron 86400 (createCheckpoint control)) $ waitForTermination where withThread init action = bracket (forkIO $ init) (killThread) (const action) -- | Entry point for the continuation server: starts the server without MACID support runStateless :: Conf -> [WebCont Html ()] -> [ServerPartT IO Response] -> IO () runStateless c conts controller = do e <- newMVar Map.empty counter <- newMVar (0 :: Int) let env = (counter, e) simpleHTTP c (runConts env `mplus` msum (map (runWeb' [] env) conts) `mplus` msum controller) -- | Defines a normal form style to use for a given type class DefaultForm i where frm :: Form Html Identity i instance DefaultForm String where frm = input Nothing instance DefaultForm Integer where frm = inputInteger Nothing instance DefaultForm Int where frm = fromInteger <$> (inputInteger Nothing) instance DefaultForm Bool where frm = checkbox Nothing instance (DefaultForm a, DefaultForm b) => DefaultForm (a,b) where frm = (,) <$> frm <*> frm instance HTML Integer where toHtml = toHtml . show instance Applicative Identity where pure = return; (<*>) = ap;