packages feed

iterio-server 0.0 → 0.1

raw patch · 7 files changed

+442/−24 lines, 7 filesdep +mtldep +splitdep +transformersPVP ok

version bump matches the API change (PVP)

Dependencies added: mtl, split, transformers

API changes (from Hackage documentation)

- Data.IterIO.Server.TCPServer: instance Show (TCPServer inp m out)
+ Data.IterIO.Http.Support.Action: Param :: ByteString -> ByteString -> [(ByteString, ByteString)] -> Param
+ Data.IterIO.Http.Support.Action: data Param
+ Data.IterIO.Http.Support.Action: destroySession :: Monad m => Action t m ()
+ Data.IterIO.Http.Support.Action: getHttpReq :: Monad m => Action t m (HttpReq t)
+ Data.IterIO.Http.Support.Action: param :: Monad m => ByteString -> Action t m (Maybe Param)
+ Data.IterIO.Http.Support.Action: paramHeaders :: Param -> [(ByteString, ByteString)]
+ Data.IterIO.Http.Support.Action: paramKey :: Param -> ByteString
+ Data.IterIO.Http.Support.Action: paramValue :: Param -> ByteString
+ Data.IterIO.Http.Support.Action: params :: Monad m => Action t m ([Param])
+ Data.IterIO.Http.Support.Action: requestHeader :: Monad m => ByteString -> Action t m (Maybe ByteString)
+ 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.Action: setSession :: Monad m => String -> Action t m ()
+ Data.IterIO.Http.Support.Action: type Action t m a = StateT (HttpReq t, HttpResp m, [Param]) m a
+ Data.IterIO.Http.Support.Responses: redirectTo :: Monad m => String -> Action t m ()
+ Data.IterIO.Http.Support.Responses: render :: Monad m => String -> ByteString -> Action t m ()
+ Data.IterIO.Http.Support.Responses: respond404 :: Monad m => Action t m ()
+ Data.IterIO.Http.Support.Responses: respondStat :: Monad m => HttpStatus -> Action t m ()
+ Data.IterIO.Http.Support.RestController: class RestController a
+ Data.IterIO.Http.Support.RestController: restCreate :: (RestController a, Monad m) => a -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restDestroy :: (RestController a, Monad m) => a -> ByteString -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restEdit :: (RestController a, Monad m) => a -> ByteString -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restIndex :: (RestController a, Monad m) => a -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restNew :: (RestController a, Monad m) => a -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restShow :: (RestController a, Monad m) => a -> ByteString -> Action t m ()
+ Data.IterIO.Http.Support.RestController: restUpdate :: (RestController a, Monad m) => a -> ByteString -> Action t m ()
+ Data.IterIO.Http.Support.RestController: routeRestController :: (Monad m, RestController a) => String -> a -> HttpRoute m t
+ Data.IterIO.Http.Support.Routing: runLHttpRoute :: Monad m => HttpRoute m s -> HttpReq s -> Iter ByteString m (HttpResp m)
+ Data.IterIO.Server.TCPServer: TCPServer :: PortNumber -> Inum inp inp m () -> (Socket -> m (Iter inp m (), Onum inp m ())) -> (m () -> IO ()) -> TCPServer inp m
+ Data.IterIO.Server.TCPServer: defaultServerAcceptor :: (ListLikeIO inp e, ChunkData inp, MonadIO m) => Socket -> m (Iter inp m (), Onum inp m a)
+ Data.IterIO.Server.TCPServer: instance Show (TCPServer inp m)
+ Data.IterIO.Server.TCPServer: minimalTCPServer :: (ListLikeIO inp e, ChunkData inp) => TCPServer inp IO
+ Data.IterIO.Server.TCPServer: serverAcceptor :: TCPServer inp m -> Socket -> m (Iter inp m (), Onum inp m ())
+ Data.IterIO.Server.TCPServer: serverHandler :: TCPServer inp m -> Inum inp inp m ()
+ Data.IterIO.Server.TCPServer: serverPort :: TCPServer inp m -> PortNumber
+ Data.IterIO.Server.TCPServer: serverResultHandler :: TCPServer inp m -> m () -> IO ()
- Data.IterIO.Server.TCPServer: data TCPServer inp m out
+ Data.IterIO.Server.TCPServer: data TCPServer inp m
- Data.IterIO.Server.TCPServer: echoServer :: HasFork m => PortNumber -> TCPServer String m String
+ Data.IterIO.Server.TCPServer: echoServer :: PortNumber -> TCPServer String IO
- Data.IterIO.Server.TCPServer: runTCPServer :: (ListLikeIO inp e, ListLikeIO out e, ChunkData inp, ChunkData out, HasFork m) => TCPServer inp m out -> m ()
+ Data.IterIO.Server.TCPServer: runTCPServer :: (ListLikeIO inp e, ChunkData inp, Monad m) => TCPServer inp m -> IO ()
- Data.IterIO.Server.TCPServer: simpleHttpServer :: HasFork m => PortNumber -> HttpRequestHandler m () -> TCPServer ByteString m ByteString
+ Data.IterIO.Server.TCPServer: simpleHttpServer :: PortNumber -> HttpRequestHandler IO () -> TCPServer ByteString IO

