packages feed

happstack-dlg 0.1.1 → 0.1.2

raw patch · 6 files changed

+405/−311 lines, 6 filesdep +applicative-extrasdep +bytestringdep +formletsPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: applicative-extras, bytestring, formlets, xhtml

API changes (from Hackage documentation)

- Happstack.Server.Dialogs: closeDialogManager :: DialogManager m -> IO ()
- Happstack.Server.Dialogs: data DialogManager m
- Happstack.Server.Dialogs: data Dlg m a
- Happstack.Server.Dialogs: dialog :: (MonadIO m) => DialogManager m -> Dlg m () -> ServerPartT m Response
- Happstack.Server.Dialogs: instance (Monad m) => Monad (Dlg m)
- Happstack.Server.Dialogs: instance (MonadIO m) => MonadIO (Dlg m)
- Happstack.Server.Dialogs: instance MonadTrans Dlg
- Happstack.Server.Dialogs: makeDialogManager :: NominalDiffTime -> IO (DialogManager m)
- Happstack.Server.Dialogs: perform :: (Monad m) => ServerPartT m a -> Dlg m a
- Happstack.Server.Dialogs: showPage :: (Monad m) => Page m -> ServerPartT m a -> Dlg m a
- Happstack.Server.Dialogs: type Page m = Int -> ServerPartT m Response
- Happstack.Server.Dialogs.Scaffold: class Scaffolded a
- Happstack.Server.Dialogs.Scaffold: escapeHtml :: String -> String
- Happstack.Server.Dialogs.Scaffold: instance (Scaffolded a, Scaffolded b) => Scaffolded (a, b)
- Happstack.Server.Dialogs.Scaffold: instance (Scaffolded a, Scaffolded b, Scaffolded c) => Scaffolded (a, b, c)
- Happstack.Server.Dialogs.Scaffold: instance Scaffolded ()
- Happstack.Server.Dialogs.Scaffold: instance Scaffolded Bool
- Happstack.Server.Dialogs.Scaffold: instance Scaffolded Int
- Happstack.Server.Dialogs.Scaffold: instance Scaffolded String
- Happstack.Server.Dialogs.Scaffold: parse :: (Scaffolded a, Monad m) => String -> ServerPartT m a
- Happstack.Server.Dialogs.Scaffold: render :: (Scaffolded a) => String -> a -> String
- Happstack.Server.Dialogs.Scaffold: scaffold :: (Scaffolded a, Monad m) => String -> a -> Dlg m a
+ Happstack.Server.Dialogues: closeDialogueManager :: DialogueManager m -> IO ()
+ Happstack.Server.Dialogues: data DialogueManager m
+ Happstack.Server.Dialogues: data Dlg m a
+ Happstack.Server.Dialogues: dialogue :: (MonadIO m) => DialogueManager m -> Dlg m () -> ServerPartT m Response
+ Happstack.Server.Dialogues: instance (Monad m) => Monad (Dlg m)
+ Happstack.Server.Dialogues: instance (MonadIO m) => MonadIO (Dlg m)
+ Happstack.Server.Dialogues: instance MonadTrans Dlg
+ Happstack.Server.Dialogues: makeDialogueManager :: NominalDiffTime -> IO (DialogueManager m)
+ Happstack.Server.Dialogues: perform :: (Monad m) => ServerPartT m a -> Dlg m a
+ Happstack.Server.Dialogues: showPage :: (Monad m) => Page m -> ServerPartT m a -> Dlg m a
+ Happstack.Server.Dialogues: type Page m = String -> ServerPartT m Response
+ Happstack.Server.Dialogues.Formlets: showForm :: (Monad m) => XHtmlForm Identity a -> Dlg m (Maybe a)
+ Happstack.Server.Dialogues.Scaffold: Choice :: [a] -> Int -> Choice a
+ Happstack.Server.Dialogues.Scaffold: chooseFrom :: [a] -> Choice a
+ Happstack.Server.Dialogues.Scaffold: class Scaffolded a
+ Happstack.Server.Dialogues.Scaffold: data Choice a
+ Happstack.Server.Dialogues.Scaffold: escapeHtml :: String -> String
+ Happstack.Server.Dialogues.Scaffold: fromChoice :: Choice a -> a
+ Happstack.Server.Dialogues.Scaffold: instance (Eq a, Show a) => Scaffolded (Choice a)
+ Happstack.Server.Dialogues.Scaffold: instance (Scaffolded a, Scaffolded b) => Scaffolded (a, b)
+ Happstack.Server.Dialogues.Scaffold: instance (Scaffolded a, Scaffolded b, Scaffolded c) => Scaffolded (a, b, c)
+ Happstack.Server.Dialogues.Scaffold: instance (Show a) => Show (Choice a)
+ Happstack.Server.Dialogues.Scaffold: instance Scaffolded ()
+ Happstack.Server.Dialogues.Scaffold: instance Scaffolded Bool
+ Happstack.Server.Dialogues.Scaffold: instance Scaffolded ByteString
+ Happstack.Server.Dialogues.Scaffold: instance Scaffolded Int
+ Happstack.Server.Dialogues.Scaffold: instance Scaffolded String
+ Happstack.Server.Dialogues.Scaffold: parse :: (Scaffolded a, Monad m) => String -> a -> ServerPartT m a
+ Happstack.Server.Dialogues.Scaffold: render :: (Scaffolded a) => String -> a -> String
+ Happstack.Server.Dialogues.Scaffold: scaffold :: (Scaffolded a, Monad m) => String -> a -> Dlg m a
+ Happstack.Server.Dialogues.Scaffold: toChoice :: (Bounded a, Enum a, Eq a) => a -> Choice a

