Began major refactoring of code
This commit is contained in:
parent
3265d7a717
commit
26ad604a19
@ -32,15 +32,34 @@ import System.Environment (getEnvironment)
|
|||||||
|
|
||||||
import qualified Data.ByteString.Char8 as B
|
import qualified Data.ByteString.Char8 as B
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Web.Encodings (parseHttpAccept)
|
import Web.Encodings
|
||||||
import Web.Mime
|
import Web.Mime
|
||||||
import Data.List (intercalate)
|
import Data.List (intercalate)
|
||||||
import Web.Routes (encodePathInfo, decodePathInfo)
|
import Web.Routes (encodePathInfo, decodePathInfo)
|
||||||
|
|
||||||
mkYesod :: String -> [Resource] -> Q [Dec]
|
import Control.Concurrent.MVar
|
||||||
|
import Control.Arrow ((***))
|
||||||
|
import Data.Convertible.Text (cs)
|
||||||
|
|
||||||
|
import Data.Time.Clock
|
||||||
|
|
||||||
|
-- | Generates URL datatype and site function for the given 'Resource's. This
|
||||||
|
-- is used for creating sites, *not* subsites. See 'mkYesodSub' for the latter.
|
||||||
|
-- Use 'parseRoutes' in generate to create the 'Resource's.
|
||||||
|
mkYesod :: String -- ^ name of the argument datatype
|
||||||
|
-> [Resource]
|
||||||
|
-> Q [Dec]
|
||||||
mkYesod name = mkYesodGeneral name [] False
|
mkYesod name = mkYesodGeneral name [] False
|
||||||
|
|
||||||
mkYesodSub :: String -> [Name] -> [Resource] -> Q [Dec]
|
-- | Generates URL datatype and site function for the given 'Resource's. This
|
||||||
|
-- is used for creating subsites, *not* sites. See 'mkYesod' for the latter.
|
||||||
|
-- Use 'parseRoutes' in generate to create the 'Resource's. In general, a
|
||||||
|
-- subsite is not executable by itself, but instead provides functionality to
|
||||||
|
-- be embedded in other sites.
|
||||||
|
mkYesodSub :: String -- ^ name of the argument datatype
|
||||||
|
-> [Name] -- ^ a list of classes the master datatype must be an instance of
|
||||||
|
-> [Resource]
|
||||||
|
-> Q [Dec]
|
||||||
mkYesodSub name clazzes = mkYesodGeneral name clazzes True
|
mkYesodSub name clazzes = mkYesodGeneral name clazzes True
|
||||||
|
|
||||||
explodeHandler :: HasReps c
|
explodeHandler :: HasReps c
|
||||||
@ -74,6 +93,8 @@ mkYesodGeneral name clazzes isSub res = do
|
|||||||
}
|
}
|
||||||
return $ (if isSub then id else (:) yes) [w, x, y, z]
|
return $ (if isSub then id else (:) yes) [w, x, y, z]
|
||||||
|
|
||||||
|
-- | Convert the given argument into a WAI application, executable with any WAI
|
||||||
|
-- handler. You can use 'basicHandler' if you wish.
|
||||||
toWaiApp :: Yesod y => y -> IO W.Application
|
toWaiApp :: Yesod y => y -> IO W.Application
|
||||||
toWaiApp a = do
|
toWaiApp a = do
|
||||||
key' <- encryptKey a
|
key' <- encryptKey a
|
||||||
@ -82,7 +103,7 @@ toWaiApp a = do
|
|||||||
$ jsonp
|
$ jsonp
|
||||||
$ methodOverride
|
$ methodOverride
|
||||||
$ cleanPath
|
$ cleanPath
|
||||||
$ \thePath -> clientsession encryptedCookies key' mins
|
$ \thePath -> clientsession encryptedCookies key' mins -- FIXME allow user input for encryptedCookies
|
||||||
$ toWaiApp' a thePath
|
$ toWaiApp' a thePath
|
||||||
|
|
||||||
toWaiApp' :: Yesod y
|
toWaiApp' :: Yesod y
|
||||||
@ -91,7 +112,7 @@ toWaiApp' :: Yesod y
|
|||||||
-> [(B.ByteString, B.ByteString)]
|
-> [(B.ByteString, B.ByteString)]
|
||||||
-> W.Request
|
-> W.Request
|
||||||
-> IO W.Response
|
-> IO W.Response
|
||||||
toWaiApp' y resource session env = do
|
toWaiApp' y resource session' env = do
|
||||||
let site = getSite
|
let site = getSite
|
||||||
method = B.unpack $ W.methodToBS $ W.requestMethod env
|
method = B.unpack $ W.methodToBS $ W.requestMethod env
|
||||||
types = httpAccept env
|
types = httpAccept env
|
||||||
@ -99,7 +120,7 @@ toWaiApp' y resource session env = do
|
|||||||
eurl = quasiParse site pathSegments
|
eurl = quasiParse site pathSegments
|
||||||
render u = approot y ++ '/'
|
render u = approot y ++ '/'
|
||||||
: encodePathInfo (fixSegs $ quasiRender site u)
|
: encodePathInfo (fixSegs $ quasiRender site u)
|
||||||
rr <- parseWaiRequest env session
|
rr <- parseWaiRequest env session'
|
||||||
onRequest y rr
|
onRequest y rr
|
||||||
print pathSegments -- FIXME remove
|
print pathSegments -- FIXME remove
|
||||||
let ya = case eurl of
|
let ya = case eurl of
|
||||||
@ -153,3 +174,62 @@ fixSegs [x]
|
|||||||
| any (== '.') x = [x]
|
| any (== '.') x = [x]
|
||||||
| otherwise = [x, ""] -- append trailing slash
|
| otherwise = [x, ""] -- append trailing slash
|
||||||
fixSegs (x:xs) = x : fixSegs xs
|
fixSegs (x:xs) = x : fixSegs xs
|
||||||
|
|
||||||
|
parseWaiRequest :: W.Request
|
||||||
|
-> [(B.ByteString, B.ByteString)] -- ^ session
|
||||||
|
-> IO Request
|
||||||
|
parseWaiRequest env session' = do
|
||||||
|
let gets' = map (cs *** cs) $ decodeUrlPairs $ W.queryString env
|
||||||
|
let reqCookie = fromMaybe B.empty $ lookup W.Cookie $ W.requestHeaders env
|
||||||
|
cookies' = map (cs *** cs) $ parseCookies reqCookie
|
||||||
|
acceptLang = lookup W.AcceptLanguage $ W.requestHeaders env
|
||||||
|
langs = map cs $ maybe [] parseHttpAccept acceptLang
|
||||||
|
langs' = case lookup langKey cookies' of
|
||||||
|
Nothing -> langs
|
||||||
|
Just x -> x : langs
|
||||||
|
langs'' = case lookup langKey gets' of
|
||||||
|
Nothing -> langs'
|
||||||
|
Just x -> x : langs'
|
||||||
|
session'' = map (cs *** cs) session'
|
||||||
|
rbthunk <- iothunk $ rbHelper env
|
||||||
|
return $ Request gets' cookies' session'' rbthunk env langs''
|
||||||
|
|
||||||
|
rbHelper :: W.Request -> IO RequestBodyContents
|
||||||
|
rbHelper = fmap (fix1 *** map fix2) . parseRequestBody lbsSink where
|
||||||
|
fix1 = map (cs *** cs)
|
||||||
|
fix2 (x, FileInfo a b c) = (cs x, FileInfo (cs a) (cs b) c)
|
||||||
|
|
||||||
|
-- | Produces a \"compute on demand\" value. The computation will be run once
|
||||||
|
-- it is requested, and then the result will be stored. This will happen only
|
||||||
|
-- once.
|
||||||
|
iothunk :: IO a -> IO (IO a)
|
||||||
|
iothunk = fmap go . newMVar . Left where
|
||||||
|
go :: MVar (Either (IO a) a) -> IO a
|
||||||
|
go mvar = modifyMVar mvar go'
|
||||||
|
go' :: Either (IO a) a -> IO (Either (IO a) a, a)
|
||||||
|
go' (Right val) = return (Right val, val)
|
||||||
|
go' (Left comp) = do
|
||||||
|
val <- comp
|
||||||
|
return (Right val, val)
|
||||||
|
|
||||||
|
responseToWaiResponse :: (W.Status, [Header], ContentType, Content)
|
||||||
|
-> IO W.Response
|
||||||
|
responseToWaiResponse (sc, hs, ct, c) = do
|
||||||
|
hs' <- mapM headerToPair hs
|
||||||
|
let hs'' = (W.ContentType, cs $ contentTypeToString ct) : hs'
|
||||||
|
return $ W.Response sc hs'' $ case c of
|
||||||
|
ContentFile fp -> Left fp
|
||||||
|
ContentEnum e -> Right $ W.Enumerator e
|
||||||
|
|
||||||
|
-- | Convert Header to a key/value pair.
|
||||||
|
headerToPair :: Header -> IO (W.ResponseHeader, B.ByteString)
|
||||||
|
headerToPair (AddCookie minutes key value) = do
|
||||||
|
now <- getCurrentTime
|
||||||
|
let expires = addUTCTime (fromIntegral $ minutes * 60) now
|
||||||
|
return (W.SetCookie, cs $ key ++ "=" ++ value ++"; path=/; expires="
|
||||||
|
++ formatW3 expires)
|
||||||
|
headerToPair (DeleteCookie key) = return
|
||||||
|
(W.SetCookie, cs $
|
||||||
|
key ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
|
||||||
|
headerToPair (Header key value) =
|
||||||
|
return (W.responseHeaderFromBS $ cs key, cs value)
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
{-# LANGUAGE TypeSynonymInstances #-}
|
{-# LANGUAGE TypeSynonymInstances #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
module Yesod.Hamlet
|
module Yesod.Hamlet
|
||||||
( hamletToContent
|
( -- * Hamlet library
|
||||||
, hamletToRepHtml
|
Hamlet
|
||||||
, PageContent (..)
|
|
||||||
, Hamlet
|
|
||||||
, hamlet
|
, hamlet
|
||||||
, HtmlContent (..)
|
, HtmlContent (..)
|
||||||
|
-- * Convert to something displayable
|
||||||
|
, hamletToContent
|
||||||
|
, hamletToRepHtml
|
||||||
|
-- * Page templates
|
||||||
|
, PageContent (..)
|
||||||
|
-- * data-object
|
||||||
, HtmlObject
|
, HtmlObject
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
@ -22,12 +27,19 @@ import Data.Convertible.Text
|
|||||||
import Data.Object
|
import Data.Object
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
|
|
||||||
|
-- | Content for a web page. By providing this datatype, we can easily create
|
||||||
|
-- generic site templates, which would have the type signature:
|
||||||
|
--
|
||||||
|
-- > PageContent url -> Hamlet url IO ()
|
||||||
data PageContent url = PageContent
|
data PageContent url = PageContent
|
||||||
{ pageTitle :: HtmlContent
|
{ pageTitle :: HtmlContent
|
||||||
, pageHead :: Hamlet url IO ()
|
, pageHead :: Hamlet url IO ()
|
||||||
, pageBody :: Hamlet url IO ()
|
, pageBody :: Hamlet url IO ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- FIXME some typeclasses for the stuff below?
|
||||||
|
-- | Converts the given Hamlet template into 'Content', which can be used in a
|
||||||
|
-- Yesod 'Response'.
|
||||||
hamletToContent :: Hamlet (Routes sub) IO () -> GHandler sub master Content
|
hamletToContent :: Hamlet (Routes sub) IO () -> GHandler sub master Content
|
||||||
hamletToContent h = do
|
hamletToContent h = do
|
||||||
render <- getUrlRender
|
render <- getUrlRender
|
||||||
@ -40,16 +52,9 @@ hamletToContent h = do
|
|||||||
Right ((), x) -> return $ Right x
|
Right ((), x) -> return $ Right x
|
||||||
iter' iter seed text = iter seed $ cs text
|
iter' iter seed text = iter seed $ cs text
|
||||||
|
|
||||||
hamletToRepHtml :: Hamlet (Routes y) IO () -> Handler y RepHtml
|
-- | Wraps the 'Content' generated by 'hamletToContent' in a 'RepHtml'.
|
||||||
hamletToRepHtml h = do
|
hamletToRepHtml :: Hamlet (Routes sub) IO () -> GHandler sub master RepHtml
|
||||||
c <- hamletToContent h
|
hamletToRepHtml = fmap RepHtml . hamletToContent
|
||||||
return $ RepHtml c
|
|
||||||
|
|
||||||
-- FIXME some type of JSON combined output...
|
|
||||||
--hamletToRepHtmlJson :: x
|
|
||||||
-- -> (x -> Hamlet (Routes y) IO ())
|
|
||||||
-- -> (x -> Json)
|
|
||||||
-- -> Handler y RepHtmlJson
|
|
||||||
|
|
||||||
instance Monad m => ConvertSuccess String (Hamlet url m ()) where
|
instance Monad m => ConvertSuccess String (Hamlet url m ()) where
|
||||||
convertSuccess = outputHtml . Unencoded . cs
|
convertSuccess = outputHtml . Unencoded . cs
|
||||||
|
|||||||
@ -78,7 +78,7 @@ newtype YesodApp = YesodApp
|
|||||||
:: (ErrorResponse -> YesodApp)
|
:: (ErrorResponse -> YesodApp)
|
||||||
-> Request
|
-> Request
|
||||||
-> [ContentType]
|
-> [ContentType]
|
||||||
-> IO Response
|
-> IO (W.Status, [Header], ContentType, Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
------ Handler monad
|
------ Handler monad
|
||||||
@ -164,28 +164,28 @@ runHandler handler mrender sroute tomr ma tosa = YesodApp $ \eh rr cts -> do
|
|||||||
})
|
})
|
||||||
(\e -> return ([], HCError $ toErrorHandler e))
|
(\e -> return ([], HCError $ toErrorHandler e))
|
||||||
let handleError e = do
|
let handleError e = do
|
||||||
Response _ hs ct c <- unYesodApp (eh e) safeEh rr cts
|
(_, hs, ct, c) <- unYesodApp (eh e) safeEh rr cts
|
||||||
let hs' = headers ++ hs
|
let hs' = headers ++ hs
|
||||||
return $ Response (getStatus e) hs' ct c
|
return $ (getStatus e, hs', ct, c)
|
||||||
let sendFile' ct fp = do
|
let sendFile' ct fp = do
|
||||||
c <- BL.readFile fp
|
c <- BL.readFile fp
|
||||||
return $ Response W.Status200 headers ct $ cs c
|
return (W.Status200, headers, ct, cs c)
|
||||||
case contents of
|
case contents of
|
||||||
HCError e -> handleError e
|
HCError e -> handleError e
|
||||||
HCSpecial (Redirect rt loc) -> do
|
HCSpecial (Redirect rt loc) -> do
|
||||||
let hs = Header "Location" loc : headers
|
let hs = Header "Location" loc : headers
|
||||||
return $ Response (getRedirectStatus rt) hs TypePlain $ cs ""
|
return (getRedirectStatus rt, hs, TypePlain, cs "")
|
||||||
HCSpecial (SendFile ct fp) -> Control.Exception.catch
|
HCSpecial (SendFile ct fp) -> Control.Exception.catch
|
||||||
(sendFile' ct fp)
|
(sendFile' ct fp)
|
||||||
(handleError . toErrorHandler)
|
(handleError . toErrorHandler)
|
||||||
HCContent a -> do
|
HCContent a -> do
|
||||||
(ct, c) <- chooseRep a cts
|
(ct, c) <- chooseRep a cts
|
||||||
return $ Response W.Status200 headers ct c
|
return (W.Status200, headers, ct, c)
|
||||||
|
|
||||||
safeEh :: ErrorResponse -> YesodApp
|
safeEh :: ErrorResponse -> YesodApp
|
||||||
safeEh er = YesodApp $ \_ _ _ -> do
|
safeEh er = YesodApp $ \_ _ _ -> do
|
||||||
liftIO $ hPutStrLn stderr $ "Error handler errored out: " ++ show er
|
liftIO $ hPutStrLn stderr $ "Error handler errored out: " ++ show er
|
||||||
return $ Response W.Status500 [] TypePlain $ cs "Internal Server Error"
|
return (W.Status500, [], TypePlain, cs "Internal Server Error")
|
||||||
|
|
||||||
------ Special handlers
|
------ Special handlers
|
||||||
specialResponse :: SpecialResponse -> GHandler sub master a
|
specialResponse :: SpecialResponse -> GHandler sub master a
|
||||||
@ -231,3 +231,15 @@ header a = addHeader . Header a
|
|||||||
|
|
||||||
addHeader :: Header -> GHandler sub master ()
|
addHeader :: Header -> GHandler sub master ()
|
||||||
addHeader h = Handler $ \_ -> return ([h], HCContent ())
|
addHeader h = Handler $ \_ -> return ([h], HCContent ())
|
||||||
|
|
||||||
|
getStatus :: ErrorResponse -> W.Status
|
||||||
|
getStatus NotFound = W.Status404
|
||||||
|
getStatus (InternalError _) = W.Status500
|
||||||
|
getStatus (InvalidArgs _) = W.Status400
|
||||||
|
getStatus PermissionDenied = W.Status403
|
||||||
|
getStatus (BadMethod _) = W.Status405
|
||||||
|
|
||||||
|
getRedirectStatus :: RedirectType -> W.Status
|
||||||
|
getRedirectStatus RedirectPermanent = W.Status301
|
||||||
|
getRedirectStatus RedirectTemporary = W.Status302
|
||||||
|
getRedirectStatus RedirectSeeOther = W.Status303
|
||||||
|
|||||||
@ -36,12 +36,10 @@ import Yesod
|
|||||||
import Data.Convertible.Text
|
import Data.Convertible.Text
|
||||||
|
|
||||||
import Control.Monad.Attempt
|
import Control.Monad.Attempt
|
||||||
import qualified Data.ByteString.Char8 as B8
|
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
|
||||||
import Data.Typeable (Typeable)
|
import Data.Typeable (Typeable)
|
||||||
import Control.Exception (Exception)
|
import Control.Exception (Exception)
|
||||||
import Control.Applicative ((<$>))
|
|
||||||
|
|
||||||
-- FIXME check referer header to determine destination
|
-- FIXME check referer header to determine destination
|
||||||
|
|
||||||
@ -189,16 +187,16 @@ getLogout = do
|
|||||||
redirectToDest RedirectTemporary $ defaultDest y
|
redirectToDest RedirectTemporary $ defaultDest y
|
||||||
|
|
||||||
-- | Gets the identifier for a user if available.
|
-- | Gets the identifier for a user if available.
|
||||||
maybeIdentifier :: (Functor m, Monad m, RequestReader m) => m (Maybe String)
|
maybeIdentifier :: RequestReader m => m (Maybe String)
|
||||||
maybeIdentifier =
|
maybeIdentifier = do
|
||||||
fmap cs . lookup (B8.pack authCookieName) . reqSession
|
s <- session
|
||||||
<$> getRequest
|
return $ listToMaybe $ s authCookieName
|
||||||
|
|
||||||
-- | Gets the display name for a user if available.
|
-- | Gets the display name for a user if available.
|
||||||
displayName :: (Functor m, Monad m, RequestReader m) => m (Maybe String)
|
displayName :: RequestReader m => m (Maybe String)
|
||||||
displayName = do
|
displayName = do
|
||||||
rr <- getRequest
|
s <- session
|
||||||
return $ fmap cs $ lookup (B8.pack authDisplayName) $ reqSession rr
|
return $ listToMaybe $ s authDisplayName
|
||||||
|
|
||||||
-- | Gets the identifier for a user. If user is not logged in, redirects them
|
-- | Gets the identifier for a user. If user is not logged in, redirects them
|
||||||
-- to the login page.
|
-- to the login page.
|
||||||
|
|||||||
117
Yesod/Request.hs
117
Yesod/Request.hs
@ -1,10 +1,5 @@
|
|||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
||||||
{-# LANGUAGE TypeSynonymInstances #-}
|
|
||||||
{-# LANGUAGE DeriveDataTypeable #-}
|
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
{-# LANGUAGE PackageImports #-}
|
{-# LANGUAGE PackageImports #-}
|
||||||
{-# LANGUAGE NoMonomorphismRestriction #-}
|
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
--
|
--
|
||||||
-- Module : Yesod.Request
|
-- Module : Yesod.Request
|
||||||
@ -15,75 +10,73 @@
|
|||||||
-- Stability : Stable
|
-- Stability : Stable
|
||||||
-- Portability : portable
|
-- Portability : portable
|
||||||
--
|
--
|
||||||
-- Code for extracting parameters from requests.
|
-- | Provides a parsed version of the raw 'W.Request' data.
|
||||||
--
|
--
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
module Yesod.Request
|
module Yesod.Request
|
||||||
(
|
(
|
||||||
-- * Request
|
-- * Request datatype
|
||||||
Request (..)
|
RequestBodyContents
|
||||||
|
, Request (..)
|
||||||
, RequestReader (..)
|
, RequestReader (..)
|
||||||
|
-- * Convenience functions
|
||||||
, waiRequest
|
, waiRequest
|
||||||
, cookies
|
, languages
|
||||||
|
-- * Lookup parameters
|
||||||
, getParams
|
, getParams
|
||||||
, postParams
|
, postParams
|
||||||
, languages
|
, cookies
|
||||||
, parseWaiRequest
|
, session
|
||||||
-- * Parameter
|
-- * Parameter type synonyms
|
||||||
, ParamName
|
, ParamName
|
||||||
, ParamValue
|
, ParamValue
|
||||||
, ParamError
|
, ParamError
|
||||||
#if TEST
|
|
||||||
, testSuite
|
|
||||||
#endif
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Network.Wai as W
|
import qualified Network.Wai as W
|
||||||
import Yesod.Definitions
|
import Yesod.Definitions
|
||||||
import Web.Encodings
|
import Web.Encodings
|
||||||
import qualified Data.ByteString as B
|
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import Data.Convertible.Text
|
|
||||||
import Control.Arrow ((***))
|
|
||||||
import Data.Maybe (fromMaybe)
|
|
||||||
import "transformers" Control.Monad.IO.Class
|
import "transformers" Control.Monad.IO.Class
|
||||||
import Control.Concurrent.MVar
|
|
||||||
import Control.Monad (liftM)
|
import Control.Monad (liftM)
|
||||||
|
|
||||||
#if TEST
|
|
||||||
import Test.Framework (testGroup, Test)
|
|
||||||
--import Test.Framework.Providers.HUnit
|
|
||||||
--import Test.HUnit hiding (Test)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
type ParamName = String
|
type ParamName = String
|
||||||
type ParamValue = String
|
type ParamValue = String
|
||||||
type ParamError = String
|
type ParamError = String
|
||||||
|
|
||||||
|
-- | The reader monad specialized for 'Request'.
|
||||||
class Monad m => RequestReader m where
|
class Monad m => RequestReader m where
|
||||||
getRequest :: m Request
|
getRequest :: m Request
|
||||||
instance RequestReader ((->) Request) where
|
instance RequestReader ((->) Request) where
|
||||||
getRequest = id
|
getRequest = id
|
||||||
|
|
||||||
languages :: (Functor m, RequestReader m) => m [Language]
|
-- | Get the list of supported languages supplied by the user.
|
||||||
languages = reqLangs `fmap` getRequest
|
languages :: RequestReader m => m [Language]
|
||||||
|
languages = reqLangs `liftM` getRequest
|
||||||
|
|
||||||
-- | Get the req 'W.Request' value.
|
-- | Get the request\'s 'W.Request' value.
|
||||||
waiRequest :: RequestReader m => m W.Request
|
waiRequest :: RequestReader m => m W.Request
|
||||||
waiRequest = reqWaiRequest `liftM` getRequest
|
waiRequest = reqWaiRequest `liftM` getRequest
|
||||||
|
|
||||||
|
-- | A tuple containing both the POST parameters and submitted files.
|
||||||
type RequestBodyContents =
|
type RequestBodyContents =
|
||||||
( [(ParamName, ParamValue)]
|
( [(ParamName, ParamValue)]
|
||||||
, [(ParamName, FileInfo String BL.ByteString)]
|
, [(ParamName, FileInfo String BL.ByteString)]
|
||||||
)
|
)
|
||||||
|
|
||||||
-- | The req information passed through W, cleaned up a bit.
|
-- | The parsed request information.
|
||||||
data Request = Request
|
data Request = Request
|
||||||
{ reqGetParams :: [(ParamName, ParamValue)]
|
{ reqGetParams :: [(ParamName, ParamValue)]
|
||||||
, reqCookies :: [(ParamName, ParamValue)]
|
, reqCookies :: [(ParamName, ParamValue)]
|
||||||
, reqSession :: [(B.ByteString, B.ByteString)]
|
-- | Session data stored in a cookie via the clientsession package. FIXME explain how to extend.
|
||||||
|
, reqSession :: [(ParamName, ParamValue)]
|
||||||
|
-- | The POST parameters and submitted files. This is stored in an IO
|
||||||
|
-- thunk, which essentially means it will be computed once at most, but
|
||||||
|
-- only if requested. This allows avoidance of the potentially costly
|
||||||
|
-- parsing of POST bodies for pages which do not use them.
|
||||||
, reqRequestBody :: IO RequestBodyContents
|
, reqRequestBody :: IO RequestBodyContents
|
||||||
, reqWaiRequest :: W.Request
|
, reqWaiRequest :: W.Request
|
||||||
|
-- | Languages which the client supports.
|
||||||
, reqLangs :: [Language]
|
, reqLangs :: [Language]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,8 +87,10 @@ multiLookup ((k, v):rest) pn
|
|||||||
| otherwise = multiLookup rest pn
|
| otherwise = multiLookup rest pn
|
||||||
|
|
||||||
-- | All GET paramater values with the given name.
|
-- | All GET paramater values with the given name.
|
||||||
getParams :: Request -> ParamName -> [ParamValue]
|
getParams :: RequestReader m => m (ParamName -> [ParamValue])
|
||||||
getParams rr = multiLookup $ reqGetParams rr
|
getParams = do
|
||||||
|
rr <- getRequest
|
||||||
|
return $ multiLookup $ reqGetParams rr
|
||||||
|
|
||||||
-- | All POST paramater values with the given name.
|
-- | All POST paramater values with the given name.
|
||||||
postParams :: MonadIO m => Request -> m (ParamName -> [ParamValue])
|
postParams :: MonadIO m => Request -> m (ParamName -> [ParamValue])
|
||||||
@ -103,52 +98,14 @@ postParams rr = do
|
|||||||
(pp, _) <- liftIO $ reqRequestBody rr
|
(pp, _) <- liftIO $ reqRequestBody rr
|
||||||
return $ multiLookup pp
|
return $ multiLookup pp
|
||||||
|
|
||||||
-- | Produces a \"compute on demand\" value. The computation will be run once
|
|
||||||
-- it is requested, and then the result will be stored. This will happen only
|
|
||||||
-- once.
|
|
||||||
iothunk :: IO a -> IO (IO a)
|
|
||||||
iothunk = fmap go . newMVar . Left where
|
|
||||||
go :: MVar (Either (IO a) a) -> IO a
|
|
||||||
go mvar = modifyMVar mvar go'
|
|
||||||
go' :: Either (IO a) a -> IO (Either (IO a) a, a)
|
|
||||||
go' (Right val) = return (Right val, val)
|
|
||||||
go' (Left comp) = do
|
|
||||||
val <- comp
|
|
||||||
return (Right val, val)
|
|
||||||
|
|
||||||
-- | All cookies with the given name.
|
-- | All cookies with the given name.
|
||||||
cookies :: Request -> ParamName -> [ParamValue]
|
cookies :: RequestReader m => m (ParamName -> [ParamValue])
|
||||||
cookies rr name =
|
cookies = do
|
||||||
map snd . filter (fst `equals` name) . reqCookies $ rr
|
rr <- getRequest
|
||||||
where
|
return $ multiLookup $ reqCookies rr
|
||||||
equals f x y = f y == x
|
|
||||||
|
|
||||||
parseWaiRequest :: W.Request
|
-- | All session data with the given name.
|
||||||
-> [(B.ByteString, B.ByteString)] -- ^ session
|
session :: RequestReader m => m (ParamName -> [ParamValue])
|
||||||
-> IO Request
|
session = do
|
||||||
parseWaiRequest env session = do
|
rr <- getRequest
|
||||||
let gets' = map (cs *** cs) $ decodeUrlPairs $ W.queryString env
|
return $ multiLookup $ reqSession rr
|
||||||
let reqCookie = fromMaybe B.empty $ lookup W.Cookie $ W.requestHeaders env
|
|
||||||
cookies' = map (cs *** cs) $ parseCookies reqCookie
|
|
||||||
acceptLang = lookup W.AcceptLanguage $ W.requestHeaders env
|
|
||||||
langs = map cs $ maybe [] parseHttpAccept acceptLang
|
|
||||||
langs' = case lookup langKey cookies' of
|
|
||||||
Nothing -> langs
|
|
||||||
Just x -> x : langs
|
|
||||||
langs'' = case lookup langKey gets' of
|
|
||||||
Nothing -> langs'
|
|
||||||
Just x -> x : langs'
|
|
||||||
rbthunk <- iothunk $ rbHelper env
|
|
||||||
return $ Request gets' cookies' session rbthunk env langs''
|
|
||||||
|
|
||||||
rbHelper :: W.Request -> IO RequestBodyContents
|
|
||||||
rbHelper = fmap (fix1 *** map fix2) . parseRequestBody lbsSink where
|
|
||||||
fix1 = map (cs *** cs)
|
|
||||||
fix2 (x, FileInfo a b c) = (cs x, FileInfo (cs a) (cs b) c)
|
|
||||||
|
|
||||||
#if TEST
|
|
||||||
testSuite :: Test
|
|
||||||
testSuite = testGroup "Yesod.Request"
|
|
||||||
[
|
|
||||||
]
|
|
||||||
#endif
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
{-# LANGUAGE TypeSynonymInstances #-}
|
{-# LANGUAGE TypeSynonymInstances #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE DeriveDataTypeable #-}
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
{-# LANGUAGE CPP #-}
|
|
||||||
{-# LANGUAGE Rank2Types #-}
|
{-# LANGUAGE Rank2Types #-}
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
--
|
--
|
||||||
@ -19,42 +18,28 @@
|
|||||||
--
|
--
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
module Yesod.Response
|
module Yesod.Response
|
||||||
( -- * Representations
|
( -- * Content
|
||||||
Content (..)
|
Content (..)
|
||||||
|
, toContent
|
||||||
|
-- * Representations
|
||||||
, ChooseRep
|
, ChooseRep
|
||||||
, HasReps (..)
|
, HasReps (..)
|
||||||
, defChooseRep
|
, defChooseRep
|
||||||
, ioTextToContent
|
|
||||||
-- ** Convenience wrappers
|
|
||||||
, staticRep
|
|
||||||
-- ** Specific content types
|
-- ** Specific content types
|
||||||
, RepHtml (..)
|
, RepHtml (..)
|
||||||
, RepJson (..)
|
, RepJson (..)
|
||||||
, RepHtmlJson (..)
|
, RepHtmlJson (..)
|
||||||
, RepPlain (..)
|
, RepPlain (..)
|
||||||
, RepXml (..)
|
, RepXml (..)
|
||||||
-- * Response type
|
|
||||||
, Response (..)
|
|
||||||
-- * Special responses
|
-- * Special responses
|
||||||
, RedirectType (..)
|
, RedirectType (..)
|
||||||
, getRedirectStatus
|
|
||||||
, SpecialResponse (..)
|
, SpecialResponse (..)
|
||||||
-- * Error responses
|
-- * Error responses
|
||||||
, ErrorResponse (..)
|
, ErrorResponse (..)
|
||||||
, getStatus
|
|
||||||
-- * Header
|
-- * Header
|
||||||
, Header (..)
|
, Header (..)
|
||||||
, headerToPair
|
|
||||||
-- * Converting to WAI values
|
|
||||||
, responseToWaiResponse
|
|
||||||
#if TEST
|
|
||||||
-- * Tests
|
|
||||||
, testSuite
|
|
||||||
, runContent
|
|
||||||
#endif
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Time.Clock
|
|
||||||
import Data.Maybe (mapMaybe)
|
import Data.Maybe (mapMaybe)
|
||||||
import qualified Data.ByteString as B
|
import qualified Data.ByteString as B
|
||||||
import qualified Data.ByteString.Lazy as L
|
import qualified Data.ByteString.Lazy as L
|
||||||
@ -62,22 +47,19 @@ import Data.Text.Lazy (Text)
|
|||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Convertible.Text
|
import Data.Convertible.Text
|
||||||
|
|
||||||
import Web.Encodings (formatW3)
|
|
||||||
import qualified Network.Wai as W
|
import qualified Network.Wai as W
|
||||||
import qualified Network.Wai.Enumerator as WE
|
import qualified Network.Wai.Enumerator as WE
|
||||||
|
|
||||||
#if TEST
|
|
||||||
import Yesod.Request hiding (testSuite)
|
|
||||||
import Web.Mime hiding (testSuite)
|
|
||||||
#else
|
|
||||||
import Yesod.Request
|
import Yesod.Request
|
||||||
import Web.Mime
|
import Web.Mime
|
||||||
#endif
|
|
||||||
|
|
||||||
#if TEST
|
|
||||||
import Test.Framework (testGroup, Test)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
-- | There are two different methods available for providing content in the
|
||||||
|
-- response: via files and enumerators. The former allows server to use
|
||||||
|
-- optimizations (usually the sendfile system call) for serving static files.
|
||||||
|
-- The latter is a space-efficient approach to content.
|
||||||
|
--
|
||||||
|
-- It can be tedious to write enumerators; often times, you will be well served
|
||||||
|
-- to use 'toContent'.
|
||||||
data Content = ContentFile FilePath
|
data Content = ContentFile FilePath
|
||||||
| ContentEnum (forall a.
|
| ContentEnum (forall a.
|
||||||
(a -> B.ByteString -> IO (Either a a))
|
(a -> B.ByteString -> IO (Either a a))
|
||||||
@ -94,13 +76,18 @@ instance ConvertSuccess Text Content where
|
|||||||
convertSuccess lt = cs (cs lt :: L.ByteString)
|
convertSuccess lt = cs (cs lt :: L.ByteString)
|
||||||
instance ConvertSuccess String Content where
|
instance ConvertSuccess String Content where
|
||||||
convertSuccess s = cs (cs s :: Text)
|
convertSuccess s = cs (cs s :: Text)
|
||||||
|
instance ConvertSuccess (IO Text) Content where
|
||||||
|
convertSuccess = swapEnum . WE.fromLBS' . fmap cs
|
||||||
|
|
||||||
type ChooseRep = [ContentType] -> IO (ContentType, Content)
|
-- | A synonym for 'convertSuccess' to make the desired output type explicit.
|
||||||
|
toContent :: ConvertSuccess x Content => x -> Content
|
||||||
|
toContent = cs
|
||||||
|
|
||||||
-- | It would be nice to simplify 'Content' to the point where this is
|
-- | A function which gives targetted representations of content based on the
|
||||||
-- unnecesary.
|
-- content-types the user accepts.
|
||||||
ioTextToContent :: IO Text -> Content
|
type ChooseRep =
|
||||||
ioTextToContent = swapEnum . WE.fromLBS' . fmap cs
|
[ContentType] -- ^ list of content-types user accepts, ordered by preference
|
||||||
|
-> IO (ContentType, Content)
|
||||||
|
|
||||||
swapEnum :: W.Enumerator -> Content
|
swapEnum :: W.Enumerator -> Content
|
||||||
swapEnum (W.Enumerator e) = ContentEnum e
|
swapEnum (W.Enumerator e) = ContentEnum e
|
||||||
@ -110,13 +97,16 @@ class HasReps a where
|
|||||||
chooseRep :: a -> ChooseRep
|
chooseRep :: a -> ChooseRep
|
||||||
|
|
||||||
-- | A helper method for generating 'HasReps' instances.
|
-- | A helper method for generating 'HasReps' instances.
|
||||||
|
--
|
||||||
|
-- This function should be given a list of pairs of content type and conversion
|
||||||
|
-- functions. If none of the content types match, the first pair is used.
|
||||||
defChooseRep :: [(ContentType, a -> IO Content)] -> a -> ChooseRep
|
defChooseRep :: [(ContentType, a -> IO Content)] -> a -> ChooseRep
|
||||||
defChooseRep reps a ts = do
|
defChooseRep reps a ts = do
|
||||||
let (ct, c) =
|
let (ct, c) =
|
||||||
case mapMaybe helper ts of
|
case mapMaybe helper ts of
|
||||||
(x:_) -> x
|
(x:_) -> x
|
||||||
[] -> case reps of
|
[] -> case reps of
|
||||||
[] -> error "Empty reps"
|
[] -> error "Empty reps to defChooseRep"
|
||||||
(x:_) -> x
|
(x:_) -> x
|
||||||
c' <- c a
|
c' <- c a
|
||||||
return (ct, c')
|
return (ct, c')
|
||||||
@ -141,13 +131,6 @@ instance HasReps [(ContentType, Content)] where
|
|||||||
where
|
where
|
||||||
go = simpleContentType . contentTypeToString
|
go = simpleContentType . contentTypeToString
|
||||||
|
|
||||||
-- | Data with a single representation.
|
|
||||||
staticRep :: ConvertSuccess x Content
|
|
||||||
=> ContentType
|
|
||||||
-> x
|
|
||||||
-> [(ContentType, Content)]
|
|
||||||
staticRep ct x = [(ct, cs x)]
|
|
||||||
|
|
||||||
newtype RepHtml = RepHtml Content
|
newtype RepHtml = RepHtml Content
|
||||||
instance HasReps RepHtml where
|
instance HasReps RepHtml where
|
||||||
chooseRep (RepHtml c) _ = return (TypeHtml, c)
|
chooseRep (RepHtml c) _ = return (TypeHtml, c)
|
||||||
@ -167,19 +150,12 @@ newtype RepXml = RepXml Content
|
|||||||
instance HasReps RepXml where
|
instance HasReps RepXml where
|
||||||
chooseRep (RepXml c) _ = return (TypeXml, c)
|
chooseRep (RepXml c) _ = return (TypeXml, c)
|
||||||
|
|
||||||
data Response = Response W.Status [Header] ContentType Content
|
|
||||||
|
|
||||||
-- | Different types of redirects.
|
-- | Different types of redirects.
|
||||||
data RedirectType = RedirectPermanent
|
data RedirectType = RedirectPermanent
|
||||||
| RedirectTemporary
|
| RedirectTemporary
|
||||||
| RedirectSeeOther
|
| RedirectSeeOther
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
getRedirectStatus :: RedirectType -> W.Status
|
|
||||||
getRedirectStatus RedirectPermanent = W.Status301
|
|
||||||
getRedirectStatus RedirectTemporary = W.Status302
|
|
||||||
getRedirectStatus RedirectSeeOther = W.Status303
|
|
||||||
|
|
||||||
-- | Special types of responses which should short-circuit normal response
|
-- | Special types of responses which should short-circuit normal response
|
||||||
-- processing.
|
-- processing.
|
||||||
data SpecialResponse =
|
data SpecialResponse =
|
||||||
@ -197,13 +173,6 @@ data ErrorResponse =
|
|||||||
| BadMethod String
|
| BadMethod String
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
getStatus :: ErrorResponse -> W.Status
|
|
||||||
getStatus NotFound = W.Status404
|
|
||||||
getStatus (InternalError _) = W.Status500
|
|
||||||
getStatus (InvalidArgs _) = W.Status400
|
|
||||||
getStatus PermissionDenied = W.Status403
|
|
||||||
getStatus (BadMethod _) = W.Status405
|
|
||||||
|
|
||||||
----- header stuff
|
----- header stuff
|
||||||
-- | Headers to be added to a 'Result'.
|
-- | Headers to be added to a 'Result'.
|
||||||
data Header =
|
data Header =
|
||||||
@ -211,36 +180,3 @@ data Header =
|
|||||||
| DeleteCookie String
|
| DeleteCookie String
|
||||||
| Header String String
|
| Header String String
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
-- | Convert Header to a key/value pair.
|
|
||||||
headerToPair :: Header -> IO (W.ResponseHeader, B.ByteString)
|
|
||||||
headerToPair (AddCookie minutes key value) = do
|
|
||||||
now <- getCurrentTime
|
|
||||||
let expires = addUTCTime (fromIntegral $ minutes * 60) now
|
|
||||||
return (W.SetCookie, cs $ key ++ "=" ++ value ++"; path=/; expires="
|
|
||||||
++ formatW3 expires)
|
|
||||||
headerToPair (DeleteCookie key) = return
|
|
||||||
(W.SetCookie, cs $
|
|
||||||
key ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
|
|
||||||
headerToPair (Header key value) =
|
|
||||||
return (W.responseHeaderFromBS $ cs key, cs value)
|
|
||||||
|
|
||||||
responseToWaiResponse :: Response -> IO W.Response
|
|
||||||
responseToWaiResponse (Response sc hs ct c) = do
|
|
||||||
hs' <- mapM headerToPair hs
|
|
||||||
let hs'' = (W.ContentType, cs $ contentTypeToString ct) : hs'
|
|
||||||
return $ W.Response sc hs'' $ case c of
|
|
||||||
ContentFile fp -> Left fp
|
|
||||||
ContentEnum e -> Right $ W.Enumerator e
|
|
||||||
|
|
||||||
#if TEST
|
|
||||||
runContent :: Content -> IO L.ByteString
|
|
||||||
runContent (ContentFile fp) = L.readFile fp
|
|
||||||
runContent (ContentEnum c) = WE.toLBS $ W.Enumerator c
|
|
||||||
|
|
||||||
----- Testing
|
|
||||||
testSuite :: Test
|
|
||||||
testSuite = testGroup "Yesod.Response"
|
|
||||||
[
|
|
||||||
]
|
|
||||||
#endif
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user