{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE OverloadedStrings #-} module Snap.Dialogues ( DlgManager, makeDlgManager, HasDlgManager(..), Dlg, Page, showPage, dialogue ) where import Control.Applicative import Control.Concurrent import Control.Monad import Control.Monad.Trans import Snap.Extension.Session import Snap.SessionUtil import Snap.Types import Text.Regex.Posix import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Map (Map) import qualified Data.Map as M newtype DlgManager m = DlgManager { unDlgManager :: MVar (Map SessionKey (Dlg m ())) } makeDlgManager :: IO (DlgManager m) makeDlgManager = fmap DlgManager (newMVar M.empty) class HasDlgManager m a | a -> m where getDlgManager :: a -> DlgManager 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 (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 = ByteString -> m () {- 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) {- Converts an action in the underlying Monad into a Dlg. This is essentially a mechanism for escaping the confines of the dialogue mechanism and performing your own processing with the request. -} instance MonadTrans Dlg where lift x = Action (x >>= return . Done) {- Dlg is an instance of MonadIO by passing it through to the underlying monad. -} instance MonadIO m => MonadIO (Dlg m) where liftIO = lift . liftIO {-| Converts methods for rendering and parsing the result of a page into a 'Dlg' step. -} showPage :: Monad m => Page m -> m a -> Dlg m a showPage p r = Step p (Action (r >>= return . Done)) dlgToken :: MonadSnap m => ByteString -> (SessionKey -> m a) -> m a dlgToken tname action = popPathTo $ \token -> case token =~ ("^([^\\-]*)-([0-9]+)$" :: ByteString) of [[_, name, tok]] | name == tname -> action $ read $ B.unpack tok _ -> pass findDlg :: (MonadSession m, HasDlgManager m t, t ~ Session m) => SessionKey -> m (Dlg m ()) findDlg tok = do dref <- fmap (unDlgManager . getDlgManager) getSession dlgs <- liftIO $ readMVar dref maybe mzero return $ M.lookup tok dlgs handle :: (MonadSession m, HasDlgManager m t, t ~ Session m) => ByteString -> Dlg m () -> m () handle _ (Done _) = pass handle name (Action a) = a >>= handle name handle name (Step p f) = do dref <- fmap (unDlgManager . getDlgManager) getSession dlgs <- liftIO $ takeMVar dref k <- liftIO $ uniqueKey dlgs liftIO $ putMVar dref (M.insert k f dlgs) p (name `B.append` "-" `B.append` B.pack (show k)) {-| The 'dialogue' function builds a @Snap ()@ that handles a given dialogue. The URLs of the dialog are of the form "/.../dlg-55555", where "dlg" is the prefix (passed as a parameter) and 55555 is the (numeric) dialogue ID. Requests to "/.../dlg" create a new dialogue. In general, this can be combined in normal ways with other routing constructs, so long as request URIs of the above forms reach this handler. When pages are served as part of a dialog, their relative paths are passed on to later handlers, -} dialogue :: (MonadSession m, HasDlgManager m t, t ~ Session m) => ByteString -> Dlg m () -> m () dialogue name dlg = new <|> continue where new = dir name $ ifTop $ handle name dlg continue = dlgToken name $ \key -> ifTop $ findDlg key >>= handle name