module Web.Fn (
FnRequest
, defaultFnRequest
, RequestContext(..)
, toWAI
, Req
, Route
, route
, fallthrough
, (==>)
, (!=>)
, (//)
, (/?)
, path
, end
, anything
, segment
, method
, FromParam(..)
, ParamError(..)
, param
, paramMany
, paramOpt
, File(..)
, file
, files
, staticServe
, sendFile
, okText
, okJson
, okHtml
, errText
, errHtml
, notFoundText
, notFoundHtml
, redirect
, redirectReferer
, tempFileBackEnd'
) where
import qualified Blaze.ByteString.Builder.Char.Utf8 as B
import Control.Applicative ((<$>))
import Control.Arrow (second)
import Control.Concurrent.MVar
import Control.Monad (join)
import Control.Monad.Trans.Resource (InternalState,
closeInternalState,
createInternalState)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as LB
import Data.Either (lefts, rights)
import qualified Data.HashMap.Strict as HM
import Data.Maybe (fromJust)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Text.Read (decimal, double)
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Parse (FileInfo (..), Param,
lbsBackEnd,
parseRequestBody)
import qualified Network.Wai.Parse as Parse
import System.Directory (doesFileExist,
getTemporaryDirectory,
removeFile)
import System.FilePath (takeExtension)
data Store b a = Store b (b -> a)
instance Functor (Store b) where
fmap f (Store b h) = Store b (f . h)
type Route ctxt = ctxt -> Req -> IO (Maybe (IO (Maybe Response)))
type PostMVar = Maybe (MVar (Maybe (([Param], [Parse.File FilePath]), InternalState)))
type FnRequest = (Request, PostMVar)
defaultFnRequest :: FnRequest
defaultFnRequest = (defaultRequest, Nothing)
class RequestContext ctxt where
requestLens :: Functor f => (FnRequest -> f FnRequest) -> ctxt -> f ctxt
requestLens f c = setRequest c <$> f (getRequest c)
getRequest :: ctxt -> FnRequest
getRequest c =
let (Store r _) = requestLens (`Store` id) c
in r
setRequest :: ctxt -> FnRequest -> ctxt
setRequest c r =
let (Store _ b) = requestLens (`Store` id) c
in b r
instance RequestContext FnRequest where
getRequest = id
setRequest _ = id
toWAI :: RequestContext ctxt => ctxt -> (ctxt -> IO Response) -> Application
toWAI ctxt f req cont =
do mv <- newMVar Nothing
do resp <- f (setRequest ctxt (req, Just mv))
posted <- tryTakeMVar mv
case join posted of
Nothing -> return ()
Just (_,is) -> closeInternalState is
cont resp
route :: RequestContext ctxt =>
ctxt ->
[Route ctxt] ->
IO (Maybe Response)
route ctxt pths =
do let (r,post) = getRequest ctxt
m = either (const GET) id (parseMethod (requestMethod r))
req = (r, filter (/= "") (pathInfo r), queryString r, m, post)
route' req pths
where route' _ [] = return Nothing
route' req (x:xs) =
do mact <- x ctxt req
case mact of
Nothing -> route' req xs
Just action ->
do resp <- action
case resp of
Nothing -> route' req xs
Just response -> return (Just response)
fallthrough :: IO (Maybe Response) -> IO Response -> IO Response
fallthrough a ft =
do response <- a
case response of
Nothing -> ft
Just r -> return r
mimeMap :: HM.HashMap String ByteString
mimeMap = HM.fromList [
( ".asc" , "text/plain" ),
( ".asf" , "video/x-ms-asf" ),
( ".asx" , "video/x-ms-asf" ),
( ".avi" , "video/x-msvideo" ),
( ".bz2" , "application/x-bzip" ),
( ".c" , "text/plain" ),
( ".class" , "application/octet-stream" ),
( ".conf" , "text/plain" ),
( ".cpp" , "text/plain" ),
( ".css" , "text/css" ),
( ".cxx" , "text/plain" ),
( ".dtd" , "text/xml" ),
( ".dvi" , "application/x-dvi" ),
( ".gif" , "image/gif" ),
( ".gz" , "application/x-gzip" ),
( ".hs" , "text/plain" ),
( ".htm" , "text/html" ),
( ".html" , "text/html" ),
( ".ico" , "image/x-icon" ),
( ".jar" , "application/x-java-archive" ),
( ".jpeg" , "image/jpeg" ),
( ".jpg" , "image/jpeg" ),
( ".js" , "text/javascript" ),
( ".json" , "application/json" ),
( ".log" , "text/plain" ),
( ".m3u" , "audio/x-mpegurl" ),
( ".mov" , "video/quicktime" ),
( ".mp3" , "audio/mpeg" ),
( ".mpeg" , "video/mpeg" ),
( ".mpg" , "video/mpeg" ),
( ".ogg" , "application/ogg" ),
( ".pac" , "application/x-ns-proxy-autoconfig" ),
( ".pdf" , "application/pdf" ),
( ".png" , "image/png" ),
( ".ps" , "application/postscript" ),
( ".qt" , "video/quicktime" ),
( ".sig" , "application/pgp-signature" ),
( ".spl" , "application/futuresplash" ),
( ".svg" , "image/svg+xml" ),
( ".swf" , "application/x-shockwave-flash" ),
( ".tar" , "application/x-tar" ),
( ".tar.bz2" , "application/x-bzip-compressed-tar" ),
( ".tar.gz" , "application/x-tgz" ),
( ".tbz" , "application/x-bzip-compressed-tar" ),
( ".text" , "text/plain" ),
( ".tgz" , "application/x-tgz" ),
( ".torrent" , "application/x-bittorrent" ),
( ".ttf" , "application/x-font-truetype" ),
( ".txt" , "text/plain" ),
( ".wav" , "audio/x-wav" ),
( ".wax" , "audio/x-ms-wax" ),
( ".wma" , "audio/x-ms-wma" ),
( ".wmv" , "video/x-ms-wmv" ),
( ".xbm" , "image/x-xbitmap" ),
( ".xml" , "text/xml" ),
( ".xpm" , "image/x-xpixmap" ),
( ".xwd" , "image/x-xwindowdump" ),
( ".zip" , "application/zip" ) ]
staticServe :: RequestContext ctxt => Text -> ctxt -> IO (Maybe Response)
staticServe d ctxt = do
let pth = T.intercalate "/" $ d : pathInfo (fst . getRequest $ ctxt)
if "/" `T.isPrefixOf` pth || ".." `T.isInfixOf` pth
then return Nothing
else sendFile (T.unpack pth)
sendFile :: FilePath -> IO (Maybe Response)
sendFile pth =
do exists <- doesFileExist pth
if exists
then do let ext = takeExtension pth
contentType = case HM.lookup ext mimeMap of
Nothing -> []
Just t -> [(hContentType, t)]
return $ Just $ responseFile status200
contentType
pth
Nothing
else return Nothing
type Req = (Request, [Text], Query, StdMethod, PostMVar)
(==>) :: RequestContext ctxt =>
(Req -> IO (Maybe (Req, k -> a))) ->
(ctxt -> k) ->
ctxt ->
Req ->
IO (Maybe a)
(match ==> handle) ctxt req =
do rsp <- match req
case rsp of
Nothing -> return Nothing
Just ((_,pathInfo',_,_,_), k) ->
let (request, mv) = getRequest ctxt in
return $ Just (k $ handle (setRequest ctxt (request { pathInfo = pathInfo' }, mv)))
tempFileBackEnd' :: InternalState -> ignored1 -> FileInfo () -> IO ByteString -> IO FilePath
tempFileBackEnd' is x fi@(FileInfo nm _ _) = Parse.tempFileBackEndOpts getTemporaryDirectory (T.unpack $ T.decodeUtf8 nm) is x fi
readBody mv request =
modifyMVar_ mv
(\r -> case r of
Nothing ->
do is <- createInternalState
rb <- parseRequestBody (tempFileBackEnd' is) request
return (Just (rb, is))
Just _ -> return r)
(!=>) :: RequestContext ctxt =>
(Req -> IO (Maybe (Req, k -> a))) ->
(ctxt -> k) ->
ctxt ->
Req ->
IO (Maybe a)
(match !=> handle) ctxt req =
do let (request, Just mv) = getRequest ctxt
readBody mv request
rsp <- match req
case rsp of
Nothing -> return Nothing
Just ((_,pathInfo',_,_,_), k) ->
do return $ Just (k $ handle (setRequest ctxt (request { pathInfo = pathInfo' }, Just mv)))
(//) :: (r -> IO (Maybe (r, k -> k'))) ->
(r -> IO (Maybe (r, k' -> a))) ->
r -> IO (Maybe (r, k -> a))
(match1 // match2) req = do
r1 <- match1 req
case r1 of
Nothing -> return Nothing
Just (req', k) ->
do r2 <- match2 req'
return $ case r2 of
Nothing -> Nothing
Just (req'', k') -> Just (req'', k' . k)
(/?) :: (r -> IO (Maybe (r, k -> k'))) ->
(r -> IO (Maybe (r, k' -> a))) ->
r -> IO (Maybe (r, k -> a))
(/?) = (//)
path :: Text -> Req -> IO (Maybe (Req, a -> a))
path s req =
return $ case req of
(r,y:ys,q,m,x) | y == s -> Just ((r,ys, q, m, x), id)
_ -> Nothing
end :: Req -> IO (Maybe (Req, a -> a))
end req =
return $ case req of
(_,[],_,_,_) -> Just (req, id)
_ -> Nothing
anything :: Req -> IO (Maybe (Req, a -> a))
anything req = return $ Just (req, id)
segment :: FromParam p => Req -> IO (Maybe (Req, (p -> a) -> a))
segment req =
return $ case req of
(r,y:ys,q,m,x) -> case fromParam [y] of
Left _ -> Nothing
Right p -> Just ((r, ys, q, m, x), \k -> k p)
_ -> Nothing
method :: StdMethod -> Req -> IO (Maybe (Req, a -> a))
method m r@(_,_,_,m',_) | m == m' = return $ Just (r, id)
method _ _ = return Nothing
data ParamError = ParamMissing | ParamTooMany | ParamUnparsable | ParamOtherError Text deriving (Eq, Show)
class FromParam a where
fromParam :: [Text] -> Either ParamError a
instance FromParam Text where
fromParam [x] = Right x
fromParam [] = Left ParamMissing
fromParam _ = Left ParamTooMany
instance FromParam Int where
fromParam [t] = case decimal t of
Left _ -> Left ParamUnparsable
Right m | snd m /= "" ->
Left ParamUnparsable
Right (v, _) -> Right v
fromParam [] = Left ParamMissing
fromParam _ = Left ParamTooMany
instance FromParam Double where
fromParam [t] = case double t of
Left _ -> Left ParamUnparsable
Right m | snd m /= "" ->
Left ParamUnparsable
Right (v, _) -> Right v
fromParam [] = Left ParamMissing
fromParam _ = Left ParamTooMany
instance FromParam a => FromParam [a] where
fromParam ps = let res = map (fromParam . (:[])) ps in
case lefts res of
[] -> Right $ rights res
_ -> Left $ ParamOtherError "Couldn't parse all parameters."
instance FromParam a => FromParam (Maybe a) where
fromParam [x] = Just <$> fromParam [x]
fromParam [] = Right Nothing
fromParam _ = Left ParamTooMany
findParamMatches :: FromParam p => Text -> [(ByteString, Maybe ByteString)] -> Either ParamError p
findParamMatches n ps = fromParam .
map (maybe "" T.decodeUtf8 . snd) .
filter ((== T.encodeUtf8 n) . fst) $
ps
getMVarParams mv = case mv of
Just mv' -> do v <- readMVar mv'
return $ case v of
Nothing -> []
Just ((ps',_),_) -> ps'
Nothing -> return []
param :: FromParam p => Text -> Req -> IO (Maybe (Req, (p -> a) -> a))
param n req =
do let (_,_,q,_,mv) = req
ps <- getMVarParams mv
return $ case findParamMatches n (q ++ map (second Just) ps) of
Right y -> Just (req, \k -> k y)
Left _ -> Nothing
paramMany :: FromParam p => Text -> Req -> IO (Maybe (Req, ([p] -> a) -> a))
paramMany n req =
do let (_,_,q,_,mv) = req
ps <- getMVarParams mv
return $ case findParamMatches n (q ++ map (second Just) ps) of
Left _ -> Nothing
Right ys -> Just (req, \k -> k ys)
paramOpt :: FromParam p =>
Text ->
Req ->
IO (Maybe (Req, (Either ParamError p -> a) -> a))
paramOpt n req =
do let (_,_,q,_,mv) = req
ps <- getMVarParams mv
return $ Just (req, \k -> k (findParamMatches n (q ++ map (second Just) ps)))
data File = File { fileName :: Text
, fileContentType :: Text
, filePath :: FilePath
}
getMVarFiles mv req =
case mv of
Nothing -> error $ "Fn: tried to read a 'file' or 'files', but FnRequest wasn't initialized with MVar."
Just mv' -> do
readBody mv' req
Just ((_,fs'),_) <- readMVar mv'
return $ map (\(n, FileInfo nm ct c) ->
(T.decodeUtf8 n, File (T.decodeUtf8 nm)
(T.decodeUtf8 ct)
c)) fs'
file :: Text -> Req -> IO (Maybe (Req, (File -> a) -> a))
file n req =
do let (r,_,_,_,mv) = req
fs <- getMVarFiles mv r
return $ case filter ((== n) . fst) fs of
[(_, f)] -> Just (req, \k -> k f)
_ -> Nothing
files :: Req -> IO (Maybe (Req, ([(Text, File)] -> a) -> a))
files req =
do let (r,_,_,_,mv) = req
fs <- getMVarFiles mv r
return $ Just (req, \k -> k fs)
returnText :: Text -> Status -> ByteString -> IO (Maybe Response)
returnText text status content =
return $ Just $
responseBuilder status
[(hContentType, content)]
(B.fromText text)
plainText :: ByteString
plainText = "text/plain; charset=utf-8"
applicationJson :: ByteString
applicationJson = "application/json; charset=utf-8"
html :: ByteString
html = "text/html; charset=utf-8"
okText :: Text -> IO (Maybe Response)
okText t = returnText t status200 plainText
okJson :: Text -> IO (Maybe Response)
okJson j = returnText j status200 applicationJson
okHtml :: Text -> IO (Maybe Response)
okHtml t = returnText t status200 html
errText :: Text -> IO (Maybe Response)
errText t = returnText t status500 plainText
errHtml :: Text -> IO (Maybe Response)
errHtml t = returnText t status500 html
notFoundText :: Text -> IO Response
notFoundText t = fromJust <$> returnText t status404 plainText
notFoundHtml :: Text -> IO Response
notFoundHtml t = fromJust <$> returnText t status404 html
redirect :: Text -> IO (Maybe Response)
redirect target =
return $ Just $
responseBuilder status303
[(hLocation, T.encodeUtf8 target)]
(B.fromText "")
redirectReferer :: RequestContext ctxt => ctxt -> IO (Maybe Response)
redirectReferer ctxt =
let rs = requestHeaders $ fst $ getRequest ctxt in
case lookup hReferer rs of
Nothing -> redirect "/"
Just r -> redirect (T.decodeUtf8 r)