@@ -28,7 +28,7 @@ module Network.Wai.HTTP2
2828 -- * Applications
2929 HTTP2Application
3030 -- * Responder
31− , Responder
31+ , Responder(..)
3232 , RespondFunc
3333 , Body
3434 , BodyOf
@@ -41,10 +41,13 @@ module Network.Wai.HTTP2
4141 -- * Conveniences
4242 , promoteApplication
4343 -- ** Responders
44− , responder
44+ , respond
45+ , respondCont
46+ , respondIO
4547 , respondFile
4648 , respondFilePart
4749 , respondNotFound
50+ , respondWith
4851 -- ** Stream Bodies
4952 , SimpleBody
5053 , streamFilePart
@@ -54,6 +57,7 @@ module Network.Wai.HTTP2
5457
5558import Blaze.ByteString.Builder (Builder)
5659import Control.Exception (Exception, SomeException, throwIO)
60+import Control.Monad.Trans.Cont (ContT(..))
5761import Data.ByteString (ByteString)
5862import Data.IORef (newIORef, readIORef, writeIORef)
5963#if __GLASGOW_HASKELL__ < 709
@@ -118,7 +122,8 @@ type RespondFunc s a = H.Status -> H.ResponseHeaders -> TrailerFunc a -> Body a
118122-- The respond function is similar to the one in 'Network.Wai.Application', but
119123-- it only takes a streaming body, the status and headers are curried, and it
120124-- passes on any result value from the stream body.
121−type Responder = forall s. (forall a. RespondFunc s a) -> IO s
125+newtype Responder = Responder
126+ { runResponder :: forall s. (forall a. RespondFunc s a) -> IO s }
122127
123128-- | A function given to an 'HTTP2Application' to initiate a server-pushed
124129-- stream. Its argument is the same as the result of an 'HTTP2Application', so
@@ -150,9 +155,9 @@ streamFilePart path part write _ = write $ FileChunk path part
150155-- If you want the range to be inferred automatically from the Range header,
151156-- use 'respondFile' instead.
152157respondFilePart :: H.Status -> H.ResponseHeaders -> FilePath -> FilePart -> Responder
153−respondFilePart s h path part respond = do
158+respondFilePart s h path part = Responder $ \k -> do
154159 let (s', h') = adjustForFilePart s h part
155− respond s' h' mempty $ streamFilePart path part
160+ k s' h' mempty $ streamFilePart path part
156161
157162-- | Serve the requested range of the specified file (based on the Range
158163-- header), using the given 'H.Status' and 'H.ResponseHeaders' as a base. If
@@ -161,11 +166,11 @@ respondFilePart s h path part respond = do
161166-- the status will be replaced with 206 and the Content-Range header will be
162167-- added. The Content-Length header will always be added.
163168respondFile :: H.Status -> H.ResponseHeaders -> FilePath -> H.RequestHeaders -> Responder
164−respondFile s h path reqHdrs respond = do
169+respondFile s h path reqHdrs = Responder $ \k -> do
165170 fileSize <- tryGetFileSize path
166171 case fileSize of
167− Left _ -> respondNotFound h respond
168− Right size -> respondFileExists s h path size reqHdrs respond
172+ Left _ -> runResponder (respondNotFound h) k
173+ Right size -> runResponder (respondFileExists s h path size reqHdrs) k
169174
170175-- As 'respondFile', but with prior knowledge of the file's existence and size.
171176respondFileExists :: H.Status -> H.ResponseHeaders -> FilePath -> Integer -> H.RequestHeaders -> Responder
@@ -174,12 +179,42 @@ respondFileExists s h path size reqHdrs =
174179
175180-- | Respond with a minimal 404 page with the given headers.
176181respondNotFound :: H.ResponseHeaders -> Responder
177−respondNotFound h respond = respond H.notFound404 h' mempty $
182+respondNotFound h = Responder $ \k -> k H.notFound404 h' mempty $
178183 streamBuilder "File not found."
179184 where
180185 contentType = (H.hContentType, "text/plain; charset=utf-8")
181186 h' = contentType:filter ((/=H.hContentType) . fst) h
182187
188+-- | Construct a 'Responder' that will just call the 'RespondFunc' with the
189+-- given arguments.
190+respond :: H.Status -> H.ResponseHeaders -> TrailerFunc a -> Body a -> Responder
191+respond s h t b = Responder $ \k -> k s h t b
192+
193+-- | Fold the given bracketing action into a 'Responder'. Note the first
194+-- argument is isomorphic to @Codensity IO a@ or @forall s. ContT s IO a@, and
195+-- is the type of a partially-applied 'Control.Exception.bracket' or
196+-- @with@-style function.
197+--
198+-- > respondWith (bracket acquire release) $
199+-- > \x -> respondNotFound [("x", show x)]
200+--
201+-- is equivalent to
202+--
203+-- > Responder $ \k -> bracket acquire release $
204+-- > \x -> runResponder (respondNotFound [("x", show x)] k
205+--
206+-- This is morally equivalent to ('>>=') on 'Codensity' 'IO'.
207+respondWith :: (forall s. (a -> IO s) -> IO s) -> (a -> Responder) -> Responder
208+respondWith with f = respondCont $ f <$> ContT with
209+
210+-- | Fold the 'ContT' into the contained 'Responder'.
211+respondCont :: (forall r. ContT r IO Responder) -> Responder
212+respondCont cont = Responder $ \k -> runContT cont $ \r -> runResponder r k
213+
214+-- | Fold the 'IO' into the contained 'Responder'.
215+respondIO :: IO Responder -> Responder
216+respondIO io = Responder $ \k -> io >>= \r -> runResponder r k
217+
183218-- | Create a response body consisting of a single builder.
184219streamBuilder :: Builder -> Body ()
185220streamBuilder builder write _ = write $ BuilderChunk builder
@@ -198,17 +233,17 @@ streamSimple body write flush = body (write . BuilderChunk) flush
198233--
199234-- The 'Request' is used to determine the right file range to serve for
200235-- 'ResponseFile'.
201−responder :: Request -> Response -> Responder
202−responder req response respond = case response of
203− (ResponseBuilder s h b) -> respond s h mempty (streamBuilder b)
204− (ResponseStream s h body) -> respond s h mempty (streamSimple body)
205− (ResponseRaw _ fallback) -> responder req fallback respond
206− (ResponseFile s h path mpart) -> case mpart of
207− -- We can't use 'maybe' because the type checker can't instantiate the
208− -- type variable at 'Responder', which is a universally quantified
209− -- type.
210− Nothing -> respondFile s h path (requestHeaders req) respond
211− Just part -> respondFilePart s h path part respond
236+promoteResponse :: Request -> Response -> Responder
237+promoteResponse req response = case response of
238+ (ResponseBuilder s h b) ->
239+ Responder $ \k -> k s h mempty (streamBuilder b)
240+ (ResponseStream s h body) ->
241+ Responder $ \k -> k s h mempty (streamSimple body)
242+ (ResponseRaw _ fallback) -> promoteResponse req fallback
243+ (ResponseFile s h path mpart) -> maybe
244+ (respondFile s h path $ requestHeaders req)
245+ (respondFilePart s h path)
246+ mpart
212247
213248-- | An 'Network.Wai.Application' we tried to promote neither called its
214249-- respond action nor raised; this is only possible if it imported the
@@ -221,15 +256,15 @@ instance Exception RespondNeverCalled
221256-- | Promote a normal WAI 'Application' to an 'HTTP2Application' by ignoring
222257-- the HTTP/2-specific features.
223258promoteApplication :: Application -> HTTP2Application
224−promoteApplication app req _ respond = do
259+promoteApplication app req _ = Responder $ \k -> do
225260 -- In HTTP2Applications, the Responder is required to ferry a value of
226261 -- arbitrary type from the RespondFunc back to the caller of the
227262 -- application, but in Application the type is fixed to ResponseReceived.
228263 -- To add this extra power to an Application, we have to squirrel it away
229264 -- in an IORef as a hack.
230265 ref <- newIORef Nothing
231− let respond' r = do
232− writeIORef ref . Just =<< responder req r respond
266+ let k' r = do
267+ writeIORef ref . Just =<< runResponder (promoteResponse req r) k
233268 return ResponseReceived
234− ResponseReceived <- app req respond'
269+ ResponseReceived <- app req k'
235270 readIORef ref >>= maybe (throwIO RespondNeverCalled) return