Files

happstack-dlg.cabal view
@@ -1,5 +1,5 @@ Name:                happstack-dlg-Version:             0.1.1+Version:             0.1.2 Cabal-Version:       >= 1.2 Synopsis:            Cross-request user interactions for Happstack Description:         A mechanism for user interactions that extend through@@ -17,7 +17,9 @@ Library     Build-Depends:   base <5, containers, mtl,                      happstack-server, time, random,-                     template-haskell-    Exposed-Modules: Happstack.Server.Dialogs,-                     Happstack.Server.Dialogs.Scaffold+                     template-haskell, formlets, xhtml,+                     applicative-extras, bytestring+    Exposed-Modules: Happstack.Server.Dialogues,+                     Happstack.Server.Dialogues.Scaffold,+                     Happstack.Server.Dialogues.Formlets     Hs-Source-Dirs:  src
− src/Happstack/Server/Dialogs.hs
@@ -1,212 +0,0 @@-module Happstack.Server.Dialogs (-    Dlg,-    Page,-    perform,-    showPage,-    DialogManager,-    makeDialogManager,-    closeDialogManager,-    dialog-    )-    where--import Happstack.Server-import Control.Monad.Trans-import Control.Monad.Reader-import Control.Concurrent-import Control.Concurrent.MVar-import Data.Maybe-import Data.Char-import Data.Time-import System.Random--import Data.Map (Map)-import qualified Data.Map as M--{-|-    A value of a 'Dlg' type represents a dialog between the user and the application,-    after which the application builds a value of type 'a'.  The trivial case is that-    the value is already known.  Alternatively, it may be that there is some action to-    be performed, or else that the user needs to be asked or told something.--}-data Dlg m a = Done a-             | Action (ServerPartT m (Dlg m a))-             | Step (Page m) (Dlg m a)--{-|-    A value of 'Page' type represents a way of rendering a page, given a unique ID-    that should be included in responses in order to reassociate the response with the-    current dialog.--}-type Page m        = Int -> ServerPartT m Response--{--    Dlg is a monad in the obvious way: return represents a dialog that has no-    steps; and (>>=) combines dialogs by doing the first part of the first-    dialog, and then continuing with the rest.--}-instance Monad m => Monad (Dlg m) where-    return         = Done-    Done x   >>= y = y x-    Action x >>= y = Action (x >>= return . (>>= y))-    Step p f >>= y = Step p (f >>= y)--instance MonadTrans Dlg where-    lift = Action . lift . (>>= return . Done)--instance MonadIO m => MonadIO (Dlg m) where-    liftIO = lift . liftIO--{--    Converts a 'ServerPartT' into a Dlg.  This is essentially a mechanism for-    escaping the confines of the dialog mechanism and performing your own-    processing with the request.--}-perform :: Monad m => ServerPartT m a -> Dlg m a-perform x = Action (x >>= return . Done)--{-|-    Converts methods for rendering and parsing the result of a page into a-    'Dlg' step.--}-showPage :: Monad m => Page m -> ServerPartT m a -> Dlg m a -showPage p r = Step p (Action (fmap Done r))--{-|-    A 'DialogSession' represents a single user's active dialogs, which are retained-    for an entire session.  A reaper thread clears up sessions that have not been-    touched for some session timeout, so each session also stores the last time it-    was touched.  In addition, each session is associated with a fixed client address-    and cannot be used from a different client, which avoid hijacking.--}-data DialogSession m = DialogSession {-    client      :: String,-    lastTouched :: MVar UTCTime,-    dialogs     :: MVar (Map Int (Dlg m ()))-    }--{-|-    A 'DialogManager' is responsible for maintaining the state for 'Dlg' sequences-    for all users.  To do this, it keeps for each user a session object encapsulating-    their dialogs, and associates each user with a dialog using cookies.--}-data DialogManager m = DialogManager {-    sessions    :: MVar (Map Int (DialogSession m)),-    open        :: MVar Bool-    }--{-|-    Determine whether a session is still valid or not.--}-goodSession :: NominalDiffTime -> (Int, DialogSession m) -> IO Bool-goodSession timeout (_, DialogSession _ tref _) = do-    st <- readMVar tref-    ct <- getCurrentTime -    return (diffUTCTime ct st <= timeout)--{-|-    Monadic while statement, for convenience.--}-whileM :: Monad m => m Bool -> m a -> m ()-whileM cond action = do-    b <- cond-    if b then action >> whileM cond action else return ()--{-|-    Create a new 'DialogManager' to manage a set of dialogs in the web application.-    This also spawns the session reaper, which cleans up sessions that haven't been-    touched for a given time period.--}-makeDialogManager :: NominalDiffTime -> IO (DialogManager m)-makeDialogManager timeout = do-    oref <- newMVar True-    sref <- newMVar M.empty-    forkIO $ whileM (readMVar oref) $ do-        threadDelay 5000-        sessionMap   <- takeMVar sref-        goodSessions <- filterM (goodSession timeout) (M.assocs sessionMap)-        putMVar sref (M.fromList goodSessions)-    return (DialogManager { sessions = sref, open = oref })--{-|-    Closes a DialogManager, which will cause it to cease accepting any incoming-    requests, and also to terminate the session reaper thread.--}-closeDialogManager :: DialogManager m -> IO ()-closeDialogManager (DialogManager _ oref) = swapMVar oref False >> return ()--{-|-    Given a 'Map' with a key type that can be randomly chosen, returns a key-    that is not currently in the map.--}-uniqueKey :: (Random k, Ord k) => Map k a -> IO k-uniqueKey m = do k <- randomIO-                 if M.member k m then uniqueKey m else return k--{-|-    Adds a 'DialogSession' and associated cookie.  This always sets a new blank-    session, so should only be used when there is no session already.--}-addDialogSession :: MonadIO m => DialogManager m -> ServerPartT m (DialogSession m)-addDialogSession (DialogManager sref _) = do-    rq <- askRq-    (k, session) <- liftIO $ do-        smap <- takeMVar sref-        k    <- uniqueKey smap-        let (c, _) = rqPeer rq-        ct   <- getCurrentTime-        tref <- newMVar ct-        dref <- newMVar M.empty-        let session = DialogSession c tref dref-        putMVar sref (M.insert k session smap)-        return (k, session)-    addCookie (-1) (mkCookie "dlg-sid" (show k))-    return session--{-|-    Ensures that there is a 'DialogSession' for the current user, and returns it.-    Adds a blank one if necessary.  This also updates the last touched time for the-    session, preventing it from being removed by the reaper thread for a while.--}-getDialogSession :: MonadIO m => DialogManager m -> ServerPartT m (DialogSession m)-getDialogSession dmgr@(DialogManager sref oref) = do-    open    <- liftIO $ readMVar oref-    unless open mzero-    rq      <- askRq-    msid    <- getDataFn (lookCookieValue "dlg-sid")-    case msid of-        Nothing  -> addDialogSession dmgr-        Just sid -> do smap <- liftIO $ readMVar sref-                       case M.lookup (read sid) smap of-                            Nothing -> addDialogSession dmgr-                            Just s@(DialogSession { lastTouched = tref }) ->-                                if fst (rqPeer rq) /= client s-                                    then addDialogSession dmgr-                                    else liftIO $ do ct <- getCurrentTime-                                                     swapMVar tref ct-                                                     return s--{-|-    The 'dialog' function builds a 'ServerPartT' that handles a given dialog.  In-    general, it can be combined in normal ways with guards and such, so long as changes-    in the request parameters won't cause it to be missed when future requests are made in-    the same dialog.--}-dialog :: MonadIO m => DialogManager m -> Dlg m () -> ServerPartT m Response-dialog dmgr@(DialogManager sref oref) dlg = do-        (DialogSession _ _ dref) <- getDialogSession dmgr-        cont dref `mplus` this dref dlg-    where cont dref = withDataFn (lookRead "dlg-dlgid") $ \dlgid -> do-            dlgs <- liftIO $ readMVar dref-            rq   <- askRq-            case M.lookup dlgid dlgs of-                Nothing -> mzero-                Just d  -> this dref d-          this dref (Done _)   = mzero-          this dref (Action a) = a >>= this dref-          this dref (Step p f) = do-            dlgs <- liftIO $ takeMVar dref-            k    <- liftIO $ uniqueKey dlgs-            liftIO $ putMVar dref (M.insert k f dlgs)-            p k-
− src/Happstack/Server/Dialogs/Scaffold.hs
@@ -1,95 +0,0 @@-{-# LANGUAGE TemplateHaskell      #-}-{-# LANGUAGE TypeSynonymInstances #-}--module Happstack.Server.Dialogs.Scaffold where--import Happstack.Server-import Happstack.Server.Dialogs-import Language.Haskell.TH--import Data.Char  (ord)-import Data.Maybe (fromMaybe)--escapeHtml :: String -> String-escapeHtml ""                       = ""-escapeHtml ('\"':cs)                = "&#34;" ++ escapeHtml cs-escapeHtml ('\'':cs)                = "&#39;" ++ escapeHtml cs-escapeHtml ('&' :cs)                = "&#38;" ++ escapeHtml cs-escapeHtml ('<' :cs)                = "&#60;" ++ escapeHtml cs-escapeHtml ('>' :cs)                = "&#62;" ++ escapeHtml cs-escapeHtml (c   :cs) | ord c >= 160 = "&#" ++ show (ord c) ++ ";" ++ escapeHtml cs-                     | otherwise    = c : escapeHtml cs--{-|-    The 'scaffold' function builds a user interaction to display and collect information.-    In a finished web application, this should generally be replaced with a better-    mechanism for rendering pages, such as a templating engine, XSLT, or something-    of the sort.--}-scaffold :: (Scaffolded a, Monad m) => String -> a -> Dlg m a-scaffold s v = showPage out inp-    where out n = return $ setHeader "Content-type" "text/html" $ toResponse $-            "<html><body>"-            ++ escapeHtml s ++ "<br><hr>"-            ++ "<form>"-            ++ render "dlg-vals" v-            ++ "<input type=\"hidden\""-            ++ "       name=\"dlg-dlgid\""-            ++ "       value=\"" ++ show n ++ "\">"-            ++ "<input type=\"submit\" value=\"Submit\">"-            ++ "<script type=\"text/javascript\">"-            ++ "    document.forms[0].elements[0].focus();"-            ++ "</script>"-            ++ "</form>"-            ++ "</html></body>"-          inp = parse "dlg-vals"--{-|-    The 'Scaffolded' type class represents data that can be included in a form.-    To make it easy to compose several of these in a page, rendering and parsing-    are parameterized with prefix strings so that they may be made unique across-    a page.--}-class Scaffolded a where-    render :: String -> a -> String-    parse  :: Monad m => String -> ServerPartT m a--instance Scaffolded () where-    render _ () = ""-    parse  _    = return ()--instance Scaffolded String where-    render pfx s = "<input type=\"text\" name=\"" ++ pfx ++ "\" value=\"" ++ {- escapeHtml -} s ++ "\">"-    parse  pfx   = fmap (fromMaybe "") $ getDataFn (look pfx)--instance Scaffolded Bool where-    render pfx v = "<input type=\"checkbox\" name=\"" ++ pfx ++ "\" value=\"True\" "-                   ++ if v then "checked" else "" ++ ">"-    parse  pfx   = fmap (maybe False (const True)) $ getDataFn (look pfx)--instance Scaffolded Int where-    render pfx s = render pfx (show s)-    parse  pfx   = fmap read $ parse pfx--instance (Scaffolded a, Scaffolded b) => Scaffolded (a, b) where-    render pfx (x,y) = render (pfx ++ "_1") x ++ "<br>"-                    ++ render (pfx ++ "_2") y ++ "<br>"-    parse  pfx       = do x <- parse (pfx ++ "_1")-                          y <- parse (pfx ++ "_2")-                          return (x,y)--instance (Scaffolded a, Scaffolded b, Scaffolded c)-        => Scaffolded (a, b, c) where-    render pfx (x,y,z) = render (pfx ++ "_1") x ++ "<br>"-                      ++ render (pfx ++ "_2") y ++ "<br>"-                      ++ render (pfx ++ "_3") z ++ "<br>"-    parse  pfx       = do x <- parse (pfx ++ "_1")-                          y <- parse (pfx ++ "_2")-                          z <- parse (pfx ++ "_3")-                          return (x,y,z)--{--deriveScaffolded :: [Name] -> Q [Dec]-deriveScaffolded = mapM $ \n -> return $-    InstanceD [] (AppT (ConT (mkName "Scaffolded")) (ConT n)) []--}
+ src/Happstack/Server/Dialogues.hs view
@@ -0,0 +1,248 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Happstack.Server.Dialogues (+    Dlg,+    Page,+    perform,+    showPage,+    DialogueManager,+    makeDialogueManager,+    closeDialogueManager,+    dialogue+    )+    where++import Happstack.Server+import Control.Monad.Trans+import Control.Monad.Reader+import Control.Concurrent+import Control.Concurrent.MVar+import Data.Maybe+import Data.Char+import Data.Time+import Data.List+import System.Random++import Data.Map (Map)+import qualified Data.Map as M++{-|+    A value of a 'Dlg' type represents a dialogue between the user and the+    application, after which the application builds a value of type 'a'.  The+    trivial case is that the value is already known.  Alternatively, it may be+    that there is some action to be performed, or else that the user needs to+    be asked or told something.+-}+data Dlg m a = Done a+             | Action (ServerPartT m (Dlg m a))+             | Step (Page m) (Dlg m a)++{-|+    A value of 'Page' type represents a way of rendering a page, given a request URI+    that should be used for subsequent requests in order to reassociate them with the+    current dialogue.+-}+type Page m        = String -> ServerPartT m Response++{-+    Dlg is a monad in the obvious way: return represents a dialogue that has no+    steps; and (>>=) combines dialogues by doing the first part of the first+    dialogue, and then continuing with the rest.+-}+instance Monad m => Monad (Dlg m) where+    return         = Done+    Done x   >>= y = y x+    Action x >>= y = Action (x >>= return . (>>= y))+    Step p f >>= y = Step p (f >>= y)++instance MonadTrans Dlg where+    lift = Action . lift . (>>= return . Done)++instance MonadIO m => MonadIO (Dlg m) where+    liftIO = lift . liftIO++{-+    Converts a 'ServerPartT' into a Dlg.  This is essentially a mechanism for+    escaping the confines of the dialogue mechanism and performing your own+    processing with the request.+-}+perform :: Monad m => ServerPartT m a -> Dlg m a+perform x = Action (x >>= return . Done)++{-|+    Converts methods for rendering and parsing the result of a page into a+    'Dlg' step.+-}+showPage :: Monad m => Page m -> ServerPartT m a -> Dlg m a +showPage p r = Step p (Action (fmap Done r))++{-|+    A 'DialogueSession' represents a single user's active dialogs, which are+    retained for an entire session.  A reaper thread clears up sessions that+    have not been touched for some session timeout, so each session also+    stores the last time it was touched.  In addition, each session is+    associated with a fixed client address and cannot be used from a different+    client, which avoids hijacking.+-}+data DialogueSession m = DialogueSession {+    client      :: String,+    lastTouched :: MVar UTCTime,+    dialogues   :: MVar (Map Int (Dlg m ()))+    }++{-|+    A 'DialogueManager' is responsible for maintaining the state for 'Dlg'+    sequences for all users.  To do this, it keeps for each user a session+    object encapsulating their dialogues, and associates each user with their+    session using cookies.+-}+data DialogueManager m = DialogueManager {+    sessions    :: MVar (Map Int (DialogueSession m)),+    open        :: MVar Bool+    }++{-|+    Determine whether a session is still valid or not.+-}+goodSession :: NominalDiffTime -> (Int, DialogueSession m) -> IO Bool+goodSession timeout (_, DialogueSession _ tref _) = do+    st <- readMVar tref+    ct <- getCurrentTime +    return (diffUTCTime ct st <= timeout)++{-|+    Monadic while statement, for convenience.+-}+whileM :: Monad m => m Bool -> m a -> m ()+whileM cond action = do+    b <- cond+    if b then action >> whileM cond action else return ()++{-|+    Create a new 'DialogueManager' to manage a set of dialogues in the web+    application.  This also spawns the session reaper, which cleans up sessions+    that haven't been touched for a given time period.+-}+makeDialogueManager :: NominalDiffTime -> IO (DialogueManager m)+makeDialogueManager timeout = do+    oref <- newMVar True+    sref <- newMVar M.empty+    forkIO $ whileM (readMVar oref) $ do+        threadDelay 5000+        sessionMap   <- takeMVar sref+        goodSessions <- filterM (goodSession timeout) (M.assocs sessionMap)+        putMVar sref (M.fromList goodSessions)+    return (DialogueManager { sessions = sref, open = oref })++{-|+    Closes a DialogueManager, which will cause it to cease accepting any+    incoming requests, and also to terminate the session reaper thread.+-}+closeDialogueManager :: DialogueManager m -> IO ()+closeDialogueManager (DialogueManager _ oref) = do swapMVar oref False+                                                   return ()++{-|+    Given a 'Map' with a key type that can be randomly chosen, returns a key+    that is not currently in the map.+-}+uniqueKey :: (Random k, Ord k) => Map k a -> IO k+uniqueKey m = do k <- randomIO+                 if M.member k m then uniqueKey m else return k++{-|+    Adds a 'DialogueSession' and associated cookie.  This always sets a new+    blank session, so should only be used when there is no session already.+-}+addDialogueSession :: MonadIO m => DialogueManager m ->+                                   ServerPartT m (DialogueSession m)+addDialogueSession (DialogueManager sref _) = do+    rq <- askRq+    (k, session) <- liftIO $ do+        smap <- takeMVar sref+        k    <- uniqueKey smap+        let (c, _) = rqPeer rq+        ct   <- getCurrentTime+        tref <- newMVar ct+        dref <- newMVar M.empty+        let session = DialogueSession c tref dref+        putMVar sref (M.insert k session smap)+        return (k, session)+    addCookie (-1) (mkCookie "dlg-sid" (show k))+    return session++{-|+    Ensures that there is a 'DialogueSession' for the current user, and returns+    it.  Adds a blank one if necessary.  This also updates the last touched+    time for the session, preventing it from being removed by the reaper+    thread for a while.+-}+getDialogueSession :: MonadIO m => DialogueManager m ->+                                   ServerPartT m (DialogueSession m)+getDialogueSession dmgr@(DialogueManager sref oref) = do+    open    <- liftIO $ readMVar oref+    unless open mzero+    rq      <- askRq+    msid    <- getDataFn (lookCookieValue "dlg-sid")+    case msid of+        Nothing  -> addDialogueSession dmgr+        Just sid -> do smap <- liftIO $ readMVar sref+                       case M.lookup (read sid) smap of+                            Nothing -> addDialogueSession dmgr+                            Just s@(DialogueSession { lastTouched = tref }) ->+                                if fst (rqPeer rq) /= client s+                                    then addDialogueSession dmgr+                                    else liftIO $ do ct <- getCurrentTime+                                                     swapMVar tref ct+                                                     return s++{-|+    A simple response that adds trailing slashes to a path when they don't exist.+    Trailing slashes are required for dialogue paths, since a path component is used+    to distinguish the dialogue ID.+-}+addTrailingSlash :: Monad m => ServerPartT m Response+addTrailingSlash = do+    rq <- askRq+    tempRedirect (rqUri rq ++ "/") (toResponse "Please use a trailing slash")++{-|+    Inverts a guard condition. +-}+notGuard :: (ServerMonad m, MonadPlus m) => m () -> m ()+notGuard g = (g >> return mzero) `mplus` return (return ()) >>= id++{-|+    The 'dialogue' function builds a 'ServerPartT' that handles a given+    dialogue.  In general, it can be combined in normal ways with guards and+    such, so long as changes in the request parameters won't cause it to be+    missed when future requests are made in the same dialogue.+-}+dialogue :: MonadIO m => DialogueManager m -> Dlg m ()+                      -> ServerPartT m Response+dialogue dmgr dlg = do (DialogueSession _ _ dref) <- getDialogueSession dmgr+                       checkForm `mplus` continue dref+                                 `mplus` (nullDir >> handle dref (forever dlg))++    where checkForm = nullDir >> notGuard trailingSlash >> addTrailingSlash++          continue dref = path $ \ (dlgid :: Int) -> nullDir >> do+                dlgs <- liftIO $ readMVar dref+                rq   <- askRq+                case M.lookup dlgid dlgs of+                    Nothing -> handle dref dlg+                    Just d  -> handle dref d++          handle dref (Done _)   = mzero+          handle dref (Action a) = a >>= handle dref+          handle dref (Step p f) = do+                dlgs <- liftIO $ takeMVar dref+                rq   <- askRq+                k    <- liftIO $ uniqueKey dlgs+                liftIO $ putMVar dref (M.insert k f dlgs)+                p (rqUri rq </> show k)++          (</>) :: String -> String -> String+          a </> b = let is = elemIndices '/' a+                    in  if null is then a ++ "/" ++ b+                                   else take (last is) a ++ "/" ++ b+
+ src/Happstack/Server/Dialogues/Formlets.hs view
@@ -0,0 +1,24 @@+module Happstack.Server.Dialogues.Formlets where++import Happstack.Server.Dialogues++import Text.XHtml.Strict+import Text.XHtml.Strict.Formlets hiding (hidden)+import Control.Applicative.Error (Failing (..))++import Happstack.Server.SimpleHTTP hiding (method)++import Control.Monad.Identity++showForm :: Monad m => XHtmlForm Identity a -> Dlg m (Maybe a)+showForm f = showPage render parse+    where render uri = let (_, Identity html, _) = runFormState [] f+                       in  return $ toResponse $+                              form ! [method "POST", action uri] << (html+                                  +++ submit "submit" "submit"+                                  )+          parse    = withDataFn lookPairs $ \env -> do+                        let (Identity x, _, _) = runFormState (map (fmap Left) env) f+                        case x of Success a -> return (Just a)+                                  Failure _ -> return Nothing+
+ src/Happstack/Server/Dialogues/Scaffold.hs view
@@ -0,0 +1,127 @@+{-# LANGUAGE TemplateHaskell      #-}+{-# LANGUAGE TypeSynonymInstances #-}++module Happstack.Server.Dialogues.Scaffold where++import Happstack.Server+import Happstack.Server.Dialogues+import Language.Haskell.TH++import qualified Data.ByteString.Lazy.Char8 as B++import Data.Char  (ord)+import Data.Maybe (fromMaybe)++import Data.List++escapeHtml :: String -> String+escapeHtml ""                       = ""+escapeHtml ('\"':cs)                = "&#34;" ++ escapeHtml cs+escapeHtml ('\'':cs)                = "&#39;" ++ escapeHtml cs+escapeHtml ('&' :cs)                = "&#38;" ++ escapeHtml cs+escapeHtml ('<' :cs)                = "&#60;" ++ escapeHtml cs+escapeHtml ('>' :cs)                = "&#62;" ++ escapeHtml cs+escapeHtml (c   :cs) | ord c >= 160 = "&#" ++ show (ord c) ++ ";" ++ escapeHtml cs+                     | otherwise    = c : escapeHtml cs++{-|+    The 'scaffold' function builds a user interaction to display and collect information.+    In a finished web application, this should generally be replaced with a better+    mechanism for rendering pages, such as a templating engine, XSLT, or something+    of the sort.+-}+scaffold :: (Scaffolded a, Monad m) => String -> a -> Dlg m a+scaffold s v = showPage out inp+    where out uri = return $ setHeader "Content-type" "text/html" $ toResponse $+            "<html><body>"+            ++ escapeHtml s ++ "<br><hr>"+            ++ "<form action=\"" ++ uri ++ "\">"+            ++ render "dlg-vals" v+            ++ "<input type=\"submit\" value=\"Submit\">"+            ++ "<script type=\"text/javascript\">"+            ++ "    document.forms[0].elements[0].focus();"+            ++ "</script>"+            ++ "</form>"+            ++ "</html></body>"+          inp = parse "dlg-vals" v++{-|+    The 'Scaffolded' type class represents data that can be included in a form.+    To make it easy to compose several of these in a page, rendering and parsing+    are parameterized with prefix strings so that they may be made unique across+    a page.+-}+class Scaffolded a where+    render :: String -> a -> String+    parse  :: Monad m => String -> a -> ServerPartT m a++instance Scaffolded () where+    render _ () = ""+    parse  _ _  = return ()++instance Scaffolded String where+    render pfx s = "<input type=\"text\" name=\"" ++ pfx ++ "\" value=\"" ++ escapeHtml s ++ "\">"+    parse  pfx _ = fmap (fromMaybe "") $ getDataFn (look pfx)++instance Scaffolded B.ByteString where+    render pfx s = "<textarea name=\"" ++ pfx ++ "\">" ++ escapeHtml (B.unpack s) ++ "</textarea><br>"+    parse  pfx _ = do x <- getDataFn (lookInput pfx)+                      case x of Just inp -> return (inputValue inp)+                                Nothing  -> return B.empty++instance Scaffolded Bool where+    render pfx v = "<input type=\"checkbox\" name=\"" ++ pfx ++ "\" value=\"True\" "+                   ++ if v then "checked" else "" ++ ">"+    parse  pfx _ = fmap (maybe False (const True)) $ getDataFn (look pfx)++instance Scaffolded Int where+    render pfx s = render pfx (show s)+    parse  pfx v = fmap read $ parse pfx undefined++instance (Scaffolded a, Scaffolded b) => Scaffolded (a, b) where+    render pfx (x,y) = render (pfx ++ "_1") x ++ "<br>"+                    ++ render (pfx ++ "_2") y ++ "<br>"+    parse  pfx (x,y) = do x <- parse (pfx ++ "_1") x+                          y <- parse (pfx ++ "_2") y+                          return (x,y)++instance (Scaffolded a, Scaffolded b, Scaffolded c)+        => Scaffolded (a, b, c) where+    render pfx (x,y,z) = render (pfx ++ "_1") x ++ "<br>"+                      ++ render (pfx ++ "_2") y ++ "<br>"+                      ++ render (pfx ++ "_3") z ++ "<br>"+    parse  pfx (x,y,z) = do x <- parse (pfx ++ "_1") x+                            y <- parse (pfx ++ "_2") y+                            z <- parse (pfx ++ "_3") z+                            return (x,y,z)++{-|+    Wrapper type to present a list of options.+-}+data Choice a = Choice [a] Int deriving Show++toChoice :: (Bounded a, Enum a, Eq a) => a -> Choice a+toChoice x = let choices = [minBound .. maxBound]+             in  Choice choices (fromMaybe minBound $ elemIndex x choices)++fromChoice :: Choice a -> a+fromChoice (Choice cs i) = cs !! i++chooseFrom :: [a] -> Choice a+chooseFrom xs = Choice xs 0++instance (Eq a, Show a) => Scaffolded (Choice a) where+    render pfx (Choice cs i) = concat+        [ "<input type=\"radio\" name=\"" ++ escapeHtml pfx ++ "\""+          ++ " value=\"" ++ show i' ++ "\""+          ++ if i == i' then " selected>" else ">"+          ++ "<br>"+          | (c,i') <- zip cs [0..] ]+    parse  pfx (Choice cs _) = fmap (Choice cs . read) $ parse pfx undefined++{-+deriveScaffolded :: [Name] -> Q [Dec]+deriveScaffolded = mapM $ \n -> return $+    InstanceD [] (AppT (ConT (mkName "Scaffolded")) (ConT n)) []+-}+