wai-middleware-caching-0.1.0.0: src/Network/Wai/Middleware/Cache.hs
{-# LANGUAGE OverloadedStrings #-}
module Network.Wai.Middleware.Cache
( cache
, cacheNoBody
, CacheBackend(..)
, responseToLBS
) where
import Blaze.ByteString.Builder (Builder, toLazyByteString)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as LZ
import Data.IORef
import Network.Wai (Middleware, Request, Response,
requestBody, responseToStream,
mapResponseHeaders)
--------------------------------------------------------------------------------
-- | The data structure that should contains everything you need to create
-- a cache backend
data CacheBackend cacheContainer cacheKey cacheVal =
CacheBackend {
keyFromReq :: Request -> ByteString -> IO cacheKey -- ^ Get cacheKey from request and its body
, toCache :: Request -> ByteString -> IO Bool -- ^ Function to check whether cache or not
, addToCache :: cacheContainer -> cacheKey -> cacheVal -> IO () -- ^ Adding to cache
, actionOnCache :: Request -> Response -> IO () -- ^ Action to perform before each caching request
, actionOnCacheMiss :: Request -> Response -> IO () -- ^ Action to perfom before each cache miss
, responseToCacheVal :: Response -> IO cacheVal -- ^ Transform response to cached value
, cacheValToResponse :: cacheVal -> Response -- ^ Transform cached value to response
, lookupCache :: cacheContainer -> cacheKey -> IO (Maybe cacheVal) -- ^ cache lookup
, cacheContainer :: cacheContainer -- ^ A cache container
}
--------------------------------------------------------------------------------
-- Cache Backend Agnostic Cache Middleware
-- This version duplicate the body of the request making it quite far less efficient
-- than the cacheNoBody function
cache :: CacheBackend cc ck cv -- ^ A cache backend
-> Middleware
cache cb app req sendResponse = do
(req',body) <- getRequestBody req
caching <- toCache cb req' body
if not caching
then app req' sendResponse
else do
(req'',_) <- getRequestBody req'
cacheKey <- keyFromReq cb req'' body
found <- lookupCache cb (cacheContainer cb) cacheKey
maybe (app req'' (addToCacheAndRespond cb sendResponse req cacheKey))
(respondFromCache cb sendResponse req'')
found
--------------------------------------------------------------------------------
-- Cache Backend Agnostic Cache Middleware
-- This version don't provide the request body for create key or deciding
-- whether to cache. But it should be more efficient
cacheNoBody :: CacheBackend cc ck cv -- ^ A cache backend
-> Middleware
cacheNoBody cb app req sendResponse = do
caching <- toCache cb req S8.empty
if not caching
then app req sendResponse
else do
cacheKey <- keyFromReq cb req S8.empty
found <- lookupCache cb (cacheContainer cb) cacheKey
maybe (app req (addToCacheAndRespond cb sendResponse req cacheKey))
(respondFromCache cb sendResponse req)
found
addXCacheHeader :: Response -> Response
addXCacheHeader = mapResponseHeaders (("X-Cached","true"):)
respondFromCache :: CacheBackend cc ck cv
-> (Response -> IO b)
-> Request
-> cv
-> IO b
respondFromCache cb sendResponse r cachedVal = do
let response = cacheValToResponse cb cachedVal
actionOnCache cb r response
sendResponse (addXCacheHeader response)
addToCacheAndRespond :: CacheBackend cc ck cv
-> (Response -> IO b)
-> Request
-> ck
-> Response
-> IO b
addToCacheAndRespond cb sendResponse req key r = do
cacheVal <- responseToCacheVal cb r
addToCache cb (cacheContainer cb) key cacheVal
actionOnCacheMiss cb req r
sendResponse (cacheValToResponse cb cacheVal)
getRequestBody :: Request -> IO (Request, S8.ByteString)
getRequestBody req = do
let loop front = do
bs <- requestBody req
if S8.null bs
then return $ front []
else loop $ front . (bs:)
body <- loop id
-- logging the body here consumes it, so fill it back up
-- obviously not efficient
--
-- Note: previously, we simply used CL.sourceList. However,
-- that meant that you could read the request body in twice.
-- While that in itself is not a problem, the issue is that,
-- in production, you wouldn't be able to do this, and
-- therefore some bugs wouldn't show up during testing. This
-- implementation ensures that each chunk is only returned
-- once.
ichunks <- newIORef body
let rbody = atomicModifyIORef ichunks $ \chunks ->
case chunks of
[] -> ([], S8.empty)
x:y -> (y, x)
let req' = req { requestBody = rbody }
return (req', S8.concat body)
-- | Helper for your cache backend
responseToLBS :: Response -> IO LZ.ByteString
responseToLBS response = do
let (_,_,f) = responseToStream response
f $ \streamingBody -> do
builderRef <- newIORef mempty
let add :: Builder -> IO ()
add b = atomicModifyIORef builderRef $ \builder -> (builder `mappend` b,())
flush :: IO ()
flush = return ()
streamingBody add flush
fmap toLazyByteString (readIORef builderRef)