packages feed

iterio-server 0.2 → 0.3

raw patch · 8 files changed

+377/−137 lines, 8 filesdep +filepathdep +unixdep ~iterIOPVP ok

version bump matches the API change (PVP)

Dependencies added: filepath, unix

Dependency ranges changed: iterIO

API changes (from Hackage documentation)

- Data.IterIO.Http.Support.Action: routeAction :: Monad m => Action t m () -> HttpRoute m t
- Data.IterIO.Http.Support.Action: routeActionPattern :: Monad m => String -> Action t m () -> HttpRoute m t
- Data.IterIO.Http.Support.Routing: runLHttpRoute :: Monad m => HttpRoute m s -> HttpReq s -> Iter ByteString m (HttpResp m)
+ Data.IterIO.Http.Support.Action: ActionState :: HttpReq t -> HttpResp m -> [Param] -> b -> ActionState t b m
+ Data.IterIO.Http.Support.Action: actionBody :: ActionState t b m -> b
+ Data.IterIO.Http.Support.Action: actionParams :: ActionState t b m -> [Param]
+ Data.IterIO.Http.Support.Action: actionReq :: ActionState t b m -> HttpReq t
+ Data.IterIO.Http.Support.Action: actionResp :: ActionState t b m -> HttpResp m
+ Data.IterIO.Http.Support.Action: data ActionState t b m
+ Data.IterIO.Http.Support.Action: getBody :: Monad m => Action t b m b
+ Data.IterIO.Http.Support.Action: instance [safe] Show Param
+ Data.IterIO.Http.Support.Action: paramVal :: Monad m => ByteString -> Action t b m (ByteString)
+ Data.IterIO.Http.Support.Action: paramValM :: Monad m => (ByteString -> a) -> ByteString -> Action t b m (Maybe a)
+ Data.IterIO.Http.Support.Action: setParams :: Monad m => [Param] -> Action t b m [Param]
+ Data.IterIO.Http.Support.Responses: redirectBack :: Monad m => Action t b m ()
+ Data.IterIO.Http.Support.Routing: ActionRoute :: (HttpReq s -> m (Maybe (Action s b m ()))) -> ActionRoute b m s
+ Data.IterIO.Http.Support.Routing: instance [safe] Monad m => Monoid (ActionRoute b m s)
+ Data.IterIO.Http.Support.Routing: newtype ActionRoute b m s
+ Data.IterIO.Http.Support.Routing: routeAction :: Monad m => Action t b m () -> ActionRoute b m t
+ Data.IterIO.Http.Support.Routing: routeFileSys :: (String -> ByteString) -> FilePath -> ActionRoute b IO t
+ Data.IterIO.Http.Support.Routing: routeMethod :: Monad m => String -> ActionRoute b m s -> ActionRoute b m s
+ Data.IterIO.Http.Support.Routing: routeName :: Monad m => String -> ActionRoute b m s -> ActionRoute b m s
+ Data.IterIO.Http.Support.Routing: routePattern :: Monad m => String -> ActionRoute b m t -> ActionRoute b m t
+ Data.IterIO.Http.Support.Routing: routeTop :: Monad m => ActionRoute b m s -> ActionRoute b m s
+ Data.IterIO.Http.Support.Routing: routeVar :: Monad m => ActionRoute b m s -> ActionRoute b m s
+ Data.IterIO.Http.Support.Routing: runAction :: Monad m => Action s b m a -> HttpReq s -> b -> m (HttpResp m)
+ Data.IterIO.Http.Support.Routing: runActionRoute :: Monad m => ActionRoute b m s -> Action s b m ()
+ Data.IterIO.Http.Support.Routing: runIterAction :: Monad m => Action s ByteString m a -> HttpReq s -> Iter ByteString m (HttpResp m)
+ Data.IterIO.Http.Support.Utils: parseParams :: Monad m => Action t ByteString m [Param]
+ Data.IterIO.Http.Support.Utils: parseParams' :: Monad m => HttpReq a -> ByteString -> m [Param]
- Data.IterIO.Http.Support.Action: destroySession :: Monad m => Action t m ()
+ Data.IterIO.Http.Support.Action: destroySession :: Monad m => Action t b m ()
- Data.IterIO.Http.Support.Action: getHttpReq :: Monad m => Action t m (HttpReq t)
+ Data.IterIO.Http.Support.Action: getHttpReq :: Monad m => Action t b m (HttpReq t)
- Data.IterIO.Http.Support.Action: param :: Monad m => ByteString -> Action t m (Maybe Param)
+ Data.IterIO.Http.Support.Action: param :: Monad m => ByteString -> Action t b m (Maybe Param)
- Data.IterIO.Http.Support.Action: params :: Monad m => Action t m ([Param])
+ Data.IterIO.Http.Support.Action: params :: Monad m => Action t b m [Param]
- Data.IterIO.Http.Support.Action: requestHeader :: Monad m => ByteString -> Action t m (Maybe ByteString)
+ Data.IterIO.Http.Support.Action: requestHeader :: Monad m => ByteString -> Action t b m (Maybe ByteString)
- Data.IterIO.Http.Support.Action: setSession :: Monad m => String -> Action t m ()
+ Data.IterIO.Http.Support.Action: setSession :: Monad m => String -> Action t b m ()
- Data.IterIO.Http.Support.Action: type Action t m a = StateT (HttpReq t, HttpResp m, [Param]) m a
+ Data.IterIO.Http.Support.Action: type Action t b m a = StateT (ActionState t b m) m a
- Data.IterIO.Http.Support.Responses: redirectTo :: Monad m => String -> Action t m ()
+ Data.IterIO.Http.Support.Responses: redirectTo :: Monad m => String -> Action t b m ()
- Data.IterIO.Http.Support.Responses: render :: Monad m => String -> ByteString -> Action t m ()
+ Data.IterIO.Http.Support.Responses: render :: Monad m => String -> ByteString -> Action t b m ()
- Data.IterIO.Http.Support.Responses: respond404 :: Monad m => Action t m ()
+ Data.IterIO.Http.Support.Responses: respond404 :: Monad m => Action t b m ()
- Data.IterIO.Http.Support.Responses: respondStat :: Monad m => HttpStatus -> Action t m ()
+ Data.IterIO.Http.Support.Responses: respondStat :: Monad m => HttpStatus -> Action t b m ()
- Data.IterIO.Http.Support.RestController: class Monad m => RestController m a where restIndex _ = respond404 restShow _ _ = respond404 restNew _ = respond404 restCreate _ = respond404 restEdit _ _ = respond404 restUpdate _ _ = respond404 restDestroy _ _ = respond404
+ Data.IterIO.Http.Support.RestController: class Monad m => RestController t b m a where restIndex _ = respond404 restShow _ _ = respond404 restNew _ = respond404 restCreate _ = respond404 restEdit _ _ = respond404 restUpdate _ _ = respond404 restDestroy _ _ = respond404
- Data.IterIO.Http.Support.RestController: restCreate :: RestController m a => a -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restCreate :: RestController t b m a => a -> Action t b m ()
- Data.IterIO.Http.Support.RestController: restDestroy :: RestController m a => a -> ByteString -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restDestroy :: RestController t b m a => a -> ByteString -> Action t b m ()
- Data.IterIO.Http.Support.RestController: restEdit :: RestController m a => a -> ByteString -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restEdit :: RestController t b m a => a -> ByteString -> Action t b m ()
- Data.IterIO.Http.Support.RestController: restIndex :: RestController m a => a -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restIndex :: RestController t b m a => a -> Action t b m ()
- Data.IterIO.Http.Support.RestController: restNew :: RestController m a => a -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restNew :: RestController t b m a => a -> Action t b m ()
- Data.IterIO.Http.Support.RestController: restShow :: RestController m a => a -> ByteString -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restShow :: RestController t b m a => a -> ByteString -> Action t b m ()
- Data.IterIO.Http.Support.RestController: restUpdate :: RestController m a => a -> ByteString -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restUpdate :: RestController t b m a => a -> ByteString -> Action t b m ()
- Data.IterIO.Http.Support.RestController: routeRestController :: RestController m a => String -> a -> HttpRoute m t
+ Data.IterIO.Http.Support.RestController: routeRestController :: RestController t b m a => String -> a -> ActionRoute b m t