Files

+ Data/IterIO/Http/Support/Action.hs view
@@ -0,0 +1,125 @@+-- |Defines the 'Action' monad which abstracts some of the details of handling+-- HTTP requests with IterIO.+module Data.IterIO.Http.Support.Action (+    Action+  , Param(..)+  , routeAction+  , routeActionPattern+  , params+  , param+  , getHttpReq+  , setSession+  , destroySession+  , requestHeader+) where++import Control.Monad.Trans+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++-- | 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+}++-- | 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++-- |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))++-- |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))++-- |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 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))++-- | Returns a list of all 'Param's.+params :: Monad m => Action t m ([Param])+params = StateT $ \s@(_, _, prm) -> return (prm, s)++-- | 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+++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]) []++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
@@ -0,0 +1,45 @@+-- |Utility functions for responding to HTTP requests from within an 'Action'.+module Data.IterIO.Http.Support.Responses (+    render+  , redirectTo+  , respond404+  , respondStat+) where++import Control.Monad.Trans.State+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++-- | 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))++-- | 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))++-- | 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))++-- | 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 }
+ Data/IterIO/Http/Support/RestController.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE OverloadedStrings #-}+-- | This module defines the 'RestController' class+ +module Data.IterIO.Http.Support.RestController (+    RestController(..)+  , routeRestController+) where++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.Responses+import Data.Monoid++-- |The class @RestController@ allows a set of actions to be routed using+-- RESTful HTTP verbs.+class RestController a where+  -- |GET \/+  restIndex :: Monad m => a -> Action t m ()+  restIndex _ = respond404++  -- |GET \/:id+  --+  -- @id@ is passed in as the second parameter.+  restShow :: Monad m => a -> L.ByteString -> Action t m ()+  restShow _ _ = respond404++  -- |GET \/new+  restNew :: Monad m => a -> Action t m ()+  restNew _ = respond404++  -- |POST \/+  restCreate :: Monad m => a -> Action t m ()+  restCreate _ = respond404++  -- |GET \/:id\/edit+  --+  -- @id@ is passed in as the second parameter.+  restEdit :: Monad m => a -> L.ByteString -> Action t m ()+  restEdit _ _ = respond404++  -- |PUT \/:id+  --+  -- @id@ is passed in as the second parameter.+  --+  -- 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 :: Monad m => a -> L.ByteString -> Action t m ()+  restUpdate _ _ = respond404++  -- |DELETE \/:id+  -- +  -- @id@ is passed in as the second parameter.+  --+  -- 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 :: Monad m => a -> L.ByteString -> Action t m ()+  restDestroy _ _ = respond404++-- |Runs an action, passing in named parameter.+runWithVar :: Monad m => S.ByteString -> (L.ByteString -> Action t m ()) -> Action t m ()+runWithVar varName controller = do+  (Just var) <- param varName+  controller $ paramValue var++-- |Routes URLs under the given @String@ to actions in a @RestController@. For+-- example+--+-- @+--    routeRestController "posts" myRestController+-- @+--+-- will map the follwoing URLs:+--+--    * GET \/posts => myRestController#restIndex+--+--    * POST \/posts => myRestController#restCreate+--+--    * GET \/posts\/:id => myRestController#restShow+--+--    * GET \/posts\/:id\/edit => myRestController#restEdit+--+--    * GET \/posts\/:id\/new => myRestController#restNew+--+--    * DELETE \/posts\/:id => myRestController#restDestroy+--+--    * PUT \/posts\/:id => myRestController#restUpdate+--+routeRestController :: (Monad m, RestController a) => String -> a -> HttpRoute m t+routeRestController prefix controller = routeName 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+  ]+
+ Data/IterIO/Http/Support/Routing.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE OverloadedStrings #-}+-- |Utility functions for routing.+module Data.IterIO.Http.Support.Routing (+    runLHttpRoute+    ) where++import Data.Char+import qualified Data.ByteString.Lazy.Char8 as L+import qualified Data.ByteString.Char8 as S+import Data.Maybe+import Data.IterIO.Http+import Data.IterIO.HttpRoute+import Data.IterIO.Iter++-- |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+              -> HttpReq s+              -> Iter L.ByteString m (HttpResp m)+runLHttpRoute route req = runHttpRoute route $ transformedReq+  where method = upcase $ reqMethod req+        overrideHeader = lookup "X-HTTP-Method-Override" (reqHeaders req)+        transformedReq+          | method /= "GET" && method /= "POST" = req+          | isJust overrideHeader =+              req{reqMethod = (upcase . fromJust $ overrideHeader)}+          | otherwise = req+
Data/IterIO/Server/TCPServer.hs view
@@ -1,7 +1,9 @@ -- |Generic building blocks for creating TCP Servers based on 'IterIO' module Data.IterIO.Server.TCPServer (-  TCPServer,+  TCPServer(..),   runTCPServer,+  defaultServerAcceptor,+  minimalTCPServer,   simpleHttpServer,   echoServer ) where@@ -15,6 +17,7 @@ import Data.IterIO.Http import Data.ListLike.IO +-- |Sets up a TCP socket to listen on the given port. sockListenTCP :: Net.PortNumber -> IO Net.Socket sockListenTCP pn = do   sock <- Net.socket Net.AF_INET Net.Stream Net.defaultProtocol@@ -25,44 +28,75 @@  -- |'TCPServer' holds all the information necessary to run -- bind to a sock and respond to TCP requests from the network.-data TCPServer inp m out = TCPServer {+data TCPServer inp m = TCPServer {+-- |The TCP port the server will listen for incomming connections on.     serverPort :: Net.PortNumber-  , serverHandler :: Inum inp out m ()+-- |This 'Inum' implements the actual functionality of the server. The input+--  and output of the 'Inum' correspond to the input and output of the socket.+  , serverHandler :: Inum inp inp m ()+-- |A function to transform an accept incomming connection into an iter and onum.+--  Most servers should just use 'defaultSocketAcceptor' but this can be used for+--  special cases, e.g. accepting SSL connections with 'Data.IterIO.SSL.iterSSL'.+  , serverAcceptor :: Net.Socket -> m (Iter inp m (), Onum inp m ())+-- |Must execute the monadic result. Servers operating in the 'IO' Monad can+--  use 'id'.+  , serverResultHandler :: m () -> IO () } -instance Show (TCPServer inp m out) where+instance Show (TCPServer inp m) where   show s = "TCPServer { serverPort: " ++ (show $ serverPort s) ++ " }" +-- |For convenience, a TCPServer in the 'IO' Monad with null defaults:+--+--    * Port 0 (next availabel port)+--+--    * Handler set to 'inumNop'+--+--    * Acceptor set to 'defaultServerAcceptor'+--+--    * Request handler set to 'id' (noop)+--+minimalTCPServer :: (ListLikeIO inp e, ChunkData inp) => TCPServer inp IO+minimalTCPServer = TCPServer 0 inumNop defaultServerAcceptor id++-- |This acceptor creates an 'Iter' and 'Onum' using 'handleI' and+--  'enumHandle' respectively.+defaultServerAcceptor ::  (ListLikeIO inp e,+                           ChunkData inp, MonadIO m)+                      => Net.Socket -> m (Iter inp m (), Onum inp m a)+defaultServerAcceptor sock = liftIO $ do+  h <- Net.socketToHandle sock ReadWriteMode+  hSetBuffering h NoBuffering+  return (handleI h, enumHandle h)+ -- |Runs a 'TCPServer' in a loop.-runTCPServer :: (ListLikeIO inp e, ListLikeIO out e,-                 ChunkData inp, ChunkData out, HasFork m)-              => TCPServer inp m out-              -> m ()+runTCPServer :: (ListLikeIO inp e,+                 ChunkData inp, Monad m)+              => TCPServer inp m+              -> IO () runTCPServer server = do-  sock <- liftIO $ sockListenTCP $ serverPort server+  sock <- sockListenTCP $ serverPort server+  let handler = serverResultHandler server   forever $ do-    (iter, enum) <- liftIO $ do-      (s, _) <- Net.accept sock-      h <- Net.socketToHandle s ReadWriteMode-      hSetBuffering h NoBuffering-      return (handleI h, enumHandle h)-    _ <- fork $ do+    (s, _) <- Net.accept sock+    _ <- forkIO $ handler $ do+      (iter, enum) <- (serverAcceptor server) s       enum |$ serverHandler server .| iter     return ()  -- |Creates a simple HTTP server from an 'HTTPRequestHandler'.-simpleHttpServer :: (HasFork m)-                =>  Net.PortNumber-                ->  HttpRequestHandler m ()-                -> TCPServer L.ByteString m L.ByteString-simpleHttpServer port reqHandler = TCPServer port httpAppHandler+simpleHttpServer :: Net.PortNumber+                 -> HttpRequestHandler IO ()+                 -> TCPServer L.ByteString IO+simpleHttpServer port reqHandler = minimalTCPServer { serverPort = port, serverHandler = httpAppHandler }   where httpAppHandler = mkInumM $ do           req <- httpReqI           resp <- liftI $ reqHandler req           irun $ enumHttpResp resp Nothing+ -- |Creates a 'TCPServer' that echoes each line from the client until EOF.-echoServer :: (HasFork m) => Net.PortNumber -> TCPServer String m String-echoServer port = TCPServer port echoAppHandler+echoServer :: Net.PortNumber -> TCPServer String IO+echoServer port = minimalTCPServer { serverPort = port, serverHandler = echoAppHandler }   where echoAppHandler = mkInumM $ forever $ do           input <- safeLineI           case input of
+ Examples/BasicSite.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE OverloadedStrings #-}++import Control.Monad.Trans+import qualified Data.ByteString.Lazy.Char8 as L+import Data.IterIO.HttpRoute+import Data.IterIO.Server.TCPServer+import Data.Monoid+import System.Random++import Text.Blaze.Renderer.Utf8 (renderHtml)+import Text.Blaze.Html5 hiding (param)+import Text.Blaze.Html5.Attributes++import Data.IterIO.Http.Support.Action+import Data.IterIO.Http.Support.Responses++main :: IO ()+main = runTCPServer httpServer++quotes :: [String]+quotes = [  +            "Now with more cowbell."+          , "Hot potato grand champion."+          , "Goes all the way to eleven."+          , "Does the Kessel Run in less than 12 parsecs."+          , "Will work for hugs."+          , "Ate a whole wheel of cheese."+          , "Fueled by Mountain Dew"+          , "Saw a MiG 28 do a 4g negative dive."+          , "Has never started a land war in Asia."+          , "Has, like, a ton of Facebook friends."+          , "Always chooses the write homophone."]++httpServer = simpleHttpServer 8080 $ runHttpRoute $ mconcat [+    routeTop $ routeAction welcomeAction+  , routeActionPattern "/factoid/:index" quoteAction+  , routeActionPattern "/factoid" indexAction+  ]++welcomeAction :: Action t IO ()+welcomeAction = do+  render "text/html" $ renderHtml $ docTypeHtml $ do+    body $ do+      h1 "Welcome to my simple site!"+      p $ do+        "Click "; a ! href "/factoid" $ "here"; " to learn somethine about me."++indexAction :: Action t IO ()+indexAction = do+  idx <- liftIO $ getStdRandom (randomR (0,length quotes - 1)) +  let quote = quotes !! idx+  render "text/html" $ renderHtml $ docTypeHtml $ do+    body $ do+      h1 "A Random quote:"+      p $ do+        "Johnny Carson "+        a ! href (toValue $ "/factoid/" ++ (show idx)) $ toHtml quote++quoteAction :: Action t IO ()+quoteAction = do+  (Just idx) <- param "index" +  let quote = quotes !! (read $ (L.unpack . paramValue) idx)+  render "text/html" $ renderHtml $ docTypeHtml $ do+    body $ do+      h1 $ do "Johnny Carson "; toHtml quote+      p $ a ! href "/factoid" $ "Another random quote..."
iterio-server.cabal view
@@ -1,5 +1,5 @@ Name:           iterio-server-Version:        0.0+Version:        0.1 build-type:     Simple License:        BSD3 License-File:   LICENSE@@ -11,6 +11,9 @@ Category:       Network, Enumerator Cabal-Version:  >= 1.6 +Extra-source-files:+  Examples/BasicSite.hs+ Description:   This module contains a set of generic building blocks for building servers with IterIO. @@ -23,10 +26,18 @@                  bytestring >= 0.9 && < 1,                  ListLike >= 3 && < 4,                  monadIO >= 0.10 && < 0.20,+                 mtl >= 2.0 && < 2.1,                  iterIO >= 0.2 && < 0.3,-                 network >= 2.3 && < 2.4+                 network >= 2.3 && < 2.4,+                 split >= 0.1 && < 0.2,+                 transformers >= 0.2 && < 0.3    ghc-options: -Wall    Exposed-modules:+    Data.IterIO.Http.Support.Action+    Data.IterIO.Http.Support.Responses+    Data.IterIO.Http.Support.RestController+    Data.IterIO.Http.Support.Routing     Data.IterIO.Server.TCPServer+