Files

Data/IterIO/Http/Support.hs view
@@ -1,10 +1,16 @@+{-# LANGUAGE CPP #-}+#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 702)+{-# LANGUAGE Safe #-}+#endif module Data.IterIO.Http.Support ( module Data.IterIO.Http.Support.Action                                 , module Data.IterIO.Http.Support.Responses                                 , module Data.IterIO.Http.Support.RestController                                 , module Data.IterIO.Http.Support.Routing+                                , module Data.IterIO.Http.Support.Utils                                 )where  import Data.IterIO.Http.Support.Action import Data.IterIO.Http.Support.Responses import Data.IterIO.Http.Support.RestController import Data.IterIO.Http.Support.Routing+import Data.IterIO.Http.Support.Utils
Data/IterIO/Http/Support/Action.hs view
@@ -1,125 +1,100 @@+{-# LANGUAGE CPP #-}+#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 702)+{-# LANGUAGE Safe #-}+#endif -- |Defines the 'Action' monad which abstracts some of the details of handling -- HTTP requests with IterIO. module Data.IterIO.Http.Support.Action (     Action+  , ActionState(..)   , Param(..)-  , routeAction-  , routeActionPattern-  , params-  , param+  , params, param, paramVal, paramValM+  , setParams+  , getBody   , getHttpReq-  , setSession-  , destroySession+  , setSession, destroySession   , requestHeader ) where -import Control.Monad.Trans+import Control.Monad import Control.Monad.Trans.State import qualified Data.ByteString.Char8 as S import qualified Data.ByteString.Lazy.Char8 as L-import Data.List-import Data.List.Split-import Data.IterIO import Data.IterIO.Http-import Data.IterIO.HttpRoute+import Data.List (find)  -- | A request parameter from a form field in the HTTP body data Param = Param {     paramKey :: S.ByteString   , paramValue :: L.ByteString   , paramHeaders :: [(S.ByteString, S.ByteString)] -- ^ Header of a @multipart/form-data@ post+} deriving (Show)++data ActionState t b m = ActionState {+    actionReq  :: HttpReq t+  , actionResp :: HttpResp m+  , actionParams   :: [Param]+  , actionBody :: b }  -- | A 'StateT' monad in which requests can be handled. It keeps track of the -- 'HttpReq', the form parameters from the request body and an 'HttpResp' used -- to reply to the client.-type Action t m a = StateT (HttpReq t, HttpResp m, [Param]) m a---- | Routes an 'Action'-routeAction :: Monad m => Action t m () -> HttpRoute m t-routeAction action = routeFn $ runAction action---- | Routes an 'Action' to the given URL pattern. Patterns can include--- directories as well as variable patterns (prefixed with @:@) to be passed--- into the 'Action' as extra 'Param's. Some examples of URL patters:------  * \/posts\/:id------  * \/posts\/:id\/new------  * \/:date\/posts\/:category\/new----routeActionPattern :: Monad m => String -> Action t m () -> HttpRoute m t-routeActionPattern pattern action = foldl' addVar (routeFn $ runActionWithRouteNames patternList action) patternList-  where patternList = reverse $ filter ((/= 0) . length) $ splitOn "/" pattern-        addVar rt (':':_) = routeVar rt-        addVar rt name = routeName name rt+type Action t b m a = StateT (ActionState t b m) m a  -- |Sets a the value for \"_sess\" in the cookie to the given string.-setSession :: Monad m => String -> Action t m ()-setSession cookie = StateT $ \(req, resp, prm) ->-  let cookieHeader = S.pack $ "Set-Cookie: _sess=" ++ cookie ++ "; path=/;"-  in return $ ((), (req, resp { respHeaders = cookieHeader:(respHeaders resp)}, prm))+setSession :: Monad m => String -> Action t b m ()+setSession cookie = modify $ \s ->+  let cookieHeader = (S.pack "Set-Cookie", S.pack $ "_sess=" ++ cookie ++ "; path=/;")+  in s { actionResp = respAddHeader cookieHeader (actionResp s)}  -- |Removes the \"_sess\" key-value pair from the cookie.-destroySession :: Monad m => Action t m ()-destroySession = StateT $ \(req, resp, prm) ->-  let cookieHeader = S.pack $ "Set-Cookie: _sess=; path=/; expires=Thu, Jan 01 1970 00:00:00 UTC;"-  in return $ ((), (req, resp { respHeaders = cookieHeader:(respHeaders resp)}, prm))+destroySession :: Monad m => Action t b m ()+destroySession = modify $ \s ->+  let cookieHeader = (S.pack "Set-Cookie", S.pack "_sess=; path=/; expires=Thu, Jan 01 1970 00:00:00 UTC;")+  in s { actionResp = respAddHeader cookieHeader (actionResp s)}  -- |Returns the value of an Http Header from the request if it exists otherwise -- 'Nothing'-requestHeader :: Monad m => S.ByteString -> Action t m (Maybe S.ByteString)+requestHeader :: Monad m => S.ByteString -> Action t b m (Maybe S.ByteString) requestHeader name = do   httpReq <- getHttpReq   return $ lookup name (reqHeaders httpReq)  -- |Returns the 'HttpReq' for the current request.-getHttpReq :: Monad m => Action t m (HttpReq t)-getHttpReq = StateT $ \(req, resp, prm) -> return $ (req, (req, resp, prm))+getHttpReq :: Monad m => Action t b m (HttpReq t)+getHttpReq = gets actionReq +-- |Returns the body of the current request.+getBody :: Monad m => Action t b m b+getBody = gets actionBody++-- | Set the list of 'Param's.+setParams :: Monad m => [Param] -> Action t b m [Param]+setParams prms = do+  modify $ \s -> s { actionParams = prms }+  return prms+ -- | Returns a list of all 'Param's.-params :: Monad m => Action t m ([Param])-params = StateT $ \s@(_, _, prm) -> return (prm, s)+params :: Monad m => Action t b m [Param]+params = do+  gets actionParams  -- | Returns the 'Param' corresponding to the specified key or 'Nothing' -- if one is not present in the request.-param :: Monad m => S.ByteString -> Action t m (Maybe Param)-param key = do-  prms <- params-  return $ foldl go Nothing prms-  where go Nothing p = if paramKey p == key then Just p else Nothing-        go a _ = a---runAction :: Monad m-              => Action s m ()-              -> HttpReq s-              -> Iter L.ByteString m (HttpResp m)-runAction = runActionWithRouteNames []--runActionWithRouteNames :: Monad m-              => [String]-              -> Action s m ()-              -> HttpReq s-              -> Iter L.ByteString m (HttpResp m)-runActionWithRouteNames routeNames action req = do-  prms <- paramList req-  let pathLstParams = pathLstToParams req routeNames-  (_, (_, response, _)) <- lift $ (runStateT action) (req, mkHttpHead stat200, pathLstParams ++ prms)-  return $ response+param :: Monad m => S.ByteString -> Action t b m (Maybe Param)+param key = find ((== key) . paramKey) `liftM` params +-- | Force get parameter value+paramVal :: Monad m => S.ByteString -> Action t b m (L.ByteString)+paramVal n = param n >>=+  maybe (fail "No such parameter") (return . paramValue) -pathLstToParams :: HttpReq s -> [String] -> [Param]-pathLstToParams req routeNames = result-  where (result, _) = foldl go ([], reqPathParams req) routeNames-        go (prms, plst) (':':var) = ((transform var $ head plst):prms, tail plst)-        go s _ = s-        transform k v = Param (S.pack k) (L.fromChunks [v]) []+-- | Get (maybe) paramater value and transform it with @f@+paramValM :: Monad m+          => (L.ByteString -> a)+          -> S.ByteString+          -> Action t b m (Maybe a)+paramValM f n = fmap (f . paramValue) `liftM` param n -paramList :: Monad m => HttpReq s -> Iter L.ByteString m [Param]-paramList req = foldForm req handle []-  where handle accm field = do-          val <- pureI-          return $ (Param (ffName field) val (ffHeaders field)):accm 
Data/IterIO/Http/Support/Responses.hs view
@@ -1,7 +1,12 @@+{-# LANGUAGE CPP #-}+#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 702)+{-# LANGUAGE Safe #-}+#endif -- |Utility functions for responding to HTTP requests from within an 'Action'. module Data.IterIO.Http.Support.Responses (     render   , redirectTo+  , redirectBack   , respond404   , respondStat ) where@@ -10,36 +15,44 @@ import qualified Data.ByteString.Char8 as S import qualified Data.ByteString.Lazy.Char8 as L import Data.IterIO-import Data.IterIO.Http.Support.Action (Action)+import Data.IterIO.Http.Support.Action (Action, ActionState(..), requestHeader) import Data.IterIO.Http  -- | Responds to the client with an empty @404@ (Not Found) response.-respond404 :: Monad m => Action t m ()-respond404 = StateT $-  \(req, _, params) -> return $ ((), (req, resp404 req, params))+respond404 :: Monad m => Action t b m ()+respond404 = modify $+  \s -> s { actionResp = resp404 $ actionReq s }  -- | Replaces the HTTP status in the current 'HttpResp' with the given -- 'HttpStatus'.-respondStat :: Monad m => HttpStatus -> Action t m ()-respondStat status = StateT $-  \(req, resp, params) -> return $ ((), (req, resp { respStatus = status }, params))+respondStat :: Monad m => HttpStatus -> Action t b m ()+respondStat status = modify $+  \s -> s { actionResp = (actionResp s) { respStatus = status} }  -- | Responds to the client with a @303@ (Temporary Redirect) response to the given -- path. redirectTo :: Monad m            => String -- ^ The path to redirect to-           -> Action t m ()-redirectTo path = StateT $-  \(req, _, params) -> return $ ((), (req, resp303 path, params))+           -> Action t b m ()+redirectTo path = modify $+  \s -> s { actionResp = resp303 path } +-- | Redirect \"back\" according to the \"referer\" header.+redirectBack :: Monad m => Action t b m ()+redirectBack = do+  mhdr <- requestHeader (S.pack "referer")+  maybe (fail "Referer header not set") (redirectTo . S.unpack) mhdr+  + -- | Responds to the client with a @200@ (Success) response with the given body -- and mime-type. render :: Monad m        => String -- ^ The mime-type of the response (commonly \"text\/html\")        -> L.ByteString -- ^ The response body-       -> Action t m ()-render ctype text = StateT $ \(req, resp, prm) -> return $ ((), (req, mkResp resp, prm))-  where len = S.pack $ "Content-Length: " ++ show (L.length text)-        ctypeHeader = S.pack $ "Content-Type: " ++ ctype-        mkResp resp = resp { respHeaders = respHeaders resp ++ [ctypeHeader, len],-                        respBody = inumPure text }+       -> Action t b m ()+render ctype text = modify $ \s -> s { actionResp = mkResp $ actionResp s }+  where len = (S.pack "Content-Length", S.pack . show . L.length $ text)+        ctypeHeader = (S.pack "Content-Type", S.pack ctype)+        mkResp resp = resp { respHeaders = ctypeHeader : (len : respHeaders resp)+                           , respBody = inumPure text }+
Data/IterIO/Http/Support/RestController.hs view
@@ -1,3 +1,7 @@+{-# LANGUAGE CPP #-}+#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 702)+{-# LANGUAGE Safe #-}+#endif {-# LANGUAGE OverloadedStrings, MultiParamTypeClasses #-} -- | This module defines the 'RestController' class  @@ -8,36 +12,36 @@  import qualified Data.ByteString as S import qualified Data.ByteString.Lazy as L-import Data.IterIO.HttpRoute import Data.IterIO.Http.Support.Action+import Data.IterIO.Http.Support.Routing import Data.IterIO.Http.Support.Responses import Data.Monoid  -- |The class @RestController@ allows a set of actions to be routed using -- RESTful HTTP verbs.-class Monad m => RestController m a where+class Monad m => RestController t b m a where   -- |GET \/-  restIndex :: a -> Action t m ()+  restIndex :: a -> Action t b m ()   restIndex _ = respond404    -- |GET \/:id   --   -- @id@ is passed in as the second parameter.-  restShow :: a -> L.ByteString -> Action t m ()+  restShow :: a -> L.ByteString -> Action t b m ()   restShow _ _ = respond404    -- |GET \/new-  restNew :: a -> Action t m ()+  restNew :: a -> Action t b m ()   restNew _ = respond404    -- |POST \/-  restCreate :: a -> Action t m ()+  restCreate :: a -> Action t b m ()   restCreate _ = respond404    -- |GET \/:id\/edit   --   -- @id@ is passed in as the second parameter.-  restEdit :: a -> L.ByteString -> Action t m ()+  restEdit :: a -> L.ByteString -> Action t b m ()   restEdit _ _ = respond404    -- |PUT \/:id@@ -47,7 +51,7 @@   -- Since @PUT@ is not supported by many browsers, this action also responds to   -- requests containing the HTTP header "X-HTTP-Method-Override: PUT"   -- regardless of the actual HTTP method (@GET@ or @POST@)-  restUpdate :: a -> L.ByteString -> Action t m ()+  restUpdate :: a -> L.ByteString -> Action t b m ()   restUpdate _ _ = respond404    -- |DELETE \/:id@@ -57,11 +61,11 @@   -- Since @DELETE@ is not supported by many browsers, this action also responds to   -- requests containing the HTTP header "X-HTTP-Method-Override: DELETE"   -- regardless of the actual HTTP method (@GET@ or @POST@)-  restDestroy :: a -> L.ByteString -> Action t m ()+  restDestroy :: a -> L.ByteString -> Action t b m ()   restDestroy _ _ = respond404  -- |Runs an action, passing in named parameter.-runWithVar :: Monad m => S.ByteString -> (L.ByteString -> Action t m ()) -> Action t m ()+runWithVar :: Monad m => S.ByteString -> (L.ByteString -> Action t b m ()) -> Action t b m () runWithVar varName controller = do   (Just var) <- param varName   controller $ paramValue var@@ -89,14 +93,15 @@ -- --    * PUT \/posts\/:id => myRestController#restUpdate ---routeRestController :: RestController m a => String -> a -> HttpRoute m t-routeRestController prefix controller = routeName prefix $ mconcat [+routeRestController :: RestController t b m a => String -> a -> ActionRoute b m t+routeRestController prefix controller = routePattern prefix $ mconcat [     routeTop $ routeMethod "GET" $ routeAction $ restIndex controller   , routeTop $ routeMethod "POST" $ routeAction $ restCreate controller-  , routeMethod "GET" $ routeActionPattern "/:id/edit" $ runWithVar "id" $ restEdit controller-  , routeMethod "GET" $ routeActionPattern "/new" $ restNew controller-  , routeMethod "GET" $ routeActionPattern "/:id" $ runWithVar "id" $ restShow controller-  , routeMethod "DELETE" $ routeActionPattern "/:id" $ runWithVar "id" $ restDestroy controller-  , routeMethod "PUT" $ routeActionPattern "/:id" $ runWithVar "id" $ restUpdate controller+  , routeMethod "GET" $ routePattern "/new" $ routeAction $ restNew controller+  , routeMethod "GET" $ routePattern "/:id/edit" $ routeAction $ runWithVar "id" $ restEdit controller+  , routeMethod "GET" $ routePattern "/:id" $ routeAction $ runWithVar "id" $ restShow controller+  , routeMethod "DELETE" $ routePattern "/:id" $ routeAction $ runWithVar "id" $ restDestroy controller+  , routeMethod "PUT" $ routePattern "/:id" $ routeAction $ runWithVar "id" $ restUpdate controller+  , routeMethod "POST" $ routePattern "/:id" $ routeAction $ runWithVar "id" $ restUpdate controller   ] 
Data/IterIO/Http/Support/Routing.hs view
@@ -1,35 +1,239 @@+{-# LANGUAGE CPP #-}+#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 702)+{-# LANGUAGE Safe #-}+#endif {-# LANGUAGE OverloadedStrings #-} -- |Utility functions for routing. module Data.IterIO.Http.Support.Routing (-    runLHttpRoute+    ActionRoute(..)+  , runActionRoute, runIterAction, runAction+  , routeAction, routePattern+  , routeName, routeVar+  , routeTop, routeMethod, routeFileSys     ) where +import Control.Monad.Trans+import Control.Monad.Trans.State import Data.Char import qualified Data.ByteString.Lazy.Char8 as L import qualified Data.ByteString.Char8 as S+import Data.List+import Data.List.Split import Data.Maybe+import Data.Monoid+import Data.IterIO import Data.IterIO.Http-import Data.IterIO.HttpRoute-import Data.IterIO.Iter+import Data.IterIO.Http.Support.Action+import Data.IterIO.Http.Support.Responses +import System.FilePath.Posix+import System.Posix.Files+ -- |Converts a 'ByteString' to upper-case upcase :: S.ByteString -> S.ByteString upcase = S.map toUpper --- |Like 'runHttpRoute' but replaces @GET@ and @POST@ request headers with the--- value of the @X-HTTP-Method-Override@ HTTP header if it is present. This--- allows applicaitons to respond to @DELETE@ and @PUT@ methods even though--- many browsers do not support those methods.-runLHttpRoute :: Monad m-              => HttpRoute m s+-- | Like 'runAction' but consumes the rest of the request for the+-- body+runIterAction :: Monad m+              => Action s L.ByteString m a               -> HttpReq s               -> Iter L.ByteString m (HttpResp m)-runLHttpRoute route req = runHttpRoute route $ transformedReq+runIterAction act req = do+  body <- inumHttpBody req .| pureI+  lift $ runAction act req body++-- | Runs an 'ActionRoute'. If it satisfies the request, the+-- underlying 'Action' is returned, otherwise an 'Action' responding+-- with HTTP 404 (Not Found) is returned instead. Can be used at the+-- top of a request handler, for example:+-- +-- > httpHandler :: Action b m ()+-- > httpHandler = runActionRoute $ mconcat [+-- >     routeTop $ routeAction homeAction+-- >   , routeMethod "POST" $ routeAction handleForm+-- >   , routeMethod "GET" $ routeAction showForm+-- >   ]+-- +-- But also can be nested inside of another action to created nested+-- routes, or state dependant routes:+-- +-- > httpHandler :: Action b m ()+-- > httpHandler = runActionRoute $ mconcat [+-- >     routeTop $ routeAction homeAction+-- >   , routeName "foo" $ routeAction $ runActionRoute $ mconcat [+-- >         routeMethod "GET" $ runAction showForm+-- >       , routeMethod "POST" $ runAction handleForm+-- >       ]+-- >   ]+--+-- or+--+-- > handleForm = do+-- >   day <- lift $ getDayOfWeek+-- >   case mod day 2 of+-- >     0 -> runActionRoute $ routeName "stts" $ routeAction doRes+-- >     1 -> runActionRoute $ routeName "mwf" $ routeAction doRes+--+runActionRoute :: Monad m+              => ActionRoute b m s+              -> Action s b m ()+runActionRoute (ActionRoute route) = do+  req <- gets actionReq+  res <- lift $ route req+  fromMaybe respond404 res++-- | An @ActionRoute@ either matches an 'HttpReq' and returns a+-- corresponding 'Action' that responds to it, or 'Nothing'+-- signifying, no approriate 'Action' was found. @ActionRoute@s can+-- be strung together with, e.g., 'mappend' such that each will be+-- searched in turn until an 'Action' responding to the 'HttpReq' is+-- found.+newtype ActionRoute b m s = ActionRoute (HttpReq s -> m (Maybe (Action s b m ())))+-- TODO(alevy): Implement ActionRoute in terms of Reader++instance Monad m => Monoid (ActionRoute b m s) where+  mempty = ActionRoute $ const $ return Nothing+  mappend (ActionRoute a) (ActionRoute b) =+    ActionRoute $ \req -> do+      f <- a req+      case f of+        Just _ -> return f+        Nothing -> b req++popPath :: Bool -> HttpReq s -> HttpReq s+popPath isParm req =+    case reqPathLst req of+      h:t -> req { reqPathLst = t+                 , reqPathCtx = reqPathCtx req ++ [h]+                 , reqPathParams = if isParm then h : reqPathParams req+                                             else reqPathParams req+                 }+      _   -> error "empty path"++-- | Routes a specific directory name, like 'routeMap' for a singleton+-- map.+routeName :: Monad m => String -> ActionRoute b m s -> ActionRoute b m s+routeName name (ActionRoute route) = ActionRoute check+    where sname = S.pack name+          headok (h:_) | h == sname = True+          headok _                  = False+          check req | headok (reqPathLst req) = route $ popPath False req+          check _                             = return Nothing++-- | Matches any directory name, but additionally pushes it onto the+-- front of the 'reqPathParams' list in the 'HttpReq' structure.  This+-- allows the name to serve as a variable argument to the eventual+-- handling function.+routeVar :: Monad m => ActionRoute b m s -> ActionRoute b m s+routeVar (ActionRoute route) = ActionRoute check+    where check req = case reqPathLst req of+                        _:_ -> route $ popPath True req+                        _   -> return Nothing++-- | Match request with path \"/\".+routeTop :: Monad m => ActionRoute b m s -> ActionRoute b m s+routeTop (ActionRoute route) = ActionRoute $ \req ->+                               if null $ reqPathLst req then route req+                               else return Nothing++-- | Match requests with the given method (\"GET\", \"POST\", etc).+routeMethod :: Monad m => String -> ActionRoute b m s -> ActionRoute b m s+routeMethod method (ActionRoute route) = ActionRoute check+  where smethod = S.pack method+        check req | reqMethod req /= smethod = return Nothing+                  | otherwise = route req++-- | Routes an 'Action'+routeAction :: Monad m => Action t b m () -> ActionRoute b m t+routeAction action = ActionRoute $ const . return . Just $ action++-- | Routes an 'Action' to the given URL pattern. Patterns can include+-- directories as well as variable patterns (prefixed with @:@) to be passed+-- into the 'Action' as extra 'Param's. Some examples of URL patters:+--+--  * \/posts\/:id+--+--  * \/posts\/:id\/new+--+--  * \/:date\/posts\/:category\/new+--+routePattern :: Monad m => String -> ActionRoute b m t -> ActionRoute b m t+routePattern pattern action = foldl' addVar (routeActionWithRouteNames patternList action) patternList+  where patternList = reverse $ filter (not . null) $ splitOn "/" pattern+        addVar rt (':':_) = routeVar rt+        addVar rt name = routeName name rt++routeActionWithRouteNames :: Monad m+          => [String]+          -> ActionRoute b m s+          -> ActionRoute b m s+routeActionWithRouteNames routeNames (ActionRoute route) = ActionRoute $ \req -> do+  mact <- route req+  case mact of+    Just act -> return . Just $ do+      prms <- params+      _ <- setParams (prms ++ pathLstToParams req routeNames)+      act+    Nothing -> return Nothing++-- | Runs an 'Action' given an 'HttpReq' and body. Returns the 'HttpResp'+-- generated by the 'Action'.+runAction :: Monad m+          => Action s b m a+          -> HttpReq s+          -> b+          -> m (HttpResp m)+runAction action req body = do+  let s = ActionState transformedReq (mkHttpHead stat200) [] body+  (_, result) <- runStateT action s+  return $ actionResp result   where method = upcase $ reqMethod req-        overrideHeader = lookup "X-HTTP-Method-Override" (reqHeaders req)+        overrideHeader = lookup "x-http-method-override" (reqHeaders req)         transformedReq           | method /= "GET" && method /= "POST" = req           | isJust overrideHeader =               req{reqMethod = (upcase . fromJust $ overrideHeader)}           | otherwise = req++pathLstToParams :: HttpReq s -> [String] -> [Param]+pathLstToParams req routeNames = result+  where (result, _) = foldl go ([], reqPathParams req) routeNames+        go (prms, plst@(_:_)) (':':var) = ((transform var $ head plst):prms, tail plst)+        go s _ = s+        transform k v = Param (S.pack k) (L.fromChunks [v]) []++routeFileSys :: (String -> S.ByteString)+             -> FilePath+             -> ActionRoute b IO t+routeFileSys mimeMap root = ActionRoute $ \req -> do+  mresp <- check req+  case mresp of+    Just resp -> return $ Just $ modify $ \s -> s { actionResp = resp }+    _ -> return Nothing+  where check req = do +          let path = foldl (</>) root (map S.unpack $ reqPathLst req)+          exist <- liftIO $ fileExist path+          if exist then do+            st <- liftIO $ getFileStatus path+            case () of+              _ | isRegularFile st -> doFile req path st+                | otherwise -> return Nothing+            else return Nothing+        doFile req path st+            | reqMethod req == "HEAD" =+                return $ Just $ resp { respStatus = stat200 }+            | reqMethod req == "GET" = do+                return $ Just $+                  resp { respBody = enumFile' path }+            | otherwise = return Nothing+          where resp = defaultHttpResp { respChunk = False+                                       , respHeaders = mkHeaders req st }+        mkHeaders req st = +          [ ("Content-Length", S.pack . show $ fileSize st)+          , ("Content-Type", mimeMap $ fileExt req) ]+        fileExt req =+          drop 1 $ takeExtension $ case reqPathLst req of+                                     [] -> "."+                                     l  -> S.unpack $ last l 
+ Data/IterIO/Http/Support/Utils.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE CPP #-}+#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 702)+{-# LANGUAGE Safe #-}+#endif++module Data.IterIO.Http.Support.Utils where++import Data.ByteString.Lazy.Char8+import Data.IterIO+import Data.IterIO.Http+import Data.IterIO.Http.Support.Action++-- | For 'Action's where the body type is a 'ByteString', parse the+-- body with 'parseParams\'' and prepend the result to the 'Action''s+-- 'Param's+parseParams :: Monad m => Action t ByteString m [Param]+parseParams = do+  req <- getHttpReq+  body <- getBody+  prms0 <- params+  prms1 <- parseParams' req body+  setParams $ prms1 ++ prms0++-- | Parse url encoded or form encoded paramters from an HTTP+-- body.+parseParams' :: Monad m => HttpReq a -> ByteString -> m [Param]+parseParams' req body = inumPure body |$ foldForm req handle []+  where handle accm field = do+          val <- pureI+          return $ (Param (ffName field) val (ffHeaders field)):accm+
Examples/BasicSite.hs view
@@ -1,19 +1,20 @@ {-# LANGUAGE OverloadedStrings #-}  import Control.Monad.Trans-import qualified Data.ByteString.Lazy.Char8 as L-import Data.IterIO.HttpRoute+import qualified Data.ByteString.Lazy.Char8 as L8 import Data.IterIO.Server.TCPServer import Data.Monoid+import Data.Maybe import System.Random  import Text.Blaze.Renderer.Utf8 (renderHtml) import Text.Blaze.Html5 hiding (param)-import Text.Blaze.Html5.Attributes+import Text.Blaze.Html5.Attributes hiding (id, min, max) -import Data.IterIO.Http.Support.Action-import Data.IterIO.Http.Support.Responses+import Data.IterIO.Http.Support +type L = L8.ByteString+ main :: IO () main = runTCPServer httpServer @@ -31,13 +32,13 @@           , "Has, like, a ton of Facebook friends."           , "Always chooses the write homophone."] -httpServer = simpleHttpServer 8080 $ runHttpRoute $ mconcat [+httpServer = simpleHttpServer 8080 $ runIterActionRoute $ mconcat [     routeTop $ routeAction welcomeAction   , routeActionPattern "/factoid/:index" quoteAction   , routeActionPattern "/factoid" indexAction   ] -welcomeAction :: Action t IO ()+welcomeAction :: Action t L IO () welcomeAction = do   render "text/html" $ renderHtml $ docTypeHtml $ do     body $ do@@ -45,7 +46,7 @@       p $ do         "Click "; a ! href "/factoid" $ "here"; " to learn somethine about me." -indexAction :: Action t IO ()+indexAction :: Action t L IO () indexAction = do   idx <- lift $ getStdRandom (randomR (0,length quotes - 1))    let quote = quotes !! idx@@ -56,11 +57,13 @@         "Johnny Carson "         a ! href (toValue $ "/factoid/" ++ (show idx)) $ toHtml quote -quoteAction :: Action t IO ()+quoteAction :: Action t L IO () quoteAction = do-  (Just idx) <- param "index" -  let quote = quotes !! (read $ (L.unpack . paramValue) idx)+  (Just idx') <- param "index" +  let idx   = maybe 0 id $ maybeRead $ (L8.unpack . paramValue) idx'+      quote = quotes !! (max 0 (min idx (length quotes -1)))   render "text/html" $ renderHtml $ docTypeHtml $ do     body $ do       h1 $ do "Johnny Carson "; toHtml quote       p $ a ! href "/factoid" $ "Another random quote..."+    where maybeRead = fmap fst . listToMaybe . reads
iterio-server.cabal view
@@ -1,5 +1,5 @@ Name:           iterio-server-Version:        0.2+Version:        0.3 build-type:     Simple License:        BSD3 License-File:   LICENSE@@ -27,9 +27,11 @@                  ListLike >= 3 && < 4,                  monadIO >= 0.10 && < 0.20,                  mtl >= 2.0 && < 2.1,-                 iterIO >= 0.2 && < 0.3,+                 iterIO >= 0.2.2 && < 0.3,                  network >= 2.3 && < 2.4,                  split >= 0.1 && < 0.2,+                 unix >= 2.5 && < 2.6,+                 filepath >= 1.3 && < 1.4,                  transformers >= 0.2 && < 0.3    ghc-options: -Wall@@ -40,5 +42,6 @@     Data.IterIO.Http.Support.Responses     Data.IterIO.Http.Support.RestController     Data.IterIO.Http.Support.Routing+    Data.IterIO.Http.Support.Utils     Data.IterIO.Server.TCPServer