git.iko.soy/typeable/servant
on master help sign in
Merge pull request #991 from phadej/servant-machines

Streaming refactoring

commite94919f4b323450c9b721619b6d4289e39fdc06f authorOleg Grenrus <oleg.grenrus@iki.fi> dateMon 5 Nov 2018, 16:23 (8 years ago) parent58ccae145c1cbd
53 files changed+2203−400
.gitignore+3 .travis.yml+17−5 cabal.project+5 doc/cookbook/basic-streaming/Streaming.lhs+126 doc/cookbook/basic-streaming/basic-streaming.cabal+28 doc/tutorial/ApiType.lhs+12−5 doc/tutorial/Client.lhs+21−21 doc/tutorial/Server.lhs+22−8 doc/tutorial/tutorial.cabal+1 servant-client-core/servant-client-core.cabal+1 servant-client-core/src/Servant/Client/Core.hs+1−1 servant-client-core/src/Servant/Client/Core/Internal/HasClient.hs+37−56 servant-client-core/src/Servant/Client/Core/Internal/Request.hs+3−1 servant-client-core/src/Servant/Client/Core/Internal/RunClient.hs+1−1 servant-client/servant-client.cabal+10−5 servant-client/src/Servant/Client.hs+1 servant-client/src/Servant/Client/Internal/HttpClient.hs+14−1 servant-client/test/Servant/StreamSpec.hs+94−66 servant-conduit/CHANGELOG.md+16 servant-conduit/LICENSE+30 servant-conduit/README.md+3 servant-conduit/Setup.hs+2 servant-conduit/example/Main.hs+103 servant-conduit/servant-conduit.cabal+59 servant-conduit/src/Servant/Conduit.hs+74 servant-docs/src/Servant/Docs/Internal.hs+6−2 servant-foreign/src/Servant/Foreign/Internal.hs+8 servant-foreign/test/Servant/ForeignSpec.hs+5 servant-machines/CHANGELOG.md+16 servant-machines/LICENSE+30 servant-machines/README.md+3 servant-machines/Setup.hs+2 servant-machines/example/Main.hs+98 servant-machines/servant-machines.cabal+56 servant-machines/src/Servant/Machines.hs+48 servant-pipes/CHANGELOG.md+16 servant-pipes/LICENSE+30 servant-pipes/README.md+3 servant-pipes/Setup.hs+2 servant-pipes/example/Main.hs+106 servant-pipes/servant-pipes.cabal+60 servant-pipes/src/Servant/Pipes.hs+77 servant-server/src/Servant/Server/Internal.hs+60−43 servant-server/test/Servant/ServerSpec.hs+4−3 servant/servant.cabal+15−4 servant/src/Servant/API.hs+4−5 servant/src/Servant/API/Internal/Test/ComprehensiveAPI.hs+3−1 servant/src/Servant/API/Stream.hs+201−171 servant/src/Servant/Links.hs+5−1 servant/src/Servant/Types/SourceT.hs+305 servant/test/Servant/API/StreamSpec.hs+84 stack.yaml+4 streaming-benchmark.sh+268
.gitignore+3
@@ -29,3 +29,6 @@ doc/_build
2929doc/venv
3030doc/tutorial/static/api.js
3131doc/tutorial/static/jq.js
32+
33+# local versions of things
34+servant-multipart
.travis.yml+17−5
@@ -69,11 +69,11 @@ install:
6969 - rm -fv cabal.project cabal.project.local
7070 - "if [ $HCNUMVER -ge 70800 ]; then sed -i.bak 's/-- ghc-options:.*/ghc-options: -j2/' ${HOME}/.cabal/config; fi"
7171 - grep -Ev -- '^\s*--' ${HOME}/.cabal/config | grep -Ev '^\s*$'
72− - "printf 'packages: \"servant\" \"servant-client\" \"servant-client-core\" \"servant-docs\" \"servant-foreign\" \"servant-server\" \"doc/tutorial\" \"doc/cookbook/basic-auth\" \"doc/cookbook/curl-mock\" \"doc/cookbook/db-postgres-pool\" \"doc/cookbook/db-sqlite-simple\" \"doc/cookbook/file-upload\" \"doc/cookbook/generic\" \"doc/cookbook/https\" \"doc/cookbook/sentry\" \"doc/cookbook/testing\" \"doc/cookbook/structuring-apis\" \"doc/cookbook/using-custom-monad\" \"doc/cookbook/using-free-client\"\\n' > cabal.project"
72+ - "printf 'packages: \"servant\" \"servant-client\" \"servant-client-core\" \"servant-docs\" \"servant-foreign\" \"servant-server\" \"doc/tutorial\" \"servant-machines\" \"servant-conduit\" \"servant-pipes\" \"doc/cookbook/basic-auth\" \"doc/cookbook/curl-mock\" \"doc/cookbook/basic-streaming\" \"doc/cookbook/db-postgres-pool\" \"doc/cookbook/db-sqlite-simple\" \"doc/cookbook/file-upload\" \"doc/cookbook/generic\" \"doc/cookbook/https\" \"doc/cookbook/sentry\" \"doc/cookbook/testing\" \"doc/cookbook/structuring-apis\" \"doc/cookbook/using-custom-monad\" \"doc/cookbook/using-free-client\"\\n' > cabal.project"
7373 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
7474 - "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server,servant-js:base, servant-pagination:servant,servant-pagination:servant-server' >> cabal.project"
7575 - touch cabal.project.local
76− - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- servant | grep -vw -- servant-client | grep -vw -- servant-client-core | grep -vw -- servant-docs | grep -vw -- servant-foreign | grep -vw -- servant-server | grep -vw -- tutorial | grep -vw -- cookbook-basic-auth | grep -vw -- cookbook-curl-mock | grep -vw -- cookbook-db-postgres-pool | grep -vw -- cookbook-db-sqlite-simple | grep -vw -- cookbook-file-upload | grep -vw -- cookbook-generic | grep -vw -- cookbook-https | grep -vw -- cookbook-sentry | grep -vw -- cookbook-testing | grep -vw -- cookbook-structuring-apis | grep -vw -- cookbook-using-custom-monad | grep -vw -- cookbook-using-free-client | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
76+ - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- servant | grep -vw -- servant-client | grep -vw -- servant-client-core | grep -vw -- servant-docs | grep -vw -- servant-foreign | grep -vw -- servant-server | grep -vw -- tutorial | grep -vw -- servant-machines | grep -vw -- servant-conduit | grep -vw -- servant-pipes | grep -vw -- cookbook-basic-auth | grep -vw -- cookbook-curl-mock | grep -vw -- cookbook-basic-streaming | grep -vw -- cookbook-db-postgres-pool | grep -vw -- cookbook-db-sqlite-simple | grep -vw -- cookbook-file-upload | grep -vw -- cookbook-generic | grep -vw -- cookbook-https | grep -vw -- cookbook-sentry | grep -vw -- cookbook-testing | grep -vw -- cookbook-structuring-apis | grep -vw -- cookbook-using-custom-monad | grep -vw -- cookbook-using-free-client | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
7777 - cat cabal.project || true
7878 - cat cabal.project.local || true
7979 - if [ -f "servant/configure.ac" ]; then
@@ -97,12 +97,24 @@ install:
9797 - if [ -f "doc/tutorial/configure.ac" ]; then
9898 (cd "doc/tutorial" && autoreconf -i);
9999 fi
100+ - if [ -f "servant-machines/configure.ac" ]; then
101+ (cd "servant-machines" && autoreconf -i);
102+ fi
103+ - if [ -f "servant-conduit/configure.ac" ]; then
104+ (cd "servant-conduit" && autoreconf -i);
105+ fi
106+ - if [ -f "servant-pipes/configure.ac" ]; then
107+ (cd "servant-pipes" && autoreconf -i);
108+ fi
100109 - if [ -f "doc/cookbook/basic-auth/configure.ac" ]; then
101110 (cd "doc/cookbook/basic-auth" && autoreconf -i);
102111 fi
103112 - if [ -f "doc/cookbook/curl-mock/configure.ac" ]; then
104113 (cd "doc/cookbook/curl-mock" && autoreconf -i);
105114 fi
115+ - if [ -f "doc/cookbook/basic-streaming/configure.ac" ]; then
116+ (cd "doc/cookbook/basic-streaming" && autoreconf -i);
117+ fi
106118 - if [ -f "doc/cookbook/db-postgres-pool/configure.ac" ]; then
107119 (cd "doc/cookbook/db-postgres-pool" && autoreconf -i);
108120 fi
@@ -134,7 +146,7 @@ install:
134146 (cd "doc/cookbook/using-free-client" && autoreconf -i);
135147 fi
136148 - rm -f cabal.project.freeze
137− - rm -rf .ghc.environment.* "servant"/dist "servant-client"/dist "servant-client-core"/dist "servant-docs"/dist "servant-foreign"/dist "servant-server"/dist "doc/tutorial"/dist "doc/cookbook/basic-auth"/dist "doc/cookbook/curl-mock"/dist "doc/cookbook/db-postgres-pool"/dist "doc/cookbook/db-sqlite-simple"/dist "doc/cookbook/file-upload"/dist "doc/cookbook/generic"/dist "doc/cookbook/https"/dist "doc/cookbook/sentry"/dist "doc/cookbook/testing"/dist "doc/cookbook/structuring-apis"/dist "doc/cookbook/using-custom-monad"/dist "doc/cookbook/using-free-client"/dist
149+ - rm -rf .ghc.environment.* "servant"/dist "servant-client"/dist "servant-client-core"/dist "servant-docs"/dist "servant-foreign"/dist "servant-server"/dist "doc/tutorial"/dist "servant-machines"/dist "servant-conduit"/dist "servant-pipes"/dist "doc/cookbook/basic-auth"/dist "doc/cookbook/curl-mock"/dist "doc/cookbook/basic-streaming"/dist "doc/cookbook/db-postgres-pool"/dist "doc/cookbook/db-sqlite-simple"/dist "doc/cookbook/file-upload"/dist "doc/cookbook/generic"/dist "doc/cookbook/https"/dist "doc/cookbook/sentry"/dist "doc/cookbook/testing"/dist "doc/cookbook/structuring-apis"/dist "doc/cookbook/using-custom-monad"/dist "doc/cookbook/using-free-client"/dist
138150 - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
139151
140152# Here starts the actual work to be performed for the package under test;
@@ -148,11 +160,11 @@ script:
148160 - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
149161 - cd ${DISTDIR} || false
150162 - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
151− - "printf 'packages: servant-*/*.cabal servant-client-*/*.cabal servant-client-core-*/*.cabal servant-docs-*/*.cabal servant-foreign-*/*.cabal servant-server-*/*.cabal tutorial-*/*.cabal cookbook-basic-auth-*/*.cabal cookbook-curl-mock-*/*.cabal cookbook-db-postgres-pool-*/*.cabal cookbook-db-sqlite-simple-*/*.cabal cookbook-file-upload-*/*.cabal cookbook-generic-*/*.cabal cookbook-https-*/*.cabal cookbook-sentry-*/*.cabal cookbook-testing-*/*.cabal cookbook-structuring-apis-*/*.cabal cookbook-using-custom-monad-*/*.cabal cookbook-using-free-client-*/*.cabal\\n' > cabal.project"
163+ - "printf 'packages: servant-*/*.cabal servant-client-*/*.cabal servant-client-core-*/*.cabal servant-docs-*/*.cabal servant-foreign-*/*.cabal servant-server-*/*.cabal tutorial-*/*.cabal servant-machines-*/*.cabal servant-conduit-*/*.cabal servant-pipes-*/*.cabal cookbook-basic-auth-*/*.cabal cookbook-curl-mock-*/*.cabal cookbook-basic-streaming-*/*.cabal cookbook-db-postgres-pool-*/*.cabal cookbook-db-sqlite-simple-*/*.cabal cookbook-file-upload-*/*.cabal cookbook-generic-*/*.cabal cookbook-https-*/*.cabal cookbook-sentry-*/*.cabal cookbook-testing-*/*.cabal cookbook-structuring-apis-*/*.cabal cookbook-using-custom-monad-*/*.cabal cookbook-using-free-client-*/*.cabal\\n' > cabal.project"
152164 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
153165 - "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server,servant-js:base, servant-pagination:servant,servant-pagination:servant-server' >> cabal.project"
154166 - touch cabal.project.local
155− - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- servant | grep -vw -- servant-client | grep -vw -- servant-client-core | grep -vw -- servant-docs | grep -vw -- servant-foreign | grep -vw -- servant-server | grep -vw -- tutorial | grep -vw -- cookbook-basic-auth | grep -vw -- cookbook-curl-mock | grep -vw -- cookbook-db-postgres-pool | grep -vw -- cookbook-db-sqlite-simple | grep -vw -- cookbook-file-upload | grep -vw -- cookbook-generic | grep -vw -- cookbook-https | grep -vw -- cookbook-sentry | grep -vw -- cookbook-testing | grep -vw -- cookbook-structuring-apis | grep -vw -- cookbook-using-custom-monad | grep -vw -- cookbook-using-free-client | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
167+ - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- servant | grep -vw -- servant-client | grep -vw -- servant-client-core | grep -vw -- servant-docs | grep -vw -- servant-foreign | grep -vw -- servant-server | grep -vw -- tutorial | grep -vw -- servant-machines | grep -vw -- servant-conduit | grep -vw -- servant-pipes | grep -vw -- cookbook-basic-auth | grep -vw -- cookbook-curl-mock | grep -vw -- cookbook-basic-streaming | grep -vw -- cookbook-db-postgres-pool | grep -vw -- cookbook-db-sqlite-simple | grep -vw -- cookbook-file-upload | grep -vw -- cookbook-generic | grep -vw -- cookbook-https | grep -vw -- cookbook-sentry | grep -vw -- cookbook-testing | grep -vw -- cookbook-structuring-apis | grep -vw -- cookbook-using-custom-monad | grep -vw -- cookbook-using-free-client | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
156168 - cat cabal.project || true
157169 - cat cabal.project.local || true
158170 - echo -en 'travis_fold:end:unpack\\r'
cabal.project+5
@@ -6,10 +6,15 @@ packages: servant/
66 servant-server/
77 doc/tutorial/
88
9+ servant-machines/
10+ servant-conduit/
11+ servant-pipes/
12+
913 -- doc/cookbook/*/*.cabal
1014
1115 doc/cookbook/basic-auth
1216 doc/cookbook/curl-mock
17+ doc/cookbook/basic-streaming
1318 doc/cookbook/db-postgres-pool
1419 doc/cookbook/db-sqlite-simple
1520 doc/cookbook/file-upload
doc/cookbook/basic-streaming/Streaming.lhsnew file+126
@@ -0,0 +1,126 @@
1+# Streaming out-of-the-box
2+
3+In other words, without streaming libraries.
4+
5+## Introduction
6+
7+- Servant supports streaming
8+- Some basic usage doesn't require usage of streaming libraries,
9+ like `conduit`, `pipes`, `machines` or `streaming`.
10+ We have bindings for them though.
11+- This is similar example file, which is bundled with each of the packages (TODO: links)
12+- `SourceT` doesn't have *Prelude* with handy combinators, so we have to write
13+ things ourselves. (Note to self: `mapM` and `foldM` would be handy to have).
14+
15+## Code
16+
17+```haskell
18+{-# LANGUAGE BangPatterns #-}
19+{-# LANGUAGE DataKinds #-}
20+{-# LANGUAGE DeriveGeneric #-}
21+{-# LANGUAGE TypeOperators #-}
22+module Main (main) where
23+
24+import Control.Concurrent
25+ (threadDelay)
26+import Control.Monad.IO.Class
27+ (MonadIO (..))
28+import qualified Data.ByteString as BS
29+import Data.Maybe
30+ (fromMaybe)
31+import Network.HTTP.Client
32+ (defaultManagerSettings, newManager)
33+import Network.Wai
34+ (Application)
35+import System.Environment
36+ (getArgs, lookupEnv)
37+import Text.Read
38+ (readMaybe)
39+
40+import Servant
41+import Servant.Client
42+import qualified Servant.Types.SourceT as S
43+
44+import qualified Network.Wai.Handler.Warp as Warp
45+
46+type FastAPI = "get" :> Capture "num" Int :> StreamGet NewlineFraming JSON (SourceIO Int)
47+
48+type API = FastAPI
49+ :<|> "slow" :> Capture "num" Int :> StreamGet NewlineFraming JSON (SourceIO Int)
50+ -- monad can be ResourceT IO too.
51+ :<|> "readme" :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)
52+ -- we can have streaming request body
53+ :<|> "proxy"
54+ :> StreamBody NoFraming OctetStream (SourceIO BS.ByteString)
55+ :> StreamPost NoFraming OctetStream (SourceIO BS.ByteString)
56+
57+api :: Proxy API
58+api = Proxy
59+
60+server :: Server API
61+server = fast :<|> slow :<|> readme :<|> proxy where
62+ fast n = liftIO $ do
63+ putStrLn $ "/get/" ++ show n
64+ return $ fastSource n
65+
66+ slow n = liftIO $ do
67+ putStrLn $ "/slow/" ++ show n
68+ return $ slowSource n
69+
70+ readme = liftIO $ do
71+ putStrLn "/proxy"
72+ return (S.readFile "README.md")
73+
74+ proxy c = liftIO $ do
75+ putStrLn "/proxy"
76+ return c
77+
78+ -- for some reason unfold leaks?
79+ fastSource = S.fromStepT . mk where
80+ mk m
81+ | m < 0 = S.Stop
82+ | otherwise = S.Yield m (mk (m - 1))
83+
84+ slowSource m = S.mapStepT delay (fastSource m) where
85+ delay S.Stop = S.Stop
86+ delay (S.Error err) = S.Error err
87+ delay (S.Skip s) = S.Skip (delay s)
88+ delay (S.Effect ms) = S.Effect (fmap delay ms)
89+ delay (S.Yield x s) = S.Effect $
90+ S.Yield x (delay s) <$ threadDelay 1000000
91+
92+app :: Application
93+app = serve api server
94+
95+cli :: Client ClientM FastAPI
96+cli :<|> _ :<|> _ :<|> _ = client api
97+
98+main :: IO ()
99+main = do
100+ args <- getArgs
101+ case args of
102+ ("server":_) -> do
103+ putStrLn "Starting cookbook-basic-streaming at http://localhost:8000"
104+ port <- fromMaybe 8000 . (>>= readMaybe) <$> lookupEnv "PORT"
105+ Warp.run port app
106+ ("client":ns:_) -> do
107+ n <- maybe (fail $ "not a number: " ++ ns) pure $ readMaybe ns
108+ mgr <- newManager defaultManagerSettings
109+ burl <- parseBaseUrl "http://localhost:8000/"
110+ withClientM (cli n) (mkClientEnv mgr burl) $ \me -> case me of
111+ Left err -> print err
112+ Right src -> do
113+ x <- S.unSourceT src (go (0 :: Int))
114+ print x
115+ where
116+ go !acc S.Stop = return acc
117+ go !acc (S.Error err) = print err >> return acc
118+ go !acc (S.Skip s) = go acc s
119+ go !acc (S.Effect ms) = ms >>= go acc
120+ go !acc (S.Yield _ s) = go (acc + 1) s
121+ _ -> do
122+ putStrLn "Try:"
123+ putStrLn "cabal new-run cookbook-basic-streaming server"
124+ putStrLn "cabal new-run cookbook-basic-streaming client 10"
125+ putStrLn "time curl -H 'Accept: application/json' localhost:8000/slow/5"
126+```
doc/cookbook/basic-streaming/basic-streaming.cabalnew file+28
@@ -0,0 +1,28 @@
1+name: cookbook-basic-streaming
2+version: 2.1
3+synopsis: Streaming in servant without streaming libs
4+homepage: http://haskell-servant.readthedocs.org/
5+license: BSD3
6+license-file: ../../../servant/LICENSE
7+author: Servant Contributors
8+maintainer: haskell-servant-maintainers@googlegroups.com
9+build-type: Simple
10+cabal-version: >=1.10
11+tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.1
12+
13+executable cookbook-basic-streaming
14+ main-is: Streaming.lhs
15+ build-tool-depends: markdown-unlit:markdown-unlit
16+ default-language: Haskell2010
17+ ghc-options: -Wall -pgmL markdown-unlit -threaded -rtsopts
18+
19+ hs-source-dirs: .
20+ build-depends: base >= 4.8 && <4.13
21+ , aeson
22+ , bytestring
23+ , servant
24+ , servant-server
25+ , servant-client
26+ , http-client
27+ , wai
28+ , warp
doc/tutorial/ApiType.lhs+12−5
@@ -129,16 +129,23 @@ type UserAPI4 = "users" :> Get '[JSON] [User]
129129
130130### `StreamGet` and `StreamPost`
131131
132+*Note*: Streaming has changed considerably in `servant-0.15`.
133+
132134The `StreamGet` and `StreamPost` combinators are defined in terms of the more general `Stream`
133135
134136``` haskell ignore
135−data Stream (method :: k1) (framing :: *) (contentType :: *) a
136−type StreamGet = Stream 'GET
137−type StreamPost = Stream 'POST
138−```
137+data Stream (method :: k1) (status :: Nat) (framing :: *) (contentType :: *) (a :: *)
139138
140−These describe endpoints that return a stream of values rather than just a single value. They not only take a single content type as a parameter, but also a framing strategy -- this specifies how the individual results are delineated from one another in the stream. The three standard strategies given with Servant are `NewlineFraming`, `NetstringFraming` and `NoFraming`, but others can be written to match other protocols.
139+type StreamGet = Stream 'GET 200
140+type StreamPost = Stream 'POST 200
141+```
141142
143+These describe endpoints that return a stream of values rather than just a
144+single value. They not only take a single content type as a parameter, but also
145+a framing strategy -- this specifies how the individual results are delineated
146+from one another in the stream. The three standard strategies given with
147+Servant are `NewlineFraming`, `NetstringFraming` and `NoFraming`, but others
148+can be written to match other protocols.
142149
143150### `Capture`
144151
doc/tutorial/Client.lhs+21−21
@@ -20,6 +20,8 @@ import GHC.Generics
2020import Network.HTTP.Client (newManager, defaultManagerSettings)
2121import Servant.API
2222import Servant.Client
23+import Servant.Types.SourceT (foreach)
24+import Control.Monad.Codensity (Codensity)
2325```
2426
2527Also, we need examples for some domain specific data types:
@@ -217,41 +219,39 @@ getClients clientEnv
217219Consider the following streaming API type:
218220
219221``` haskell
220−type StreamAPI = "positionStream" :> StreamGet NewlineFraming JSON (ResultStream Position)
222+type StreamAPI = "positionStream" :> StreamGet NewlineFraming JSON (SourceIO Position)
221223```
222224
223−Note that when we declared an API to serve, we specified a `StreamGenerator` as a producer of streams. Now we specify our result type as a `ResultStream`. With types that can be used both ways, if appropriate adaptors are written (in the form of `ToStreamGenerator` and `BuildFromStream` instances), then this asymmetry isn't necessary. Otherwise, if you want to share the same API across clients and servers, you can parameterize it like so:
224−
225−``` haskell ignore
226−type StreamAPI f = "positionStream" :> StreamGet NewlineFraming JSON (f Position)
227−type ServerStreamAPI = StreamAPI StreamGenerator
228−type ClientStreamAPI = StreamAPI ResultStream
229−```
225+Note that we use the same `SourceIO` type as on the server-side
226+(this is different from `servant-0.14`).
230227
231228In any case, here's how we write a function to query our API:
232229
233−``` haskell
230+```haskell
234231streamAPI :: Proxy StreamAPI
235232streamAPI = Proxy
236233
237−posStream :: ClientM (ResultStream Position)
238−
234+posStream :: ClientM (Codensity IO (SourceIO Position))
239235posStream = client streamAPI
240236```
241237
242−And here's how to just print out all elements from a `ResultStream`, to give some idea of how to work with them.
238+We'll get back a `Codensity IO (SourceIO Position)`. The wrapping in
239+`Codensity` is generally necessary, as `Codensity` lets us `bracket` things
240+properly. This is best explained by an example. To consume `ClientM (Codentity
241+IO ...)` we can use `withClientM` helper: the underlying HTTP connection is
242+open only until the inner functions exits. Inside the block we can e.g just
243+print out all elements from a `SourceIO`, to give some idea of how to work with
244+them.
243245
244246``` haskell
245−printResultStream :: Show a => ResultStream a -> IO ()
246−printResultStream (ResultStream k) = k $ \getResult ->
247− let loop = do
248− r <- getResult
249− case r of
250− Nothing -> return ()
251− Just x -> print x >> loop
252− in loop
247+printSourceIO :: Show a => ClientEnv -> ClientM (Codensity IO (SourceIO a)) -> IO ()
248+printSourceIO env c = withClientM c env $ \e -> case e of
249+ Left err -> putStrLn $ "Error: " ++ show err
250+ Right rs -> foreach fail print rs
253251```
254252
255−The stream is parsed and provided incrementally. So the above loop prints out each result as soon as it is received on the stream, rather than waiting until they are all available to print them at once.
253+The stream is parsed and provided incrementally. So the above loop prints out
254+each result as soon as it is received on the stream, rather than waiting until
255+they are all available to print them at once.
256256
257257You now know how to use **servant-client**!
doc/tutorial/Server.lhs+22−8
@@ -46,6 +46,7 @@ import Servant
4646import System.Directory
4747import Text.Blaze
4848import Text.Blaze.Html.Renderer.Utf8
49+import Servant.Types.SourceT (source)
4950import qualified Data.Aeson.Parser
5051import qualified Text.Blaze.Html
5152```
@@ -1160,24 +1161,37 @@ app5 = serve readerAPI (hoistServer readerAPI funToHandler funServerT)
11601161
11611162## Streaming endpoints
11621163
1163−We can create endpoints that don't just give back a single result, but give back a *stream* of results, served one at a time. Stream endpoints only provide a single content type, and also specify what framing strategy is used to delineate the results. To serve these results, we need to give back a stream producer. Adapters can be written to `Pipes`, `Conduit` and the like, or written directly as `StreamGenerator`s. StreamGenerators are IO-based continuations that are handed two functions -- the first to write the first result back, and the second to write all subsequent results back. (This is to allow handling of situations where the entire stream is prefixed by a header, or where a boundary is written between elements, but not prior to the first element). The API of a streaming endpoint needs to explicitly specify which sort of generator it produces. Note that the generator itself is returned by a `Handler` action, so that additional IO may be done in the creation of one.
1164+We can create endpoints that don't just give back a single result, but give
1165+back a *stream* of results, served one at a time. Stream endpoints only provide
1166+a single content type, and also specify what framing strategy is used to
1167+delineate the results. To serve these results, we need to give back a stream
1168+producer. Adapters can be written to *Pipes*, *Conduit* and the like, or
1169+written directly as `SourceIO`s. SourceIO builts upon servant's own `SourceT`
1170+stream type (it's simpler than *Pipes* or *Conduit*).
1171+The API of a streaming endpoint needs to explicitly specify which sort of
1172+generator it produces. Note that the generator itself is returned by a
1173+`Handler` action, so that additional IO may be done in the creation of one.
11641174
11651175``` haskell
1166−type StreamAPI = "userStream" :> StreamGet NewlineFraming JSON (StreamGenerator User)
1176+type StreamAPI = "userStream" :> StreamGet NewlineFraming JSON (SourceIO User)
11671177streamAPI :: Proxy StreamAPI
11681178streamAPI = Proxy
11691179
1170−streamUsers :: StreamGenerator User
1171−streamUsers = StreamGenerator $ \sendFirst sendRest -> do
1172− sendFirst isaac
1173− sendRest albert
1174− sendRest albert
1180+streamUsers :: SourceIO User
1181+streamUsers = source [isaac, albert, albert]
11751182
11761183app6 :: Application
11771184app6 = serve streamAPI (return streamUsers)
11781185```
11791186
1180−This simple application returns a stream of `User` values encoded in JSON format, with each value separated by a newline. In this case, the stream will consist of the value of `isaac`, followed by the value of `albert`, then the value of `albert` a third time. Importantly, the stream is written back as results are produced, rather than all at once. This means first that results are delivered when they are available, and second, that if an exception interrupts production of the full stream, nonetheless partial results have already been written back.
1187+This simple application returns a stream of `User` values encoded in JSON
1188+format, with each value separated by a newline. In this case, the stream will
1189+consist of the value of `isaac`, followed by the value of `albert`, then the
1190+value of `albert` a second time. Importantly, the stream is written back as
1191+results are produced, rather than all at once. This means first that results
1192+are delivered when they are available, and second, that if an exception
1193+interrupts production of the full stream, nonetheless partial results have
1194+already been written back.
11811195
11821196## Conclusion
11831197
doc/tutorial/tutorial.cabal+1
@@ -46,6 +46,7 @@ library
4646 , http-client
4747 , http-media
4848 , http-types
49+ , kan-extensions
4950 , mtl
5051 , string-conversions
5152 , text
servant-client-core/servant-client-core.cabal+1
@@ -70,6 +70,7 @@ library
7070 , http-api-data >= 0.3.8.1 && < 0.4
7171 , http-media >= 0.7.1.2 && < 0.8
7272 , http-types >= 0.12.1 && < 0.13
73+ , kan-extensions >= 5.2 && < 5.3
7374 , network-uri >= 2.6.1.0 && < 2.7
7475 , safe >= 0.3.17 && < 0.4
7576
servant-client-core/src/Servant/Client/Core.hs+1−1
@@ -44,7 +44,7 @@ module Servant.Client.Core
4444 , GenResponse (..)
4545 , RunClient(..)
4646 , module Servant.Client.Core.Internal.BaseUrl
47− , StreamingResponse(..)
47+ , StreamingResponse
4848
4949 -- * Writing HasClient instances
5050 -- | These functions need not be re-exported by backend libraries.
servant-client-core/src/Servant/Client/Core/Internal/HasClient.hs+37−56
@@ -16,10 +16,7 @@ module Servant.Client.Core.Internal.HasClient where
1616import Prelude ()
1717import Prelude.Compat
1818
19−import Control.Concurrent
20− (modifyMVar, newMVar)
21−import Control.Monad.IO.Class
22− (MonadIO (..))
19+import qualified Data.ByteString as BS
2320import qualified Data.ByteString.Lazy as BL
2421import Data.Foldable
2522 (toList)
@@ -27,8 +24,6 @@ import Data.List
2724 (foldl')
2825import Data.Proxy
2926 (Proxy (Proxy))
30−import Data.Semigroup
31− ((<>))
3227import Data.Sequence
3328 (fromList)
3429import Data.String
@@ -40,19 +35,22 @@ import GHC.TypeLits
4035import qualified Network.HTTP.Types as H
4136import Servant.API
4237 ((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth, BasicAuthData,
43− BuildHeadersTo (..), ByteStringParser (..), Capture',
44− CaptureAll, Description, EmptyAPI, FramingUnrender (..),
45− FromResultStream (..), Header', Headers (..), HttpVersion,
46− IsSecure, MimeRender (mimeRender),
47− MimeUnrender (mimeUnrender), NoContent (NoContent), QueryFlag,
48− QueryParam', QueryParams, Raw, ReflectMethod (..), RemoteHost,
49− ReqBody', ResultStream (..), SBoolI, Stream, Summary,
50− ToHttpApiData, Vault, Verb, WithNamedContext, contentType,
51− getHeadersHList, getResponse, toQueryParam, toUrlPiece)
38+ BuildHeadersTo (..), Capture', CaptureAll, Description,
39+ EmptyAPI, FramingUnrender (..), FromSourceIO (..),
40+ Header', Headers (..), HttpVersion, IsSecure,
41+ MimeRender (mimeRender), MimeUnrender (mimeUnrender),
42+ NoContent (NoContent), QueryFlag, QueryParam', QueryParams,
43+ Raw, ReflectMethod (..), RemoteHost, ReqBody', SBoolI, Stream,
44+ StreamBody, Summary, ToHttpApiData, Vault, Verb,
45+ WithNamedContext, contentType, getHeadersHList, getResponse,
46+ toQueryParam, toUrlPiece)
5247import Servant.API.ContentTypes
5348 (contentTypes)
5449import Servant.API.Modifiers
5550 (FoldRequired, RequiredArgument, foldRequiredArgument)
51+import Control.Monad.Codensity
52+ (Codensity (..))
53+import qualified Servant.Types.SourceT as S
5654
5755import Servant.Client.Core.Internal.Auth
5856import Servant.Client.Core.Internal.BasicAuth
@@ -274,54 +272,25 @@ instance {-# OVERLAPPING #-}
274272 hoistClientMonad _ _ f ma = f ma
275273
276274instance {-# OVERLAPPABLE #-}
277− ( RunClient m, MonadIO m, MimeUnrender ct a, ReflectMethod method,
278− FramingUnrender framing a, FromResultStream a b
279− ) => HasClient m (Stream method status framing ct b) where
275+ ( RunClient m, MimeUnrender ct chunk, ReflectMethod method,
276+ FramingUnrender framing, FromSourceIO chunk a
277+ ) => HasClient m (Stream method status framing ct a) where
280278
281− type Client m (Stream method status framing ct b) = m b
279+ type Client m (Stream method status framing ct a) = m (Codensity IO a)
280+
281+ hoistClientMonad _ _ f ma = f ma
282282
283283 clientWithRoute _pm Proxy req = do
284− sresp <- streamingRequest req
284+ sresp <- streamingRequest req
285285 { requestAccept = fromList [contentType (Proxy :: Proxy ct)]
286286 , requestMethod = reflectMethod (Proxy :: Proxy method)
287287 }
288− liftIO $ fromResultStream $ ResultStream $ \k ->
289− runStreamingResponse sresp $ \gres -> do
290− let reader = responseBody gres
291− let unrender = unrenderFrames (Proxy :: Proxy framing) (Proxy :: Proxy a)
292− loop bs = do
293− res <- BL.fromStrict <$> reader
294− if BL.null res
295− then return $ parseEOF unrender res
296− else let sofar = (bs <> res)
297− in case parseIncremental unrender sofar of
298− Just x -> return x
299− Nothing -> loop sofar
300− (frameParser, remainder) <- loop BL.empty
301− state <- newMVar remainder
302− let frameLoop bs = do
303− res <- BL.fromStrict <$> reader
304− let addIsEmptyInfo (a, r) = (r, (a, BL.null r && BL.null res))
305− if BL.null res
306− then if BL.null bs
307− then return ("", (Right "", True))
308− else return . addIsEmptyInfo $ parseEOF frameParser bs
309− else let sofar = (bs <> res)
310− in case parseIncremental frameParser sofar of
311− Just x -> return $ addIsEmptyInfo x
312− Nothing -> frameLoop sofar
313−
314− go = processResult <$> modifyMVar state frameLoop
315− processResult (Right bs,isDone) =
316− if BL.null bs && isDone
317− then Nothing
318− else Just $ case mimeUnrender (Proxy :: Proxy ct) bs :: Either String a of
319− Left err -> Left err
320− Right x -> Right x
321− processResult (Left err, _) = Just (Left err)
322− k go
288+ return $ do
289+ let mimeUnrender' = mimeUnrender (Proxy :: Proxy ct) :: BL.ByteString -> Either String chunk
290+ framingUnrender' = framingUnrender (Proxy :: Proxy framing) mimeUnrender'
291+ gres <- sresp
292+ return $ fromSourceIO $ framingUnrender' $ S.fromAction BS.null (responseBody gres)
323293
324− hoistClientMonad _ _ f ma = f ma
325294
326295-- | If you use a 'Header' in one of your endpoints in your API,
327296-- the corresponding querying function will automatically take
@@ -573,6 +542,18 @@ instance (MimeRender ct a, HasClient m api)
573542 hoistClientMonad pm _ f cl = \a ->
574543 hoistClientMonad pm (Proxy :: Proxy api) f (cl a)
575544
545+instance
546+ ( HasClient m api
547+ ) => HasClient m (StreamBody framing ctype a :> api)
548+ where
549+
550+ type Client m (StreamBody framing ctype a :> api) = a -> Client m api
551+
552+ hoistClientMonad pm _ f cl = \a ->
553+ hoistClientMonad pm (Proxy :: Proxy api) f (cl a)
554+
555+ clientWithRoute _pm Proxy _req _body = error "HasClient @StreamBody"
556+
576557-- | Make the querying function append @path@ to the request path.
577558instance (KnownSymbol path, HasClient m api) => HasClient m (path :> api) where
578559 type Client m (path :> api) = Client m api
servant-client-core/src/Servant/Client/Core/Internal/Request.hs+3−1
@@ -38,6 +38,8 @@ import Network.HTTP.Media
3838import Network.HTTP.Types
3939 (Header, HeaderName, HttpVersion, Method, QueryItem, Status,
4040 http11, methodGet)
41+import Control.Monad.Codensity
42+ (Codensity (..))
4143import Web.HttpApiData
4244 (ToHttpApiData, toEncodedUrlPiece, toHeader)
4345
@@ -89,7 +91,7 @@ data GenResponse a = Response
8991 } deriving (Eq, Show, Generic, Typeable, Functor, Foldable, Traversable)
9092
9193type Response = GenResponse LBS.ByteString
92−newtype StreamingResponse = StreamingResponse { runStreamingResponse :: forall a. (GenResponse (IO BS.ByteString) -> IO a) -> IO a }
94+type StreamingResponse = Codensity IO (GenResponse (IO BS.ByteString))
9395
9496-- A GET request to the top-level path
9597defaultRequest :: Request
servant-client-core/src/Servant/Client/Core/Internal/RunClient.hs+1−1
@@ -26,7 +26,7 @@ import Servant.API
2626import Servant.Client.Core.Internal.ClientF
2727import Servant.Client.Core.Internal.Request
2828 (GenResponse (..), Request, Response, ServantError (..),
29− StreamingResponse (..))
29+ StreamingResponse)
3030
3131class Monad m => RunClient m where
3232 -- | How to make a request.
servant-client/servant-client.cabal+10−5
@@ -50,7 +50,8 @@ library
5050
5151 -- Servant dependencies
5252 build-depends:
53− servant-client-core == 0.14.*
53+ servant == 0.14.*
54+ , servant-client-core == 0.14.*
5455
5556 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
5657 -- Here can be exceptions if we really need features from the newer versions.
@@ -60,6 +61,7 @@ library
6061 , http-media >= 0.7.1.2 && < 0.8
6162 , http-types >= 0.12.1 && < 0.13
6263 , exceptions >= 0.10.0 && < 0.11
64+ , kan-extensions >= 5.2 && < 5.3
6365 , monad-control >= 1.0.2.3 && < 1.1
6466 , semigroupoids >= 5.2.2 && < 5.4
6567 , stm >= 2.4.5.0 && < 2.6
@@ -72,7 +74,7 @@ library
7274
7375test-suite spec
7476 type: exitcode-stdio-1.0
75− ghc-options: -Wall -rtsopts -with-rtsopts=-T
77+ ghc-options: -Wall -rtsopts -threaded "-with-rtsopts=-T -N2"
7678 default-language: Haskell2010
7779 hs-source-dirs: test
7880 main-is: Spec.hs
@@ -90,6 +92,7 @@ test-suite spec
9092 , http-client
9193 , http-types
9294 , mtl
95+ , kan-extensions
9396 , servant-client
9497 , servant-client-core
9598 , text
@@ -100,16 +103,18 @@ test-suite spec
100103
101104 -- Additonal dependencies
102105 build-depends:
103− generics-sop >= 0.4.0.1 && < 0.5
104− , hspec >= 2.5.1 && < 2.6
106+ entropy >= 0.4.1.3 && < 0.5
107+ , generics-sop >= 0.4.0.1 && < 0.5
108+ , hspec >= 2.5.8 && < 2.6
105109 , HUnit >= 1.6 && < 1.7
106110 , network >= 2.8.0.0 && < 2.9
107111 , QuickCheck >= 2.12.6.1 && < 2.13
108112 , servant == 0.14.*
109113 , servant-server == 0.14.*
114+ , tdigest >= 0.2 && < 0.3
110115
111116 build-tool-depends:
112− hspec-discover:hspec-discover >= 2.5.1 && < 2.6
117+ hspec-discover:hspec-discover >= 2.5.8 && < 2.6
113118
114119test-suite readme
115120 type: exitcode-stdio-1.0
servant-client/src/Servant/Client.hs+1
@@ -7,6 +7,7 @@ module Servant.Client
77 ( client
88 , ClientM
99 , runClientM
10+ , withClientM
1011 , ClientEnv(..)
1112 , mkClientEnv
1213 , hoistClient
servant-client/src/Servant/Client/Internal/HttpClient.hs+14−1
@@ -21,6 +21,8 @@ import Control.Monad.Base
2121 (MonadBase (..))
2222import Control.Monad.Catch
2323 (MonadCatch, MonadThrow)
24+import Control.Monad.Codensity
25+ (Codensity (..))
2426import Control.Monad.Error.Class
2527 (MonadError (..))
2628import Control.Monad.Reader
@@ -141,6 +143,17 @@ instance ClientLike (ClientM a) (ClientM a) where
141143runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
142144runClientM cm env = runExceptT $ flip runReaderT env $ unClientM cm
143145
146+withClientM
147+ :: ClientM (Codensity IO a) -- ^ client with codensity result
148+ -> ClientEnv -- ^ environment
149+ -> (Either ServantError a -> IO b) -- ^ continuation
150+ -> IO b
151+withClientM cm env k = do
152+ e <- runExceptT (runReaderT (unClientM cm) env)
153+ case e of
154+ Left err -> k (Left err)
155+ Right cod -> runCodensity cod (k . Right)
156+
144157performRequest :: Request -> ClientM Response
145158performRequest req = do
146159 ClientEnv m burl cookieJar' <- ask
@@ -178,7 +191,7 @@ performStreamingRequest req = do
178191 m <- asks manager
179192 burl <- asks baseUrl
180193 let request = requestToClientRequest burl req
181− return $ StreamingResponse $
194+ return $ Codensity $
182195 \k -> Client.withResponse request m $
183196 \r -> do
184197 let status = Client.responseStatus r
servant-client/test/Servant/StreamSpec.hs+94−66
@@ -22,32 +22,38 @@
2222module Servant.StreamSpec (spec) where
2323
2424import Control.Monad
25− (replicateM_, void)
26−import qualified Data.ByteString as BS
25+ (when)
26+import Control.Monad.Codensity
27+ (Codensity (..))
28+import Control.Monad.IO.Class
29+ (MonadIO (..))
30+import Control.Monad.Trans.Except
31+import qualified Data.ByteString as BS
2732import Data.Proxy
28−import qualified Network.HTTP.Client as C
33+import qualified Data.TDigest as TD
34+import qualified Network.HTTP.Client as C
2935import Prelude ()
3036import Prelude.Compat
31−import System.IO
32− (IOMode (ReadMode), withFile)
33−import System.IO.Unsafe
34− (unsafePerformIO)
35−import Test.Hspec
36−import Test.QuickCheck
37−
3837import Servant.API
3938 ((:<|>) ((:<|>)), (:>), JSON, NetstringFraming,
40− NewlineFraming, NoFraming, OctetStream, ResultStream (..),
41− StreamGenerator (..), StreamGet)
39+ NewlineFraming, NoFraming, OctetStream, SourceIO, StreamGet)
4240import Servant.Client
4341import Servant.ClientSpec
4442 (Person (..))
45−import qualified Servant.ClientSpec as CS
43+import qualified Servant.ClientSpec as CS
4644import Servant.Server
45+import Servant.Types.SourceT
46+import System.Entropy
47+ (getEntropy, getHardwareEntropy)
48+import System.IO.Unsafe
49+ (unsafePerformIO)
50+import System.Mem
51+ (performGC)
52+import Test.Hspec
4753
4854#if MIN_VERSION_base(4,10,0)
4955import GHC.Stats
50− (gc, gcdetails_mem_in_use_bytes, getRTSStats)
56+ (gc, gcdetails_live_bytes, getRTSStats)
5157#else
5258import GHC.Stats
5359 (currentBytesUsed, getGCStats)
@@ -57,21 +63,17 @@ spec :: Spec
5763spec = describe "Servant.Stream" $ do
5864 streamSpec
5965
60−type StreamApi f =
61− "streamGetNewline" :> StreamGet NewlineFraming JSON (f Person)
62− :<|> "streamGetNetstring" :> StreamGet NetstringFraming JSON (f Person)
63− :<|> "streamALot" :> StreamGet NoFraming OctetStream (f BS.ByteString)
64−
65−
66−capi :: Proxy (StreamApi ResultStream)
67−capi = Proxy
66+type StreamApi =
67+ "streamGetNewline" :> StreamGet NewlineFraming JSON (SourceIO Person)
68+ :<|> "streamGetNetstring" :> StreamGet NetstringFraming JSON (SourceIO Person)
69+ :<|> "streamALot" :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)
6870
69−sapi :: Proxy (StreamApi StreamGenerator)
70−sapi = Proxy
71+api :: Proxy StreamApi
72+api = Proxy
7173
72−getGetNL, getGetNS :: ClientM (ResultStream Person)
73−getGetALot :: ClientM (ResultStream BS.ByteString)
74−getGetNL :<|> getGetNS :<|> getGetALot = client capi
74+getGetNL, getGetNS :: ClientM (Codensity IO (SourceIO Person))
75+getGetALot :: ClientM (Codensity IO (SourceIO BS.ByteString))
76+getGetNL :<|> getGetNS :<|> getGetALot = client api
7577
7678alice :: Person
7779alice = Person "Alice" 42
@@ -80,25 +82,23 @@ bob :: Person
8082bob = Person "Bob" 25
8183
8284server :: Application
83−server = serve sapi
84− $ return (StreamGenerator (\f r -> f alice >> r bob >> r alice))
85− :<|> return (StreamGenerator (\f r -> f alice >> r bob >> r alice))
86− :<|> return (StreamGenerator lotsGenerator)
87− where
88− lotsGenerator f r = do
89− void $ f ""
90− void $ withFile "/dev/urandom" ReadMode $
91− \handle -> streamFiveMBNTimes handle 1000 r
92− return ()
93−
94− streamFiveMBNTimes handle left sink
95− | left <= (0 :: Int) = return ()
96− | otherwise = do
97− msg <- BS.hGet handle (megabytes 5)
98− _ <- sink msg
99− streamFiveMBNTimes handle (left - 1) sink
85+server = serve api
86+ $ return (source [alice, bob, alice])
87+ :<|> return (source [alice, bob, alice])
10088
89+ -- 2 ^ (18 + 10) = 256M
90+ :<|> return (SourceT ($ lots (powerOfTwo 18)))
91+ where
92+ lots n
93+ | n < 0 = Stop
94+ | otherwise = Effect $ do
95+ let size = powerOfTwo 10
96+ mbs <- getHardwareEntropy size
97+ bs <- maybe (getEntropy size) pure mbs
98+ return (Yield bs (lots (n - 1)))
10199
100+powerOfTwo :: Int -> Int
101+powerOfTwo = (2 ^)
102102
103103{-# NOINLINE manager' #-}
104104manager' :: C.Manager
@@ -107,41 +107,69 @@ manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings
107107runClient :: ClientM a -> BaseUrl -> IO (Either ServantError a)
108108runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')
109109
110−testRunResultStream :: ResultStream a
111− -> IO ( Maybe (Either String a)
112− , Maybe (Either String a)
113− , Maybe (Either String a)
114− , Maybe (Either String a))
115−testRunResultStream (ResultStream k)
116− = k $ \act -> (,,,) <$> act <*> act <*> act <*> act
110+testRunSourceIO :: Codensity IO (SourceIO a)
111+ -> IO (Either String [a])
112+testRunSourceIO = runExceptT . runSourceT . joinCodensitySourceT
113+
114+joinCodensitySourceT :: Codensity m (SourceT m a) -> SourceT m a
115+joinCodensitySourceT cod =
116+ SourceT $ \r ->
117+ runCodensity cod $ \src ->
118+ unSourceT src r
117119
118120streamSpec :: Spec
119121streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do
120−
121122 it "works with Servant.API.StreamGet.Newline" $ \(_, baseUrl) -> do
122− Right res <- runClient getGetNL baseUrl
123− let jra = Just (Right alice)
124− jrb = Just (Right bob)
125− testRunResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
123+ Right res <- runClient getGetNL baseUrl
124+ testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
126125
127126 it "works with Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do
128− Right res <- runClient getGetNS baseUrl
129− let jra = Just (Right alice)
130− jrb = Just (Right bob)
131− testRunResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
127+ Right res <- runClient getGetNS baseUrl
128+ testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
132129
133130{-
134131 it "streams in constant memory" $ \(_, baseUrl) -> do
135− Right (ResultStream res) <- runClient getGetALot baseUrl
136− let consumeNChunks n = replicateM_ n (res void)
137− consumeNChunks 900
132+ Right rs <- runClient getGetALot baseUrl
133+ performGC
134+ -- usage0 <- getUsage
135+ -- putStrLn $ "Start: " ++ show usage0
136+ tdigest <- memoryUsage $ joinCodensitySourceT rs
137+
138+ -- putStrLn $ "Median: " ++ show (TD.median tdigest)
139+ -- putStrLn $ "Mean: " ++ show (TD.mean tdigest)
140+ -- putStrLn $ "Stddev: " ++ show (TD.stddev tdigest)
141+
142+ -- forM_ [0.01, 0.1, 0.2, 0.5, 0.8, 0.9, 0.99] $ \q ->
143+ -- putStrLn $ "q" ++ show q ++ ": " ++ show (TD.quantile q tdigest)
144+
145+ let Just stddev = TD.stddev tdigest
146+
147+ -- standard deviation of 100k is ok, we generate 256M of data after all.
148+ -- On my machine deviation is 40k-50k
149+ stddev `shouldSatisfy` (< 100000)
150+
151+memoryUsage :: SourceT IO BS.ByteString -> IO (TD.TDigest 25)
152+memoryUsage src = unSourceT src $ loop mempty (0 :: Int)
153+ where
154+ loop !acc !_ Stop = return acc
155+ loop !_ !_ (Error err) = fail err -- !
156+ loop !acc !n (Skip s) = loop acc n s
157+ loop !acc !n (Effect ms) = ms >>= loop acc n
158+ loop !acc !n (Yield _bs s) = do
159+ usage <- liftIO getUsage
160+ -- We perform GC in between as we generate garbage.
161+ when (n `mod` 1024 == 0) $ liftIO performGC
162+ loop (TD.insert usage acc) (n + 1) s
163+
164+getUsage :: IO Double
165+getUsage = fromIntegral .
138166#if MIN_VERSION_base(4,10,0)
139− memUsed <- gcdetails_mem_in_use_bytes . gc <$> getRTSStats
167+ gcdetails_live_bytes . gc <$> getRTSStats
140168#else
141− memUsed <- currentBytesUsed <$> getGCStats
169+ currentBytesUsed <$> getGCStats
142170#endif
143171 memUsed `shouldSatisfy` (< megabytes 22)
144−-}
145172
146173megabytes :: Num a => a -> a
147174megabytes n = n * (1000 ^ (2 :: Int))
175+-}
servant-conduit/CHANGELOG.mdnew file+16
@@ -0,0 +1,16 @@
1+0.10
2+----
3+
4+* `EncodeOpts` and `DecodeOpts` merged into just `EncodeOpts`.
5+* Add `TabSeparatedOpts` for `text/tab-separated-values`.
6+
7+0.9
8+---
9+
10+* Refactorerd `CSV`' type.
11+
12+0.8
13+---
14+
15+* Removed DefaultEncodeOpts and DefaultDecodeOpts in favor of a single
16+ (new) DefaultOpts
servant-conduit/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2018, Servant Contributors
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Servant Contributors nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
servant-conduit/README.mdnew file+3
@@ -0,0 +1,3 @@
1+# servant-conduit - Servant Stream support for conduit
2+
3+![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
servant-conduit/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
servant-conduit/example/Main.hsnew file+103
@@ -0,0 +1,103 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE TypeOperators #-}
4+module Main (main) where
5+
6+import Prelude ()
7+import Prelude.Compat
8+
9+import Control.Concurrent
10+ (threadDelay)
11+import Control.Monad.IO.Class
12+ (MonadIO (..))
13+import Control.Monad.Trans.Resource
14+ (ResourceT)
15+import qualified Data.ByteString as BS
16+import Data.Maybe
17+ (fromMaybe)
18+import Network.HTTP.Client
19+ (defaultManagerSettings, newManager)
20+import Network.Wai
21+ (Application)
22+import System.Environment
23+ (getArgs, lookupEnv)
24+import Text.Read
25+ (readMaybe)
26+
27+import Data.Conduit
28+import qualified Data.Conduit.Combinators as C
29+import Servant
30+import Servant.Client
31+import Servant.Conduit ()
32+
33+import qualified Network.Wai.Handler.Warp as Warp
34+
35+type FastAPI = "get" :> Capture "num" Int :> StreamGet NewlineFraming JSON (ConduitT () Int IO ())
36+
37+type API = FastAPI
38+ :<|> "slow" :> Capture "num" Int :> StreamGet NewlineFraming JSON (ConduitT () Int IO ())
39+ -- monad can be ResourceT IO too.
40+ :<|> "readme" :> StreamGet NoFraming OctetStream (ConduitT () BS.ByteString (ResourceT IO) ())
41+ -- we can have streaming request body
42+ :<|> "proxy"
43+ :> StreamBody NoFraming OctetStream (ConduitT () BS.ByteString IO ())
44+ :> StreamPost NoFraming OctetStream (ConduitT () BS.ByteString IO ())
45+
46+api :: Proxy API
47+api = Proxy
48+
49+server :: Server API
50+server = fast :<|> slow :<|> readme :<|> proxy
51+ where
52+ fast n = liftIO $ do
53+ putStrLn $ "/get/" ++ show n
54+ return $ fastConduit n
55+
56+ slow n = liftIO $ do
57+ putStrLn $ "/slow/" ++ show n
58+ return $ slowConduit n
59+
60+ readme = liftIO $ do
61+ putStrLn "/proxy"
62+ return (C.sourceFile "README.md")
63+
64+ proxy c = liftIO $ do
65+ putStrLn "/proxy"
66+ return c
67+
68+ -- for some reason unfold leaks?
69+ fastConduit = C.unfold mk where
70+ mk m
71+ | m < 0 = Nothing
72+ | otherwise = Just (m, pred m)
73+
74+ slowConduit m = fastConduit m .| C.mapM (<$ threadDelay 1000000)
75+
76+app :: Application
77+app = serve api server
78+
79+cli :: Client ClientM FastAPI
80+cli :<|> _ :<|> _ :<|> _ = client api
81+
82+main :: IO ()
83+main = do
84+ args <- getArgs
85+ case args of
86+ ("server":_) -> do
87+ putStrLn "Starting servant-conduit:example at http://localhost:8000"
88+ port <- fromMaybe 8000 . (>>= readMaybe) <$> lookupEnv "PORT"
89+ Warp.run port app
90+ ("client":ns:_) -> do
91+ n <- maybe (fail $ "not a number: " ++ ns) pure $ readMaybe ns
92+ mgr <- newManager defaultManagerSettings
93+ burl <- parseBaseUrl "http://localhost:8000/"
94+ withClientM (cli n) (mkClientEnv mgr burl) $ \me -> case me of
95+ Left err -> print err
96+ Right c -> do
97+ x <- connect c $ C.foldl (\p _ -> p + 1) (0 :: Int)
98+ print x
99+ _ -> do
100+ putStrLn "Try:"
101+ putStrLn "cabal new-run servant-conduit:example server"
102+ putStrLn "cabal new-run servant-conduit:example client 10"
103+ putStrLn "time curl -H 'Accept: application/json' localhost:8000/slow/5"
servant-conduit/servant-conduit.cabalnew file+59
@@ -0,0 +1,59 @@
1+name: servant-conduit
2+version: 1
3+synopsis: Servant Stream support for conduit.
4+description: Servant Stream support for conduit.
5+homepage: http://haskell-servant.readthedocs.org/
6+license: BSD3
7+license-file: LICENSE
8+author: Servant Contributors
9+maintainer: haskell-servant-maintainers@googlegroups.com
10+copyright: 2018 Servant Contributors
11+category: Web, Servant, Enumerator
12+build-type: Simple
13+cabal-version: >=1.10
14+bug-reports: http://github.com/haskell-servant/servant-conduit/issues
15+tested-with:
16+ GHC ==8.0.2
17+ || ==8.2.2
18+ || ==8.4.4
19+ || ==8.6.1
20+
21+source-repository head
22+ type: git
23+ location: http://github.com/haskell-servant/servant-conduit.git
24+
25+library
26+ exposed-modules: Servant.Conduit
27+ build-depends:
28+ base >=4.9 && <5
29+ , bytestring >=0.10.4.0 && <0.11
30+ , conduit >=1.3.0.2 && <1.4
31+ , mtl >=2.1 && <2.3
32+ , resourcet >=1.2.1 && <1.3
33+ , servant >=0.14 && <0.15
34+ , unliftio-core >=0.1.1 && <0.2
35+ hs-source-dirs: src
36+ default-language: Haskell2010
37+ ghc-options: -Wall
38+
39+test-suite example
40+ type: exitcode-stdio-1.0
41+ main-is: Main.hs
42+ hs-source-dirs:
43+ example
44+ ghc-options: -Wall -rtsopts -threaded
45+ build-depends:
46+ base
47+ , base-compat
48+ , bytestring
49+ , conduit
50+ , http-media
51+ , resourcet
52+ , servant
53+ , servant-conduit
54+ , servant-server >=0.14 && <0.15
55+ , servant-client >=0.14 && <0.15
56+ , wai >=3.0.3.0 && <3.3
57+ , warp >=3.0.13.1 && <3.3
58+ , http-client
59+ default-language: Haskell2010
servant-conduit/src/Servant/Conduit.hsnew file+74
@@ -0,0 +1,74 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE MultiParamTypeClasses #-}
5+{-# LANGUAGE RankNTypes #-}
6+{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE UndecidableInstances #-}
8+{-# OPTIONS_GHC -Wno-orphans #-}
9+-- | TBW
10+--
11+-- This module exports 'ToSourceIO' and 'FromSourceIO' instances.
12+module Servant.Conduit (
13+ ConduitToSourceIO (..),
14+ ) where
15+
16+import Control.Monad.IO.Class
17+ (MonadIO (..))
18+import Control.Monad.IO.Unlift
19+ (MonadUnliftIO (..))
20+import Control.Monad.Trans.Resource
21+ (ResourceT, runResourceT)
22+import Data.Conduit.Internal
23+ (ConduitT (..), Pipe (..))
24+import Servant.API.Stream
25+import qualified Servant.Types.SourceT as S
26+
27+-- | Helper class to implement @'ToSourceIO' 'ConduitT'@ instance
28+-- for various monads.
29+class ConduitToSourceIO m where
30+ conduitToSourceIO :: ConduitT i o m () -> SourceIO o
31+
32+instance ConduitToSourceIO IO where
33+ conduitToSourceIO (ConduitT con) = S.SourceT ($ go (con Done)) where
34+ go p0 = case p0 of
35+ Done () -> S.Stop
36+ HaveOutput p o -> S.Yield o (go p)
37+ NeedInput _ip up -> S.Skip (go (up ()))
38+ PipeM m -> S.Effect $ fmap go m
39+ Leftover p _l -> S.Skip (go p)
40+
41+instance m ~ IO => ConduitToSourceIO (ResourceT m) where
42+ conduitToSourceIO (ConduitT con) =
43+ S.SourceT $ \k ->
44+ runResourceT $ withRunInIO $ \runRes ->
45+ k (go runRes (con Done))
46+ where
47+ go :: (forall x. ResourceT m x -> m x)
48+ -> Pipe i i o () (ResourceT m) ()
49+ -> S.StepT IO o
50+ go _ (Done ()) = S.Stop
51+ go runRes (HaveOutput p o) = S.Yield o (go runRes p)
52+ go runRes (NeedInput _ip up) = S.Skip (go runRes (up ()))
53+ go runRes (PipeM m) = S.Effect $ runRes $ fmap (go runRes) m
54+ go runRes (Leftover p _l) = S.Skip (go runRes p)
55+
56+instance (ConduitToSourceIO m, r ~ ())
57+ => ToSourceIO o (ConduitT i o m r)
58+ where
59+ toSourceIO = conduitToSourceIO
60+
61+instance (MonadIO m, r ~ ()) => FromSourceIO o (ConduitT i o m r) where
62+ fromSourceIO src =
63+ ConduitT $ \con ->
64+ PipeM $ liftIO $ S.unSourceT src $ \step ->
65+ loop con step
66+ where
67+ loop :: MonadIO m => (() -> Pipe i i o () m b) -> S.StepT IO o -> IO (Pipe i i o () m b)
68+ loop con S.Stop = return (con ())
69+ loop _con (S.Error err) = fail err
70+ loop con (S.Skip s) = loop con s
71+ loop con (S.Effect ms) = ms >>= loop con
72+ loop con (S.Yield x s) = return (HaveOutput (PipeM (liftIO $ loop con s)) x)
73+
74+ {-# SPECIALIZE INLINE fromSourceIO :: SourceIO o -> ConduitT i o IO () #-}
servant-docs/src/Servant/Docs/Internal.hs+6−2
@@ -853,9 +853,10 @@ instance {-# OVERLAPPABLE #-}
853853-- | TODO: mention the endpoint is streaming, its framing strategy
854854--
855855-- Also there are no samples.
856+--
857+-- TODO: AcceptFraming for content-type
856858instance {-# OVERLAPPABLE #-}
857− (MimeRender ct a, KnownNat status
858− , ReflectMethod method)
859+ (Accept ct, KnownNat status, ReflectMethod method)
859860 => HasDocs (Stream method status framing ct a) where
860861 docsFor Proxy (endpoint, action) DocOptions{..} =
861862 single endpoint' action'
@@ -968,6 +969,9 @@ instance (ToSample a, AllMimeRender (ct ': cts) a, HasDocs api)
968969 t = Proxy :: Proxy (ct ': cts)
969970 p = Proxy :: Proxy a
970971
972+instance HasDocs api => HasDocs (StreamBody framing ctype a :> api) where
973+ docsFor Proxy _ _ = error "HasDocs @StreamBody"
974+
971975instance (KnownSymbol path, HasDocs api) => HasDocs (path :> api) where
972976
973977 docsFor Proxy (endpoint, action) =
servant-foreign/src/Servant/Foreign/Internal.hs+8
@@ -322,6 +322,14 @@ instance (Elem JSON list, HasForeignType lang ftype a, HasForeign lang ftype api
322322 foreignFor lang ftype (Proxy :: Proxy api) $
323323 req & reqBody .~ (Just $ typeFor lang ftype (Proxy :: Proxy a))
324324
325+instance
326+ ( HasForeign lang ftype api
327+ ) => HasForeign lang ftype (StreamBody framing ctype a :> api)
328+ where
329+ type Foreign ftype (StreamBody framing ctype a :> api) = Foreign ftype api
330+
331+ foreignFor _lang Proxy Proxy _req = error "HasForeign @StreamBody"
332+
325333instance (KnownSymbol path, HasForeign lang ftype api)
326334 => HasForeign lang ftype (path :> api) where
327335 type Foreign ftype (path :> api) = Foreign ftype api
servant-foreign/test/Servant/ForeignSpec.hs+5
@@ -16,6 +16,8 @@ import Data.Monoid
1616import Data.Proxy
1717import Servant.API.Internal.Test.ComprehensiveAPI
1818import Servant.Foreign
19+import Servant.Types.SourceT
20+ (SourceT)
1921
2022import Test.Hspec
2123
@@ -51,6 +53,9 @@ instance HasForeignType LangX String (Headers ctyps NoContent) where
5153instance HasForeignType LangX String Int where
5254 typeFor _ _ _ = "intX"
5355
56+instance HasForeignType LangX String (SourceT m a) where
57+ typeFor _ _ _ = "streamTX"
58+
5459instance HasForeignType LangX String Bool where
5560 typeFor _ _ _ = "boolX"
5661
servant-machines/CHANGELOG.mdnew file+16
@@ -0,0 +1,16 @@
1+0.10
2+----
3+
4+* `EncodeOpts` and `DecodeOpts` merged into just `EncodeOpts`.
5+* Add `TabSeparatedOpts` for `text/tab-separated-values`.
6+
7+0.9
8+---
9+
10+* Refactorerd `CSV`' type.
11+
12+0.8
13+---
14+
15+* Removed DefaultEncodeOpts and DefaultDecodeOpts in favor of a single
16+ (new) DefaultOpts
servant-machines/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2018, Servant Contributors
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Servant Contributors nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
servant-machines/README.mdnew file+3
@@ -0,0 +1,3 @@
1+# servant-machines - Servant Stream support for machines
2+
3+![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
servant-machines/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
servant-machines/example/Main.hsnew file+98
@@ -0,0 +1,98 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE TypeOperators #-}
4+module Main (main) where
5+
6+import Prelude ()
7+import Prelude.Compat
8+
9+import Control.Concurrent
10+ (threadDelay)
11+import Control.Monad.IO.Class
12+ (MonadIO (..))
13+import qualified Data.ByteString as BS
14+import Data.Maybe
15+ (fromMaybe)
16+import Data.Void
17+ (Void)
18+import Network.HTTP.Client
19+ (defaultManagerSettings, newManager)
20+import Network.Wai
21+ (Application)
22+import System.Environment
23+ (getArgs, lookupEnv)
24+import Text.Read
25+ (readMaybe)
26+
27+import Data.Machine
28+import Servant
29+import Servant.Client
30+import Servant.Machines ()
31+
32+import qualified Network.Wai.Handler.Warp as Warp
33+
34+type FastAPI = "get" :> Capture "num" Int :> StreamGet NewlineFraming JSON (MachineT IO (Is Void) Int)
35+
36+type API = FastAPI
37+ :<|> "slow" :> Capture "num" Int :> StreamGet NewlineFraming JSON (MachineT IO (Is Void) Int)
38+ :<|> "proxy"
39+ :> StreamBody NoFraming OctetStream (MachineT IO (Is Void) BS.ByteString)
40+ :> StreamPost NoFraming OctetStream (MachineT IO (Is Void) BS.ByteString)
41+
42+api :: Proxy API
43+api = Proxy
44+
45+server :: Server API
46+server = fast :<|> slow :<|> proxy
47+ where
48+ fast n = liftIO $ do
49+ putStrLn ("/get/" ++ show n)
50+ return $ fastMachine n
51+
52+ slow n = liftIO $ do
53+ putStrLn ("/slow/" ++ show n)
54+ return $ slowMachine n
55+
56+ proxy c = liftIO $ do
57+ putStrLn "/proxy"
58+ return c
59+
60+ -- for some reason unfold leaks?
61+ fastMachine m
62+ | m < 0 = MachineT (return Stop)
63+ | otherwise = MachineT (return (Yield m (fastMachine (m - 1))))
64+
65+ slowMachine m
66+ | m < 0 = MachineT (return Stop)
67+ | otherwise = MachineT $ do
68+ threadDelay 1000000
69+ return (Yield m (slowMachine (m - 1)))
70+
71+app :: Application
72+app = serve api server
73+
74+cli :: Client ClientM FastAPI
75+cli :<|> _ = client api
76+
77+main :: IO ()
78+main = do
79+ args <- getArgs
80+ case args of
81+ ("server":_) -> do
82+ putStrLn "Starting servant-machines:example at http://localhost:8000"
83+ port <- fromMaybe 8000 . (>>= readMaybe) <$> lookupEnv "PORT"
84+ Warp.run port app
85+ ("client":ns:_) -> do
86+ n <- maybe (fail $ "not a number: " ++ ns) pure $ readMaybe ns
87+ mgr <- newManager defaultManagerSettings
88+ burl <- parseBaseUrl "http://localhost:8000/"
89+ withClientM (cli n) (mkClientEnv mgr burl) $ \me -> case me of
90+ Left err -> print err
91+ Right m -> do
92+ x <- runT $ fold (\p _ -> p + 1) (0 :: Int) <~ m
93+ print x
94+ _ -> do
95+ putStrLn "Try:"
96+ putStrLn "cabal new-run servant-machines:example server"
97+ putStrLn "cabal new-run servant-machines:example client 10"
98+ putStrLn "time curl -H 'Accept: application/json' localhost:8000/slow/5"
servant-machines/servant-machines.cabalnew file+56
@@ -0,0 +1,56 @@
1+name: servant-machines
2+version: 1
3+synopsis: Servant Stream support for machines
4+description: Servant Stream support for machines.
5+homepage: http://haskell-servant.readthedocs.org/
6+license: BSD3
7+license-file: LICENSE
8+author: Servant Contributors
9+maintainer: haskell-servant-maintainers@googlegroups.com
10+copyright: 2018 Servant Contributors
11+category: Web, Servant, Enumerator
12+build-type: Simple
13+cabal-version: >=1.10
14+bug-reports: http://github.com/haskell-servant/servant-machines/issues
15+tested-with:
16+ GHC ==8.0.2
17+ || ==8.2.2
18+ || ==8.4.4
19+ || ==8.6.1
20+
21+source-repository head
22+ type: git
23+ location: http://github.com/haskell-servant/servant-machines.git
24+
25+library
26+ exposed-modules: Servant.Machines
27+ build-depends:
28+ base >=4.9 && <5
29+ , bytestring >=0.10.4.0 && <0.11
30+ , machines >=0.6.3 && <0.7
31+ , mtl >=2.1 && <2.3
32+ , servant >=0.14 && <0.15
33+ hs-source-dirs: src
34+ default-language: Haskell2010
35+ ghc-options: -Wall
36+
37+test-suite example
38+ type: exitcode-stdio-1.0
39+ main-is: Main.hs
40+ hs-source-dirs:
41+ example
42+ ghc-options: -Wall -rtsopts -threaded
43+ build-depends:
44+ base
45+ , base-compat
46+ , bytestring
47+ , http-media
48+ , servant
49+ , machines
50+ , servant-machines
51+ , servant-server >=0.14 && <0.15
52+ , servant-client >=0.14 && <0.15
53+ , wai >=3.0.3.0 && <3.3
54+ , warp >=3.0.13.1 && <3.3
55+ , http-client
56+ default-language: Haskell2010
servant-machines/src/Servant/Machines.hsnew file+48
@@ -0,0 +1,48 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE MultiParamTypeClasses #-}
5+{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE UndecidableInstances #-}
7+{-# OPTIONS_GHC -Wno-orphans #-}
8+-- | TBW
9+--
10+-- This module exports 'ToSourceIO' and 'FromSourceIO' instances.
11+module Servant.Machines (
12+ MachineToSourceIO (..),
13+ ) where
14+
15+import Control.Monad.IO.Class
16+ (MonadIO (..))
17+import Data.Machine
18+ (MachineT (..), Step (..))
19+import Servant.API.Stream
20+import qualified Servant.Types.SourceT as S
21+
22+-- | Helper class to implement @'ToSourceIO' 'MachineT'@ instance
23+-- for various monads.
24+class MachineToSourceIO m where
25+ machineToSourceIO :: MachineT m k o -> S.SourceT IO o
26+
27+instance MachineToSourceIO IO where
28+ machineToSourceIO ma = S.SourceT ($ go ma) where
29+ go (MachineT m) = S.Effect $ do
30+ step <- m
31+ case step of
32+ Stop -> return S.Stop
33+ Yield x m' -> return (S.Yield x (go m'))
34+ Await _ _ m' -> return (S.Skip (go m'))
35+
36+instance MachineToSourceIO m => ToSourceIO o (MachineT m k o) where
37+ toSourceIO = machineToSourceIO
38+
39+instance MonadIO m => FromSourceIO o (MachineT m k o) where
40+ fromSourceIO src = MachineT $ liftIO $ S.unSourceT src go
41+ where
42+ go :: S.StepT IO o -> IO (Step k o (MachineT m k o))
43+ go S.Stop = return Stop
44+ go (S.Error err) = fail err
45+ go (S.Skip s) = go s
46+ go (S.Effect ms) = ms >>= go
47+ go (S.Yield x s) = return (Yield x (MachineT (liftIO (go s))))
48+ {-# SPECIALIZE INLINE fromSourceIO :: SourceIO o -> MachineT IO k o #-}
servant-pipes/CHANGELOG.mdnew file+16
@@ -0,0 +1,16 @@
1+0.10
2+----
3+
4+* `EncodeOpts` and `DecodeOpts` merged into just `EncodeOpts`.
5+* Add `TabSeparatedOpts` for `text/tab-separated-values`.
6+
7+0.9
8+---
9+
10+* Refactorerd `CSV`' type.
11+
12+0.8
13+---
14+
15+* Removed DefaultEncodeOpts and DefaultDecodeOpts in favor of a single
16+ (new) DefaultOpts
servant-pipes/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2018, Servant Contributors
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Servant Contributors nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
servant-pipes/README.mdnew file+3
@@ -0,0 +1,3 @@
1+# servant-pipes - Servant Stream support for pipes
2+
3+![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
servant-pipes/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
servant-pipes/example/Main.hsnew file+106
@@ -0,0 +1,106 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE TypeOperators #-}
4+module Main (main) where
5+
6+import Prelude ()
7+import Prelude.Compat
8+
9+import Control.Concurrent
10+ (threadDelay)
11+import Control.Monad.IO.Class
12+ (MonadIO (..))
13+import qualified Data.ByteString as BS
14+import Data.Maybe
15+ (fromMaybe)
16+import Network.HTTP.Client
17+ (defaultManagerSettings, newManager)
18+import Network.Wai
19+ (Application)
20+import System.Environment
21+ (getArgs, lookupEnv)
22+import System.IO (IOMode (..))
23+import Text.Read
24+ (readMaybe)
25+
26+import qualified Pipes as P
27+import qualified Pipes.Prelude as P
28+import Pipes.Safe
29+ (SafeT)
30+import qualified Pipes.Safe.Prelude as P
31+import Servant
32+import Pipes.ByteString as PBS
33+import Servant.Client
34+import Servant.Pipes ()
35+
36+import qualified Network.Wai.Handler.Warp as Warp
37+
38+type FastAPI = "get" :> Capture "num" Int :> StreamGet NewlineFraming JSON (P.Producer Int IO ())
39+
40+-- TODO: Change IO to something with MonadError ServantUnrenderError
41+type API = FastAPI
42+ :<|> "slow" :> Capture "num" Int :> StreamGet NewlineFraming JSON (P.Producer Int IO ())
43+ -- monad can be SafeT IO too.
44+ :<|> "readme" :> StreamGet NoFraming OctetStream (P.Producer BS.ByteString (SafeT IO) ())
45+ -- we can have streaming request body
46+ :<|> "proxy"
47+ :> StreamBody NoFraming OctetStream (P.Producer BS.ByteString IO ())
48+ :> StreamPost NoFraming OctetStream (P.Producer BS.ByteString IO ())
49+
50+api :: Proxy API
51+api = Proxy
52+
53+server :: Server API
54+server = fast :<|> slow :<|> readme :<|> proxy
55+ where
56+ fast n = liftIO $ do
57+ putStrLn ("/get/" ++ show n)
58+ return $ fastPipe n
59+
60+ slow n = liftIO $ do
61+ putStrLn ("/slow/" ++ show n)
62+ return $ slowPipe n
63+
64+ readme = liftIO $ do
65+ putStrLn "/readme"
66+ return $ P.withFile "README.md" ReadMode PBS.fromHandle
67+
68+ proxy c = liftIO $ do
69+ putStrLn "/proxy"
70+ return c
71+
72+ -- for some reason unfold leaks?
73+ fastPipe m
74+ | m < 0 = return ()
75+ | otherwise = P.yield m >> fastPipe (m - 1)
76+
77+ slowPipe m = fastPipe m P.>-> P.mapM (<$ threadDelay 1000000)
78+
79+app :: Application
80+app = serve api server
81+
82+cli :: Client ClientM FastAPI
83+cli :<|> _ :<|> _ :<|> _ = client api
84+
85+main :: IO ()
86+main = do
87+ args <- getArgs
88+ case args of
89+ ("server":_) -> do
90+ putStrLn "Starting servant-pipes:example at http://localhost:8000"
91+ port <- fromMaybe 8000 . (>>= readMaybe) <$> lookupEnv "PORT"
92+ Warp.run port app
93+ ("client":ns:_) -> do
94+ n <- maybe (fail $ "not a number: " ++ ns) pure $ readMaybe ns
95+ mgr <- newManager defaultManagerSettings
96+ burl <- parseBaseUrl "http://localhost:8000/"
97+ withClientM (cli n) (mkClientEnv mgr burl) $ \me -> case me of
98+ Left err -> print err
99+ Right p -> do
100+ x <- P.fold (\c _ -> c + 1) (0 :: Int) id p
101+ print x
102+ _ -> do
103+ putStrLn "Try:"
104+ putStrLn "cabal new-run servant-pipes:example server"
105+ putStrLn "cabal new-run servant-pipes:example client 10"
106+ putStrLn "time curl -H 'Accept: application/json' localhost:8000/slow/5"
servant-pipes/servant-pipes.cabalnew file+60
@@ -0,0 +1,60 @@
1+name: servant-pipes
2+version: 1
3+synopsis: Servant Stream support for pipes
4+description: Servant Stream support for pipes.
5+homepage: http://haskell-servant.readthedocs.org/
6+license: BSD3
7+license-file: LICENSE
8+author: Servant Contributors
9+maintainer: haskell-servant-maintainers@googlegroups.com
10+copyright: 2018 Servant Contributors
11+category: Web, Servant, Pipes
12+build-type: Simple
13+cabal-version: >=1.10
14+bug-reports: http://github.com/haskell-servant/servant-pipes/issues
15+tested-with:
16+ GHC ==8.0.2
17+ || ==8.2.2
18+ || ==8.4.4
19+ || ==8.6.1
20+
21+source-repository head
22+ type: git
23+ location: http://github.com/haskell-servant/servant-pipes.git
24+
25+library
26+ exposed-modules: Servant.Pipes
27+ build-depends:
28+ base >=4.9 && <5
29+ , bytestring >=0.10.4.0 && <0.11
30+ , pipes >=4.3.9 && <4.4
31+ , pipes-safe >=2.3.1 && <2.4
32+ , mtl >=2.1 && <2.3
33+ , monad-control >=1.0.2.3 && <1.1
34+ , servant >=0.14 && <0.15
35+ hs-source-dirs: src
36+ default-language: Haskell2010
37+ ghc-options: -Wall
38+
39+test-suite example
40+ type: exitcode-stdio-1.0
41+ main-is: Main.hs
42+ hs-source-dirs:
43+ example
44+ ghc-options: -Wall -rtsopts -threaded
45+ build-depends:
46+ base
47+ , base-compat
48+ , bytestring
49+ , http-media
50+ , servant
51+ , pipes
52+ , pipes-safe
53+ , servant-pipes
54+ , pipes-bytestring >= 2.1.6 && <2.2
55+ , servant-server >=0.14 && <0.15
56+ , servant-client >=0.14 && <0.15
57+ , wai >=3.0.3.0 && <3.3
58+ , warp >=3.0.13.1 && <3.3
59+ , http-client
60+ default-language: Haskell2010
servant-pipes/src/Servant/Pipes.hsnew file+77
@@ -0,0 +1,77 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE MultiParamTypeClasses #-}
5+{-# LANGUAGE RankNTypes #-}
6+{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE UndecidableInstances #-}
8+{-# OPTIONS_GHC -Wno-orphans #-}
9+-- | TBW
10+--
11+-- This module exports 'ToSourceIO' and 'FromSourceIO' instances.
12+module Servant.Pipes (
13+ PipesToSourceIO (..),
14+ ) where
15+
16+import Control.Monad.IO.Class
17+ (MonadIO (..))
18+import Control.Monad.Trans.Control
19+ (liftBaseWith)
20+import Pipes
21+ (ListT (..))
22+import Pipes.Internal
23+ (Proxy (..), X, closed)
24+import Pipes.Safe
25+ (SafeT, runSafeT)
26+import Servant.API.Stream
27+import qualified Servant.Types.SourceT as S
28+
29+-- | Helper class to implement @'ToSourceIO' 'Proxy'@ instance
30+-- for various monads.
31+class PipesToSourceIO m where
32+ pipesToSourceIO :: Proxy X () () b m () -> SourceIO b
33+
34+instance PipesToSourceIO IO where
35+ pipesToSourceIO ma = S.SourceT ($ go ma) where
36+ go :: Proxy X () () b IO () -> S.StepT IO b
37+ go (Pure ()) = S.Stop
38+ go (M p) = S.Effect (fmap go p)
39+ go (Request v _) = closed v
40+ go (Respond b n) = S.Yield b (go (n ()))
41+
42+instance m ~ IO => PipesToSourceIO (SafeT m) where
43+ pipesToSourceIO ma =
44+ S.SourceT $ \k ->
45+ runSafeT $ liftBaseWith $ \runSafe ->
46+ k (go runSafe ma)
47+ where
48+ go :: (forall x. SafeT m x -> m x)
49+ -> Proxy X () () b (SafeT m) ()
50+ -> S.StepT IO b
51+ go _ (Pure ()) = S.Stop
52+ go runSafe (M p) = S.Effect $ runSafe $ fmap (go runSafe) p
53+ go _ (Request v _) = closed v
54+ go runSafe (Respond b n) = S.Yield b (go runSafe (n ()))
55+
56+instance (PipesToSourceIO m, a' ~ X, a ~ (), b' ~ (), r ~ ())
57+ => ToSourceIO b (Proxy a' a b' b m r)
58+ where
59+ toSourceIO = pipesToSourceIO
60+
61+instance PipesToSourceIO m => ToSourceIO a (ListT m a) where
62+ toSourceIO = pipesToSourceIO . enumerate
63+
64+instance (MonadIO m, a' ~ X, a ~ (), b' ~ (), r ~ ())
65+ => FromSourceIO b (Proxy a' a b' b m r)
66+ where
67+ fromSourceIO src = M $ liftIO $ S.unSourceT src (return . go) where
68+ go :: S.StepT IO b -> Proxy X () () b m ()
69+ go S.Stop = Pure ()
70+ go (S.Error err) = M (fail err)
71+ go (S.Skip s) = go s -- drives
72+ go (S.Effect ms) = M (liftIO (fmap go ms))
73+ go (S.Yield x s) = Respond x (const (go s))
74+ {-# SPECIALIZE INLINE fromSourceIO :: SourceIO x -> Proxy X () () x IO () #-}
75+
76+instance MonadIO m => FromSourceIO a (ListT m a) where
77+ fromSourceIO = Select . fromSourceIO
servant-server/src/Servant/Server/Internal.hs+60−43
@@ -61,27 +61,27 @@ import Network.Socket
6161 (SockAddr)
6262import Network.Wai
6363 (Application, Request, httpVersion, isSecure, lazyRequestBody,
64− rawQueryString, remoteHost, requestHeaders, requestMethod,
65− responseLBS, responseStream, vault)
64+ rawQueryString, remoteHost, requestBody, requestHeaders,
65+ requestMethod, responseLBS, responseStream, vault)
6666import Prelude ()
6767import Prelude.Compat
6868import Servant.API
69− ((:<|>) (..), (:>), Accept (..), BasicAuth,
70− BoundaryStrategy (..), Capture', CaptureAll, Description,
71− EmptyAPI, FramingRender (..), Header', If, IsSecure (..),
72− QueryFlag, QueryParam', QueryParams, Raw,
69+ ((:<|>) (..), (:>), Accept (..), BasicAuth, Capture',
70+ CaptureAll, Description, EmptyAPI, FramingRender (..),
71+ FramingUnrender (..), FromSourceIO (..), Header', If,
72+ IsSecure (..), QueryFlag, QueryParam', QueryParams, Raw,
7373 ReflectMethod (reflectMethod), RemoteHost, ReqBody',
74− SBool (..), SBoolI (..), Stream, StreamGenerator (..),
75− Summary, ToStreamGenerator (..), Vault, Verb,
76− WithNamedContext)
74+ SBool (..), SBoolI (..), SourceIO, Stream, StreamBody,
75+ Summary, ToSourceIO (..), Vault, Verb, WithNamedContext)
7776import Servant.API.ContentTypes
7877 (AcceptHeader (..), AllCTRender (..), AllCTUnrender (..),
79− AllMime, MimeRender (..), canHandleAcceptH)
78+ AllMime, MimeRender (..), MimeUnrender (..), canHandleAcceptH)
8079import Servant.API.Modifiers
8180 (FoldLenient, FoldRequired, RequestArgument,
8281 unfoldRequestArgument)
8382import Servant.API.ResponseHeaders
8483 (GetHeaders, Headers, getHeaders, getResponse)
84+import qualified Servant.Types.SourceT as S
8585import Web.HttpApiData
8686 (FromHttpApiData, parseHeader, parseQueryParam,
8787 parseUrlPieceMaybe, parseUrlPieces)
@@ -279,24 +279,25 @@ instance {-# OVERLAPPING #-}
279279
280280
281281instance {-# OVERLAPPABLE #-}
282− ( MimeRender ctype a, ReflectMethod method, KnownNat status,
283− FramingRender framing ctype, ToStreamGenerator b a
284− ) => HasServer (Stream method status framing ctype b) context where
282+ ( MimeRender ctype chunk, ReflectMethod method, KnownNat status,
283+ FramingRender framing, ToSourceIO chunk a
284+ ) => HasServer (Stream method status framing ctype a) context where
285285
286− type ServerT (Stream method status framing ctype b) m = m b
286+ type ServerT (Stream method status framing ctype a) m = m a
287287 hoistServerWithContext _ _ nt s = nt s
288288
289289 route Proxy _ = streamRouter ([],) method status (Proxy :: Proxy framing) (Proxy :: Proxy ctype)
290290 where method = reflectMethod (Proxy :: Proxy method)
291291 status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
292292
293+
293294instance {-# OVERLAPPING #-}
294− ( MimeRender ctype a, ReflectMethod method, KnownNat status,
295− FramingRender framing ctype, ToStreamGenerator b a,
296− GetHeaders (Headers h b)
297− ) => HasServer (Stream method status framing ctype (Headers h b)) context where
295+ ( MimeRender ctype chunk, ReflectMethod method, KnownNat status,
296+ FramingRender framing, ToSourceIO chunk a,
297+ GetHeaders (Headers h a)
298+ ) => HasServer (Stream method status framing ctype (Headers h a)) context where
298299
299− type ServerT (Stream method status framing ctype (Headers h b)) m = m (Headers h b)
300+ type ServerT (Stream method status framing ctype (Headers h a)) m = m (Headers h a)
300301 hoistServerWithContext _ _ nt s = nt s
301302
302303 route Proxy _ = streamRouter (\x -> (getHeaders x, getResponse x)) method status (Proxy :: Proxy framing) (Proxy :: Proxy ctype)
@@ -304,8 +305,8 @@ instance {-# OVERLAPPING #-}
304305 status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
305306
306307
307−streamRouter :: (MimeRender ctype a, FramingRender framing ctype, ToStreamGenerator b a) =>
308− (c -> ([(HeaderName, B.ByteString)], b))
308+streamRouter :: forall ctype a c chunk env framing. (MimeRender ctype chunk, FramingRender framing, ToSourceIO chunk a) =>
309+ (c -> ([(HeaderName, B.ByteString)], a))
309310 -> Method
310311 -> Status
311312 -> Proxy framing
@@ -321,28 +322,19 @@ streamRouter splitHeaders method status framingproxy ctypeproxy action = leafRou
321322 `addAcceptCheck` accCheck
322323 ) env request respond $ \ output ->
323324 let (headers, fa) = splitHeaders output
324− k = getStreamGenerator . toStreamGenerator $ fa in
325− Route $ responseStream status (contentHeader : headers) $ \write flush -> do
326− write . BB.lazyByteString $ header framingproxy ctypeproxy
327− case boundary framingproxy ctypeproxy of
328− BoundaryStrategyBracket f ->
329− let go x = let bs = mimeRender ctypeproxy x
330− (before, after) = f bs
331− in write ( BB.lazyByteString before
332− <> BB.lazyByteString bs
333− <> BB.lazyByteString after) >> flush
334− in k go go
335− BoundaryStrategyIntersperse sep -> k
336− (\x -> do
337− write . BB.lazyByteString . mimeRender ctypeproxy $ x
338− flush)
339− (\x -> do
340− write . (BB.lazyByteString sep <>) . BB.lazyByteString . mimeRender ctypeproxy $ x
341− flush)
342− BoundaryStrategyGeneral f ->
343− let go = (>> flush) . write . BB.lazyByteString . f . mimeRender ctypeproxy
344− in k go go
345− write . BB.lazyByteString $ trailer framingproxy ctypeproxy
325+ sourceT = toSourceIO fa
326+ S.SourceT kStepLBS = framingRender framingproxy (mimeRender ctypeproxy :: chunk -> BL.ByteString) sourceT
327+ in Route $ responseStream status (contentHeader : headers) $ \write flush -> do
328+ let loop S.Stop = flush
329+ loop (S.Error err) = fail err -- TODO: throw better error
330+ loop (S.Skip s) = loop s
331+ loop (S.Effect ms) = ms >>= loop
332+ loop (S.Yield lbs s) = do
333+ write (BB.lazyByteString lbs)
334+ flush
335+ loop s
336+
337+ kStepLBS loop
346338
347339-- | If you use 'Header' in one of the endpoints for your API,
348340-- this automatically requires your server-side handler to be a function
@@ -615,6 +607,31 @@ instance ( AllCTUnrender list a, HasServer api context, SBoolI (FoldLenient mods
615607 Left e -> delayedFailFatal err400 { errBody = cs e }
616608 Right v -> return v
617609
610+instance
611+ ( FramingUnrender framing, FromSourceIO chunk a, MimeUnrender ctype chunk
612+ , HasServer api context
613+ ) => HasServer (StreamBody framing ctype a :> api) context
614+ where
615+ type ServerT (StreamBody framing ctype a :> api) m = a -> ServerT api m
616+
617+ hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
618+
619+ route Proxy context subserver = route (Proxy :: Proxy api) context $
620+ addBodyCheck subserver ctCheck bodyCheck
621+ where
622+ ctCheck :: DelayedIO (SourceIO chunk -> a)
623+ -- TODO: do content-type check
624+ ctCheck = return fromSourceIO
625+
626+ bodyCheck :: (SourceIO chunk -> a) -> DelayedIO a
627+ bodyCheck fromRS = withRequest $ \req -> do
628+ let mimeUnrender' = mimeUnrender (Proxy :: Proxy ctype) :: BL.ByteString -> Either String chunk
629+ let framingUnrender' = framingUnrender (Proxy :: Proxy framing) mimeUnrender' :: SourceIO B.ByteString -> SourceIO chunk
630+ let body = requestBody req
631+ let rs = S.fromAction B.null body
632+ let rs' = fromRS $ framingUnrender' rs
633+ return rs'
634+
618635-- | Make sure the incoming request starts with @"/path"@, strip it and
619636-- pass the rest of the request path to @api@.
620637instance (KnownSymbol path, HasServer api context) => HasServer (path :> api) context where
servant-server/test/Servant/ServerSpec.hs+4−3
@@ -42,6 +42,7 @@ import Network.Wai
4242import Network.Wai.Test
4343 (defaultRequest, request, runSession, simpleBody,
4444 simpleHeaders, simpleStatus)
45+import qualified Servant.Types.SourceT as S
4546import Servant.API
4647 ((:<|>) (..), (:>), AuthProtect, BasicAuth,
4748 BasicAuthData (BasicAuthData), Capture, CaptureAll, Delete,
@@ -49,7 +50,7 @@ import Servant.API
4950 JSON, NoContent (..), NoFraming, OctetStream, Patch,
5051 PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw,
5152 RemoteHost, ReqBody, StdMethod (..), Stream,
52− StreamGenerator (..), Verb, addHeader)
53+ SourceIO, Verb, addHeader)
5354import Servant.API.Internal.Test.ComprehensiveAPI
5455import Servant.Server
5556 (Context ((:.), EmptyContext), Handler, Server, Tagged (..),
@@ -106,7 +107,7 @@ type VerbApi method status
106107 :<|> "accept" :> ( Verb method status '[JSON] Person
107108 :<|> Verb method status '[PlainText] String
108109 )
109− :<|> "stream" :> Stream method status NoFraming OctetStream (StreamGenerator BS.ByteString)
110+ :<|> "stream" :> Stream method status NoFraming OctetStream (SourceIO BS.ByteString)
110111
111112verbSpec :: Spec
112113verbSpec = describe "Servant.API.Verb" $ do
@@ -116,7 +117,7 @@ verbSpec = describe "Servant.API.Verb" $ do
116117 :<|> return (addHeader 5 alice)
117118 :<|> return (addHeader 10 NoContent)
118119 :<|> (return alice :<|> return "B")
119− :<|> return (StreamGenerator $ \f _ -> f "bytestring")
120+ :<|> return (S.source ["bytestring"])
120121
121122 get200 = Proxy :: Proxy (VerbApi 'GET 200)
122123 post210 = Proxy :: Proxy (VerbApi 'POST 210)
servant/servant.cabal+15−4
@@ -24,6 +24,7 @@ tested-with:
2424 GHC==8.6.1
2525extra-source-files:
2626 CHANGELOG.md
27+
2728source-repository head
2829 type: git
2930 location: http://github.com/haskell-servant/servant.git
@@ -61,6 +62,13 @@ library
6162 Servant.API.Vault
6263 Servant.API.Verbs
6364 Servant.API.WithNamedContext
65+
66+ -- Types
67+ exposed-modules:
68+ Servant.Types.SourceT
69+
70+ -- Safe links
71+ exposed-modules:
6472 Servant.Links
6573
6674 -- Deprecated modules, to be removed in late 2019
@@ -75,6 +83,7 @@ library
7583 base >= 4.9 && < 4.13
7684 , bytestring >= 0.10.8.1 && < 0.11
7785 , mtl >= 2.1 && < 2.3
86+ , transformers >= 0.3.0.0 && < 0.6
7887 , text >= 1.2.3.0 && < 1.3
7988
8089 if !impl(ghc >= 8.0)
@@ -84,19 +93,19 @@ library
8493 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
8594 -- Here can be exceptions if we really need features from the newer versions.
8695 build-depends:
87− base-compat >= 0.10.4 && < 0.11
88− , bifunctors >= 5.5.3 && < 5.6
96+ base-compat >= 0.10.5 && < 0.11
8997 , aeson >= 1.3.1.1 && < 1.5
9098 , attoparsec >= 0.13.2.2 && < 0.14
99+ , bifunctors >= 5.5.3 && < 5.6
91100 , case-insensitive >= 1.2.0.11 && < 1.3
92101 , http-api-data >= 0.3.8.1 && < 0.4
93102 , http-media >= 0.7.1.2 && < 0.8
94103 , http-types >= 0.12.1 && < 0.13
95104 , mmorph >= 1.1.2 && < 1.2
96− , tagged >= 0.8.5 && < 0.9
105+ , network-uri >= 2.6.1.0 && < 2.7
97106 , singleton-bool >= 0.1.4 && < 0.2
98107 , string-conversions >= 0.4.0.1 && < 0.5
99− , network-uri >= 2.6.1.0 && < 2.7
108+ , tagged >= 0.8.5 && < 0.9
100109 , vault >= 0.3.1.1 && < 0.4
101110
102111 hs-source-dirs: src
@@ -132,6 +141,7 @@ test-suite spec
132141 other-modules:
133142 Servant.API.ContentTypesSpec
134143 Servant.API.ResponseHeadersSpec
144+ Servant.API.StreamSpec
135145 Servant.LinksSpec
136146
137147 -- Dependencies inherited from the library. No need to specify bounds.
@@ -143,6 +153,7 @@ test-suite spec
143153 , servant
144154 , string-conversions
145155 , text
156+ , transformers
146157
147158 -- Additonal dependencies
148159 build-depends:
servant/src/Servant/API.hs+4−5
@@ -114,11 +114,10 @@ import Servant.API.ResponseHeaders
114114 ResponseHeader (..), addHeader, getHeadersHList, getResponse,
115115 noHeader)
116116import Servant.API.Stream
117− (BoundaryStrategy (..), ByteStringParser (..),
118− FramingRender (..), FramingUnrender (..),
119− FromResultStream (..), NetstringFraming, NewlineFraming,
120− NoFraming, ResultStream (..), Stream, StreamGenerator (..),
121− StreamGet, StreamPost, ToStreamGenerator (..))
117+ (FramingRender (..), FramingUnrender (..),
118+ FromSourceIO (..), NetstringFraming, NewlineFraming,
119+ NoFraming, SourceIO, Stream, StreamBody, SourceIO,
120+ StreamGet, StreamPost, ToSourceIO (..))
122121import Servant.API.Sub
123122 ((:>))
124123import Servant.API.Vault
servant/src/Servant/API/Internal/Test/ComprehensiveAPI.hs+3−1
@@ -9,6 +9,8 @@ module Servant.API.Internal.Test.ComprehensiveAPI where
99import Data.Proxy
1010 (Proxy (..))
1111import Servant.API
12+import Servant.Types.SourceT
13+ (SourceT)
1214
1315type GET = Get '[JSON] NoContent
1416
@@ -39,7 +41,7 @@ type ComprehensiveAPIWithoutRaw =
3941 Vault :> GET :<|>
4042 Verb 'POST 204 '[JSON] NoContent :<|>
4143 Verb 'POST 204 '[JSON] Int :<|>
42− Stream 'GET 200 NetstringFraming JSON [Int] :<|>
44+ StreamBody NetstringFraming JSON (SourceT IO Int) :> Stream 'GET 200 NetstringFraming JSON (SourceT IO Int) :<|>
4345 WithNamedContext "foo" '[] GET :<|>
4446 CaptureAll "foo" Int :> GET :<|>
4547 Summary "foo" :> GET :<|>
servant/src/Servant/API/Stream.hs+201−171
@@ -1,6 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveDataTypeable #-}
33{-# LANGUAGE DeriveGeneric #-}
4+{-# LANGUAGE FlexibleContexts #-}
45{-# LANGUAGE FlexibleInstances #-}
56{-# LANGUAGE FunctionalDependencies #-}
67{-# LANGUAGE KindSignatures #-}
@@ -8,39 +9,41 @@
89{-# LANGUAGE OverloadedStrings #-}
910{-# LANGUAGE PolyKinds #-}
1011{-# LANGUAGE RankNTypes #-}
11−{-# LANGUAGE TupleSections #-}
12−{-# OPTIONS_HADDOCK not-home #-}
12+{-# LANGUAGE ScopedTypeVariables #-}
13+{-# OPTIONS_HADDOCK not-home #-}
1314
1415module Servant.API.Stream (
15− Stream,
16− StreamGet,
17− StreamPost,
18− -- * Sources
19− --
20− -- | Both 'StreamGenerator' and 'ResultStream' are equivalent
21− -- to some *source* in streaming libraries.
22− StreamGenerator (..),
23− ToStreamGenerator (..),
24− ResultStream (..),
25− FromResultStream (..),
26− -- * Framing
27− FramingRender (..),
28− FramingUnrender (..),
29− BoundaryStrategy (..),
30− ByteStringParser (..),
31− -- ** Strategies
32− NoFraming,
33− NewlineFraming,
34− NetstringFraming,
35− ) where
36−
37−import Control.Arrow
38− (first)
39−import Data.ByteString.Lazy
40− (ByteString, empty)
41−import qualified Data.ByteString.Lazy.Char8 as LB
42−import Data.Foldable
43− (traverse_)
16+ Stream,
17+ StreamGet,
18+ StreamPost,
19+ StreamBody,
20+ -- * Source
21+ --
22+ -- | 'SourceIO' are equivalent to some *source* in streaming libraries.
23+ SourceIO,
24+ ToSourceIO (..),
25+ FromSourceIO (..),
26+ -- ** Auxiliary classes
27+ SourceToSourceIO (..),
28+ -- * Framing
29+ FramingRender (..),
30+ FramingUnrender (..),
31+ -- ** Strategies
32+ NoFraming,
33+ NewlineFraming,
34+ NetstringFraming,
35+ ) where
36+
37+
38+import Control.Applicative
39+ ((<|>))
40+import Control.Monad.IO.Class
41+ (MonadIO (..))
42+import qualified Data.Attoparsec.ByteString as A
43+import qualified Data.Attoparsec.ByteString.Char8 as A8
44+import qualified Data.ByteString as BS
45+import qualified Data.ByteString.Lazy as LBS
46+import qualified Data.ByteString.Lazy.Char8 as LBS8
4447import Data.List.NonEmpty
4548 (NonEmpty (..))
4649import Data.Monoid
@@ -55,158 +58,185 @@ import GHC.TypeLits
5558 (Nat)
5659import Network.HTTP.Types.Method
5760 (StdMethod (..))
58−import System.IO.Unsafe
59− (unsafeInterleaveIO)
60−import Text.Read
61− (readMaybe)
61+import Servant.Types.SourceT
6262
6363-- | A Stream endpoint for a given method emits a stream of encoded values at a
64−-- given Content-Type, delimited by a framing strategy. Stream endpoints always
65−-- return response code 200 on success. Type synonyms are provided for standard
66−-- methods.
64+-- given @Content-Type@, delimited by a @framing@ strategy.
65+-- Type synonyms are provided for standard methods.
66+--
6767data Stream (method :: k1) (status :: Nat) (framing :: *) (contentType :: *) (a :: *)
6868 deriving (Typeable, Generic)
6969
7070type StreamGet = Stream 'GET 200
7171type StreamPost = Stream 'POST 200
7272
73−-- | Stream endpoints may be implemented as producing a @StreamGenerator@ a
74−-- function that itself takes two emit functions the first to be used on the
75−-- first value the stream emits, and the second to be used on all subsequent
76−-- values (to allow interspersed framing strategies such as comma separation).
77−newtype StreamGenerator a = StreamGenerator { getStreamGenerator :: (a -> IO ()) -> (a -> IO ()) -> IO () }
78−
79−-- | ToStreamGenerator is intended to be implemented for types such as Conduit, Pipe, etc. By implementing this class, all such streaming abstractions can be used directly as endpoints.
80−class ToStreamGenerator a b | a -> b where
81− toStreamGenerator :: a -> StreamGenerator b
82−
83−instance ToStreamGenerator (StreamGenerator a) a where
84− toStreamGenerator x = x
85−
86−instance ToStreamGenerator (NonEmpty a) a where
87− toStreamGenerator (x :| xs) = StreamGenerator $ \f g -> f x >> traverse_ g xs
88−
89−instance ToStreamGenerator [a] a where
90− toStreamGenerator [] = StreamGenerator $ \_ _ -> return ()
91− toStreamGenerator (x : xs) = StreamGenerator $ \f g -> f x >> traverse_ g xs
92−
93−-- | Clients reading from streaming endpoints can be implemented as producing a
94−-- @ResultStream@ that captures the setup, takedown, and incremental logic for
95−-- a read, being an IO continuation that takes a producer of Just either values
96−-- or errors that terminates with a Nothing.
97−newtype ResultStream a = ResultStream { runResultStream :: forall b. (IO (Maybe (Either String a)) -> IO b) -> IO b }
98−
99−-- | FromResultStream is intended to be implemented for types such as Conduit, Pipe, etc. By implementing this class, all such streaming abstractions can be used directly on the client side for talking to streaming endpoints.
100−class FromResultStream a b | b -> a where
101− fromResultStream :: ResultStream a -> IO b
102−
103−instance FromResultStream a (ResultStream a) where
104− fromResultStream = return
105−
106−-- | Uses 'unsafeInterleaveIO'
107−instance FromResultStream a [a] where
108− fromResultStream x = runResultStream x lazyRead
109−
110−-- | Uses 'unsafeInterleaveIO'
111−instance FromResultStream a (NonEmpty a) where
112− fromResultStream x = runResultStream x $ \r -> do
113− e <- r
114− case e of
115− Nothing -> fail "Empty stream"
116− Just (Left er) -> fail er
117− Just (Right y) -> do
118− ys <- lazyRead r
119− return (y :| ys)
120−
121−lazyRead :: IO (Maybe (Either String a)) -> IO [a]
122−lazyRead r = go
73+-- | A stream request body.
74+--
75+-- TODO: add mods
76+data StreamBody (framing :: *) (contentType :: *) (a :: *)
77+ deriving (Typeable, Generic)
78+
79+-------------------------------------------------------------------------------
80+-- Sources
81+-------------------------------------------------------------------------------
82+
83+-- | Stream endpoints may be implemented as producing a @'SourceIO' chunk@.
84+--
85+-- Clients reading from streaming endpoints can be implemented as consuming a
86+-- @'SourceIO' chunk@.
87+--
88+type SourceIO = SourceT IO
89+
90+-- | 'ToSourceIO' is intended to be implemented for types such as Conduit, Pipe,
91+-- etc. By implementing this class, all such streaming abstractions can be used
92+-- directly as endpoints.
93+class ToSourceIO chunk a | a -> chunk where
94+ toSourceIO :: a -> SourceIO chunk
95+
96+-- | Auxiliary class for @'ToSourceIO' x ('SourceT' m x)@ instance.
97+class SourceToSourceIO m where
98+ sourceToSourceIO :: SourceT m a -> SourceT IO a
99+
100+instance SourceToSourceIO IO where
101+ sourceToSourceIO = id
102+
103+-- | Relax to use auxiliary class, have m
104+instance SourceToSourceIO m => ToSourceIO chunk (SourceT m chunk) where
105+ toSourceIO = sourceToSourceIO
106+
107+instance ToSourceIO a (NonEmpty a) where
108+ toSourceIO (x :| xs) = fromStepT (Yield x (foldr Yield Stop xs))
109+
110+instance ToSourceIO a [a] where
111+ toSourceIO = source
112+
113+-- | 'FromSourceIO' is intended to be implemented for types such as Conduit,
114+-- Pipe, etc. By implementing this class, all such streaming abstractions can
115+-- be used directly on the client side for talking to streaming endpoints.
116+class FromSourceIO chunk a | a -> chunk where
117+ fromSourceIO :: SourceIO chunk -> a
118+
119+instance MonadIO m => FromSourceIO a (SourceT m a) where
120+ fromSourceIO = sourceFromSourceIO
121+
122+sourceFromSourceIO :: forall m a. MonadIO m => SourceT IO a -> SourceT m a
123+sourceFromSourceIO src =
124+ SourceT $ \k ->
125+ k $ Effect $ liftIO $ unSourceT src (return . go)
123126 where
124− go = unsafeInterleaveIO loop
125−
126− loop = do
127− e <- r
128− case e of
129− Nothing -> return []
130− Just (Left er) -> fail er
131− Just (Right y) -> do
132− ys <- go
133− return (y : ys)
134−
135−-- | The FramingRender class provides the logic for emitting a framing strategy. The strategy emits a header, followed by boundary-delimited data, and finally a termination character. For many strategies, some of these will just be empty bytestrings.
136−class FramingRender strategy a where
137− header :: Proxy strategy -> Proxy a -> ByteString
138− boundary :: Proxy strategy -> Proxy a -> BoundaryStrategy
139− trailer :: Proxy strategy -> Proxy a -> ByteString
140−
141−-- | The bracketing strategy generates things to precede and follow the content, as with netstrings.
142−-- The intersperse strategy inserts seperators between things, as with newline framing.
143−-- Finally, the general strategy performs an arbitrary rewrite on the content, to allow escaping rules and such.
144−data BoundaryStrategy = BoundaryStrategyBracket (ByteString -> (ByteString,ByteString))
145− | BoundaryStrategyIntersperse ByteString
146− | BoundaryStrategyGeneral (ByteString -> ByteString)
147−
148−-- | A type of parser that can never fail, and has different parsing strategies (incremental, or EOF) depending if more input can be sent. The incremental parser should return `Nothing` if it would like to be sent a longer ByteString. If it returns a value, it also returns the remainder following that value.
149−data ByteStringParser a = ByteStringParser
150− { parseIncremental :: ByteString -> Maybe (a, ByteString)
151− , parseEOF :: ByteString -> (a, ByteString)
152− }
153−
154−-- | The FramingUnrender class provides the logic for parsing a framing strategy. The outer @ByteStringParser@ strips the header from a stream of bytes, and yields a parser that can handle the remainder, stepwise. Each frame may be a ByteString, or a String indicating the error state for that frame. Such states are per-frame, so that protocols that can resume after errors are able to do so. Eventually this returns an empty ByteString to indicate termination.
155−class FramingUnrender strategy a where
156− unrenderFrames :: Proxy strategy -> Proxy a -> ByteStringParser (ByteStringParser (Either String ByteString))
157−
158−-- | A framing strategy that does not do any framing at all, it just passes the input data
159−-- This will be used most of the time with binary data, such as files
127+ go :: StepT IO a -> StepT m a
128+ go Stop = Stop
129+ go (Error err) = Error err
130+ go (Skip s) = Skip (go s)
131+ go (Effect ms) = Effect (liftIO (fmap go ms))
132+ go (Yield x s) = Yield x (go s)
133+
134+-- This fires e.g. in Client.lhs
135+-- {-# OPTIONS_GHC -ddump-simpl -ddump-rule-firings -ddump-to-file #-}
136+{-# NOINLINE [2] sourceFromSourceIO #-}
137+{-# RULES "sourceFromSourceIO @IO" sourceFromSourceIO = id :: SourceT IO a -> SourceT IO a #-}
138+
139+-------------------------------------------------------------------------------
140+-- Framing
141+-------------------------------------------------------------------------------
142+
143+-- | The 'FramingRender' class provides the logic for emitting a framing strategy.
144+-- The strategy transforms a @'SourceT' m a@ into @'SourceT' m 'LBS.ByteString'@,
145+-- therefore it can prepend, append and intercalate /framing/ structure
146+-- around chunks.
147+--
148+-- /Note:/ as the @'Monad' m@ is generic, this is pure transformation.
149+--
150+class FramingRender strategy where
151+ framingRender :: Monad m => Proxy strategy -> (a -> LBS.ByteString) -> SourceT m a -> SourceT m LBS.ByteString
152+
153+-- | The 'FramingUnrender' class provides the logic for parsing a framing
154+-- strategy.
155+class FramingUnrender strategy where
156+ framingUnrender :: Monad m => Proxy strategy -> (LBS.ByteString -> Either String a) -> SourceT m BS.ByteString -> SourceT m a
157+
158+-------------------------------------------------------------------------------
159+-- NoFraming
160+-------------------------------------------------------------------------------
161+
162+-- | A framing strategy that does not do any framing at all, it just passes the
163+-- input data This will be used most of the time with binary data, such as
164+-- files
160165data NoFraming
161166
162−instance FramingRender NoFraming a where
163− header _ _ = empty
164− boundary _ _ = BoundaryStrategyGeneral id
165− trailer _ _ = empty
166−
167−instance FramingUnrender NoFraming a where
168− unrenderFrames _ _ = ByteStringParser (Just . (go,)) (go,)
169− where go = ByteStringParser (Just . (, empty) . Right) ((, empty) . Right)
170−
171−-- | A simple framing strategy that has no header or termination, and inserts a newline character between each frame.
172−-- This assumes that it is used with a Content-Type that encodes without newlines (e.g. JSON).
167+instance FramingRender NoFraming where
168+ framingRender _ = fmap
169+
170+-- | As 'NoFraming' doesn't have frame separators, we take the chunks
171+-- as given and try to convert them one by one.
172+--
173+-- That works well when @a@ is a 'ByteString'.
174+instance FramingUnrender NoFraming where
175+ framingUnrender _ f = mapStepT go
176+ where
177+ go Stop = Stop
178+ go (Error err) = Error err
179+ go (Skip s) = Skip (go s)
180+ go (Effect ms) = Effect (fmap go ms)
181+ go (Yield x s) = case f (LBS.fromStrict x) of
182+ Right y -> Yield y (go s)
183+ Left err -> Error err
184+
185+-------------------------------------------------------------------------------
186+-- NewlineFraming
187+-------------------------------------------------------------------------------
188+
189+-- | A simple framing strategy that has no header or termination, and inserts a
190+-- newline character between each frame. This assumes that it is used with a
191+-- Content-Type that encodes without newlines (e.g. JSON).
173192data NewlineFraming
174193
175−instance FramingRender NewlineFraming a where
176− header _ _ = empty
177− boundary _ _ = BoundaryStrategyIntersperse "\n"
178− trailer _ _ = empty
179−
180−instance FramingUnrender NewlineFraming a where
181− unrenderFrames _ _ = ByteStringParser (Just . (go,)) (go,)
182− where go = ByteStringParser
183− (\x -> case LB.break (== '\n') x of
184− (h,r) -> if not (LB.null r) then Just (Right h, LB.drop 1 r) else Nothing
185− )
186− (\x -> case LB.break (== '\n') x of
187− (h,r) -> (Right h, LB.drop 1 r)
188− )
189−-- | The netstring framing strategy as defined by djb: <http://cr.yp.to/proto/netstrings.txt>
194+instance FramingRender NewlineFraming where
195+ framingRender _ f = mapStepT go0 where
196+ go0 Stop = Stop
197+ go0 (Error err) = Error err
198+ go0 (Skip s) = Skip (go0 s)
199+ go0 (Yield x s) = Yield (f x) (go s)
200+ go0 (Effect ms) = Effect (fmap go0 ms)
201+
202+ go = fmap (\x -> "\n" <> f x)
203+
204+instance FramingUnrender NewlineFraming where
205+ framingUnrender _ f = transformWithAtto $ do
206+ bs <- A.takeWhile (/= 10)
207+ () <$ A.word8 10 <|> A.endOfInput
208+ either fail pure (f (LBS.fromStrict bs))
209+
210+-------------------------------------------------------------------------------
211+-- NetstringFraming
212+-------------------------------------------------------------------------------
213+
214+-- | The netstring framing strategy as defined by djb:
215+-- <http://cr.yp.to/proto/netstrings.txt>
216+--
217+-- Any string of 8-bit bytes may be encoded as @[len]":"[string]","@. Here
218+-- @[string]@ is the string and @[len]@ is a nonempty sequence of ASCII digits
219+-- giving the length of @[string]@ in decimal. The ASCII digits are @<30>@ for
220+-- 0, @<31>@ for 1, and so on up through @<39>@ for 9. Extra zeros at the front
221+-- of @[len]@ are prohibited: @[len]@ begins with @<30>@ exactly when
222+-- @[string]@ is empty.
223+--
224+-- For example, the string @"hello world!"@ is encoded as
225+-- @<31 32 3a 68 65 6c 6c 6f 20 77 6f 72 6c 64 21 2c>@,
226+-- i.e., @"12:hello world!,"@.
227+-- The empty string is encoded as @"0:,"@.
228+--
190229data NetstringFraming
191230
192−instance FramingRender NetstringFraming a where
193− header _ _ = empty
194− boundary _ _ = BoundaryStrategyBracket $ \b -> ((<> ":") . LB.pack . show . LB.length $ b, ",")
195− trailer _ _ = empty
196−
197−
198−instance FramingUnrender NetstringFraming a where
199− unrenderFrames _ _ = ByteStringParser (Just . (go,)) (go,)
200− where go = ByteStringParser
201− (\b -> let (i,r) = LB.break (==':') b
202− in case readMaybe (LB.unpack i) of
203− Just len -> if LB.length r > len
204− then Just . first Right . fmap (LB.drop 1) $ LB.splitAt len . LB.drop 1 $ r
205− else Nothing
206− Nothing -> Just (Left ("Bad netstring frame, couldn't parse value as integer value: " ++ LB.unpack i), LB.drop 1 . LB.dropWhile (/= ',') $ r))
207− (\b -> let (i,r) = LB.break (==':') b
208− in case readMaybe (LB.unpack i) of
209− Just len -> if LB.length r > len
210− then first Right . fmap (LB.drop 1) $ LB.splitAt len . LB.drop 1 $ r
211− else (Right $ LB.take len r, LB.empty)
212− Nothing -> (Left ("Bad netstring frame, couldn't parse value as integer value: " ++ LB.unpack i), LB.drop 1 . LB.dropWhile (/= ',') $ r))
231+instance FramingRender NetstringFraming where
232+ framingRender _ f = fmap $ \x ->
233+ let bs = f x
234+ in LBS8.pack (show (LBS8.length bs)) <> ":" <> bs <> ","
235+
236+instance FramingUnrender NetstringFraming where
237+ framingUnrender _ f = transformWithAtto $ do
238+ len <- A8.decimal
239+ _ <- A8.char ':'
240+ bs <- A.take len
241+ _ <- A8.char ','
242+ either fail pure (f (LBS.fromStrict bs))
servant/src/Servant/Links.hs+5−1
@@ -170,7 +170,7 @@ import Servant.API.RemoteHost
170170import Servant.API.ReqBody
171171 (ReqBody')
172172import Servant.API.Stream
173− (Stream)
173+ (Stream, StreamBody)
174174import Servant.API.Sub
175175 (type (:>))
176176import Servant.API.TypeLevel
@@ -482,6 +482,10 @@ instance HasLink sub => HasLink (ReqBody' mods ct a :> sub) where
482482 type MkLink (ReqBody' mods ct a :> sub) r = MkLink sub r
483483 toLink toA _ = toLink toA (Proxy :: Proxy sub)
484484
485+instance HasLink sub => HasLink (StreamBody framing ct a :> sub) where
486+ type MkLink (StreamBody framing ct a :> sub) r = MkLink sub r
487+ toLink toA _ = toLink toA (Proxy :: Proxy sub)
488+
485489instance (ToHttpApiData v, HasLink sub)
486490 => HasLink (Capture' mods sym v :> sub)
487491 where
servant/src/Servant/Types/SourceT.hsnew file+305
@@ -0,0 +1,305 @@
1+{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE GADTs #-}
3+{-# LANGUAGE RankNTypes #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+module Servant.Types.SourceT where
6+
7+import Control.Monad.Except
8+ (ExceptT (..), runExceptT, throwError)
9+import Control.Monad.Morph
10+ (MFunctor (..))
11+import Control.Monad.Trans.Class
12+ (MonadTrans (..))
13+import qualified Data.Attoparsec.ByteString as A
14+import qualified Data.ByteString as BS
15+import Data.Functor.Classes
16+ (Show1 (..), showsBinaryWith, showsPrec1, showsUnaryWith)
17+import Data.Functor.Identity
18+ (Identity (..))
19+import Prelude hiding
20+ (readFile)
21+import System.IO
22+ (Handle, IOMode (..), withFile)
23+
24+-- $setup
25+-- >>> :set -XOverloadedStrings
26+-- >>> import Control.Monad.Except (runExcept)
27+-- >>> import Data.Foldable (toList)
28+-- >>> import qualified Data.Attoparsec.ByteString.Char8 as A8
29+
30+-- | This is CPSised ListT.
31+newtype SourceT m a = SourceT
32+ { unSourceT :: forall b. (StepT m a -> m b) -> m b
33+ }
34+
35+mapStepT :: (StepT m a -> StepT m b) -> SourceT m a -> SourceT m b
36+mapStepT f (SourceT m) = SourceT $ \k -> m (k . f)
37+{-# INLINE mapStepT #-}
38+
39+-- | @ListT@ with additional constructors.
40+data StepT m a
41+ = Stop
42+ | Error String -- we can this argument configurable.
43+ | Skip (StepT m a) -- Note: not sure about this constructor
44+ | Yield a (StepT m a)
45+ | Effect (m (StepT m a))
46+ deriving Functor
47+
48+-- | Create 'SourceT' from 'Step'.
49+--
50+-- /Note:/ often enough you want to use 'SourceT' directly.
51+fromStepT :: StepT m a -> SourceT m a
52+fromStepT s = SourceT ($ s)
53+
54+-------------------------------------------------------------------------------
55+-- SourceT instances
56+-------------------------------------------------------------------------------
57+
58+instance Functor m => Functor (SourceT m) where
59+ fmap f = mapStepT (fmap f)
60+
61+-- | >>> toList (source [1..10])
62+-- [1,2,3,4,5,6,7,8,9,10]
63+--
64+instance Identity ~ m => Foldable (SourceT m) where
65+ foldr f z (SourceT m) = foldr f z (runIdentity (m Identity))
66+
67+instance (Applicative m, Show1 m) => Show1 (SourceT m) where
68+ liftShowsPrec sp sl d (SourceT m) = showsUnaryWith
69+ (liftShowsPrec sp sl)
70+ "fromStepT" d (Effect (m pure'))
71+ where
72+ pure' (Effect s) = s
73+ pure' s = pure s
74+
75+instance (Applicative m, Show1 m, Show a) => Show (SourceT m a) where
76+ showsPrec = showsPrec1
77+
78+-- | >>> hoist (Just . runIdentity) (source [1..3]) :: SourceT Maybe Int
79+-- fromStepT (Effect (Just (Yield 1 (Yield 2 (Yield 3 Stop)))))
80+instance MFunctor SourceT where
81+ hoist f (SourceT m) = SourceT $ \k -> k $
82+ Effect $ f $ fmap (hoist f) $ m return
83+
84+-------------------------------------------------------------------------------
85+-- StepT instances
86+-------------------------------------------------------------------------------
87+
88+instance Identity ~ m => Foldable (StepT m) where
89+ foldr f z = go where
90+ go Stop = z
91+ go (Error _) = z
92+ go (Skip s) = go s
93+ go (Yield a s) = f a (go s)
94+ go (Effect (Identity s)) = go s
95+
96+instance (Applicative m, Show1 m) => Show1 (StepT m) where
97+ liftShowsPrec sp sl = go where
98+ go _ Stop = showString "Stop"
99+ go d (Skip s) = showsUnaryWith
100+ go
101+ "Skip" d s
102+ go d (Error err) = showsUnaryWith
103+ showsPrec
104+ "Error" d err
105+ go d (Effect ms) = showsUnaryWith
106+ (liftShowsPrec go goList)
107+ "Effect" d ms
108+ go d (Yield x s) = showsBinaryWith
109+ sp go
110+ "Yield" d x s
111+
112+ goList = liftShowList sp sl
113+
114+instance (Applicative m, Show1 m, Show a) => Show (StepT m a) where
115+ showsPrec = showsPrec1
116+
117+-- | >>> lift [1,2,3] :: StepT [] Int
118+-- Effect [Yield 1 Stop,Yield 2 Stop,Yield 3 Stop]
119+--
120+instance MonadTrans StepT where
121+ lift = Effect . fmap (`Yield` Stop)
122+
123+instance MFunctor StepT where
124+ hoist f = go where
125+ go Stop = Stop
126+ go (Error err) = Error err
127+ go (Skip s) = Skip (go s)
128+ go (Yield x s) = Yield x (go s)
129+ go (Effect ms) = Effect (f (fmap go ms))
130+
131+-------------------------------------------------------------------------------
132+-- Operations
133+-------------------------------------------------------------------------------
134+
135+-- | Create pure 'SourceT'.
136+--
137+-- >>> source "foo" :: SourceT Identity Char
138+-- fromStepT (Effect (Identity (Yield 'f' (Yield 'o' (Yield 'o' Stop)))))
139+--
140+source :: [a] -> SourceT m a
141+source = fromStepT . foldr Yield Stop
142+
143+-- | Get the answers.
144+--
145+-- >>> runSourceT (source "foo" :: SourceT Identity Char)
146+-- ExceptT (Identity (Right "foo"))
147+--
148+-- >>> runSourceT (source "foo" :: SourceT [] Char)
149+-- ExceptT [Right "foo"]
150+--
151+runSourceT :: Monad m => SourceT m a -> ExceptT String m [a]
152+runSourceT (SourceT m) = ExceptT (m (runExceptT . runStepT))
153+
154+runStepT :: Monad m => StepT m a -> ExceptT String m [a]
155+runStepT Stop = return []
156+runStepT (Error err) = throwError err
157+runStepT (Skip s) = runStepT s
158+runStepT (Yield x s) = fmap (x :) (runStepT s)
159+runStepT (Effect ms) = lift ms >>= runStepT
160+
161+{-
162+-- | >>> uncons (foldr Yield Stop "foo" :: StepT Identity Char)
163+-- Identity (Just ('f',Yield 'o' (Yield 'o' Stop)))
164+--
165+uncons :: Monad m => StepT m a -> m (Maybe (a, StepT m a))
166+uncons Stop = return Nothing
167+uncons (Skip s) = uncons s
168+uncons (Yield x s) = return (Just (x, s))
169+uncons (Effect ms) = ms >>= uncons
170+uncons (Error _) =
171+-}
172+
173+-- | Filter values.
174+--
175+-- >>> toList $ mapMaybe (\x -> if odd x then Just x else Nothing) (source [0..10]) :: [Int]
176+-- [1,3,5,7,9]
177+--
178+-- >>> mapMaybe (\x -> if odd x then Just x else Nothing) (source [0..2]) :: SourceT Identity Int
179+-- fromStepT (Effect (Identity (Skip (Yield 1 (Skip Stop)))))
180+--
181+-- Illustrates why we need 'Skip'.
182+mapMaybe :: Functor m => (a -> Maybe b) -> SourceT m a -> SourceT m b
183+mapMaybe p (SourceT m) = SourceT $ \k -> m (k . mapMaybeStep p)
184+
185+mapMaybeStep :: Functor m => (a -> Maybe b) -> StepT m a -> StepT m b
186+mapMaybeStep p = go where
187+ go Stop = Stop
188+ go (Error err) = Error err
189+ go (Skip s) = Skip (go s)
190+ go (Effect ms) = Effect (fmap go ms)
191+ go (Yield x s) = case p x of
192+ Nothing -> Skip (go s)
193+ Just y -> Yield y (go s)
194+
195+-- | Run action for each value in the 'SourceT'.
196+--
197+-- >>> foreach fail print (source "abc")
198+-- 'a'
199+-- 'b'
200+-- 'c'
201+--
202+foreach
203+ :: Monad m
204+ => (String -> m ()) -- ^ error handler
205+ -> (a -> m ())
206+ -> SourceT m a
207+ -> m ()
208+foreach f g src = unSourceT src (foreachStep f g)
209+
210+-- | See 'foreach'.
211+foreachStep
212+ :: Monad m
213+ => (String -> m ()) -- ^ error handler
214+ -> (a -> m ())
215+ -> StepT m a
216+ -> m ()
217+foreachStep f g = go where
218+ go Stop = return ()
219+ go (Skip s) = go s
220+ go (Yield x s) = g x >> go s
221+ go (Error err) = f err
222+ go (Effect ms) = ms >>= go
223+
224+-------------------------------------------------------------------------------
225+-- Monadic
226+-------------------------------------------------------------------------------
227+
228+fromAction :: Functor m => (a -> Bool) -> m a -> SourceT m a
229+fromAction stop action = SourceT ($ fromActionStep stop action)
230+{-# INLINE fromAction #-}
231+
232+fromActionStep :: Functor m => (a -> Bool) -> m a -> StepT m a
233+fromActionStep stop action = loop where
234+ loop = Effect $ fmap step action
235+ step x
236+ | stop x = Stop
237+ | otherwise = Yield x loop
238+{-# INLINE fromActionStep #-}
239+
240+-------------------------------------------------------------------------------
241+-- File
242+-------------------------------------------------------------------------------
243+
244+-- | Read file.
245+--
246+-- >>> foreach fail BS.putStr (readFile "servant.cabal")
247+-- name: servant
248+-- ...
249+--
250+readFile :: FilePath -> SourceT IO BS.ByteString
251+readFile fp =
252+ SourceT $ \k ->
253+ withFile fp ReadMode $ \hdl ->
254+ k (readHandle hdl)
255+ where
256+ readHandle :: Handle -> StepT IO BS.ByteString
257+ readHandle hdl = fromActionStep BS.null (BS.hGet hdl 4096)
258+
259+-------------------------------------------------------------------------------
260+-- Attoparsec
261+-------------------------------------------------------------------------------
262+
263+-- | Transform using @attoparsec@ parser.
264+--
265+-- Note: @parser@ should not accept empty input!
266+--
267+-- >>> let parser = A.skipWhile A8.isSpace_w8 >> A.takeWhile1 A8.isDigit_w8
268+--
269+-- >>> runExcept $ runSourceT $ transformWithAtto parser (source ["1 2 3"])
270+-- Right ["1","2","3"]
271+--
272+-- >>> runExcept $ runSourceT $ transformWithAtto parser (source ["1", "2", "3"])
273+-- Right ["123"]
274+--
275+-- >>> runExcept $ runSourceT $ transformWithAtto parser (source ["1", "2 3", "4"])
276+-- Right ["12","34"]
277+--
278+-- >>> runExcept $ runSourceT $ transformWithAtto parser (source ["foobar"])
279+-- Left "Failed reading: takeWhile1"
280+--
281+transformWithAtto :: Monad m => A.Parser a -> SourceT m BS.ByteString -> SourceT m a
282+transformWithAtto parser = mapStepT (transformStepWithAtto parser)
283+
284+transformStepWithAtto
285+ :: forall a m. Monad m
286+ => A.Parser a -> StepT m BS.ByteString -> StepT m a
287+transformStepWithAtto parser = go (A.parse parser) where
288+ p0 = A.parse parser
289+
290+ go :: (BS.ByteString -> A.Result a)
291+ -> StepT m BS.ByteString -> StepT m a
292+ go _ (Error err) = Error err
293+ go p (Skip s) = Skip (go p s)
294+ go p (Effect ms) = Effect (fmap (go p) ms)
295+ go p Stop = case p mempty of
296+ A.Fail _ _ err -> Error err
297+ A.Done _ a -> Yield a Stop
298+ A.Partial _ -> Stop
299+ go p (Yield bs0 s) = loop p bs0 where
300+ loop p' bs
301+ | BS.null bs = Skip (go p' s)
302+ | otherwise = case p' bs of
303+ A.Fail _ _ err -> Error err
304+ A.Done bs' a -> Yield a (loop p0 bs')
305+ A.Partial p'' -> Skip (go p'' s)
servant/test/Servant/API/StreamSpec.hsnew file+84
@@ -0,0 +1,84 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Servant.API.StreamSpec where
3+
4+import qualified Data.Aeson as Aeson
5+import qualified Data.ByteString as BS
6+import qualified Data.ByteString.Lazy as LBS
7+import Data.Functor.Identity
8+ (Identity (..))
9+import Data.Proxy
10+ (Proxy (..))
11+import Data.String
12+ (fromString)
13+import Servant.API.Stream
14+import Servant.Types.SourceT
15+import Test.Hspec
16+import Test.QuickCheck
17+ (Property, property, (===))
18+import Test.QuickCheck.Instances ()
19+
20+spec :: Spec
21+spec = describe "Servant.API.Stream" $ do
22+ describe "NoFraming" $ do
23+ let framingUnrender' = framingUnrender (Proxy :: Proxy NoFraming) (Right . LBS.toStrict)
24+ framingRender' = framingRender (Proxy :: Proxy NoFraming) LBS.fromStrict
25+
26+ it "framingUnrender" $
27+ property $ \bss ->
28+ runUnrenderFrames framingUnrender' bss === map Right (bss :: [BS.ByteString])
29+
30+ it "roundtrip" $
31+ property $ roundtrip framingRender' framingUnrender'
32+
33+ describe "NewlineFraming" $ do
34+ let tp = framingUnrender (Proxy :: Proxy NewlineFraming) (Right . LBS.toStrict)
35+
36+ it "framingUnrender examples" $ do
37+ let expected n = map Right [fromString ("foo" ++ show (n :: Int)), "bar", "baz"]
38+
39+ runUnrenderFrames tp ["foo1\nbar\nbaz"] `shouldBe` expected 1
40+ runUnrenderFrames tp ["foo2\n", "bar\n", "baz"] `shouldBe` expected 2
41+ runUnrenderFrames tp ["foo3\nb", "ar\nbaz"] `shouldBe` expected 3
42+
43+ it "roundtrip" $ do
44+ let framingUnrender' = framingUnrender (Proxy :: Proxy NewlineFraming) Aeson.eitherDecode
45+ let framingRender' = framingRender (Proxy :: Proxy NewlineFraming) (Aeson.encode :: Int -> LBS.ByteString)
46+
47+ property $ roundtrip framingRender' framingUnrender'
48+
49+ -- it "fails if input doesn't contain newlines often" $
50+ -- runUnrenderFrames tp ["foo", "bar"] `shouldSatisfy` any isLeft
51+
52+ describe "NetstringFraming" $ do
53+ let tp = framingUnrender (Proxy :: Proxy NetstringFraming) (Right . LBS.toStrict)
54+
55+ it "framingUnrender examples" $ do
56+ let expected n = map Right [fromString ("foo" ++ show (n :: Int)), "bar", "baz"]
57+
58+ runUnrenderFrames tp ["4:foo1,3:bar,3:baz,"] `shouldBe` expected 1
59+ runUnrenderFrames tp ["4:foo2,", "3:bar,", "3:baz,"] `shouldBe` expected 2
60+ runUnrenderFrames tp ["4:foo3,3:b", "ar,3:baz,"] `shouldBe` expected 3
61+
62+ it "roundtrip" $ do
63+ let framingUnrender' = framingUnrender (Proxy :: Proxy NetstringFraming) Aeson.eitherDecode
64+ let framingRender' = framingRender (Proxy :: Proxy NetstringFraming) (Aeson.encode :: Int -> LBS.ByteString)
65+
66+ property $ roundtrip framingRender' framingUnrender'
67+
68+roundtrip
69+ :: (Eq a, Show a)
70+ => (SourceT Identity a -> SourceT Identity LBS.ByteString)
71+ -> (SourceT Identity BS.ByteString -> SourceT Identity a)
72+ -> [a]
73+ -> Property
74+roundtrip render unrender xs =
75+ map Right xs === runUnrenderFrames (unrender . fmap LBS.toStrict . render) xs
76+
77+runUnrenderFrames :: (SourceT Identity b -> SourceT Identity a) -> [b] -> [Either String a]
78+runUnrenderFrames f = go . Effect . flip unSourceT return . f . source where
79+ go :: StepT Identity a -> [Either String a]
80+ go Stop = []
81+ go (Error err) = [Left err]
82+ go (Skip s) = go s
83+ go (Yield x s) = Right x : go s
84+ go (Effect ms) = go (runIdentity ms)
stack.yaml+4
@@ -8,6 +8,10 @@ packages:
88- servant-server/
99- servant/
1010
11+- servant-conduit
12+- servant-machines/
13+- servant-pipes/
14+
1115# allow-newer: true # ignores all bounds, that's a sledgehammer
1216# - doc/tutorial/
1317
streaming-benchmark.shnew file+268
@@ -0,0 +1,268 @@
1+#!/bin/sh
2+
3+set -e
4+
5+SIZE=10000000
6+SLOWURL="http://localhost:8000/slow/2"
7+FASTURL="http://localhost:8000/get/$SIZE"
8+PROXYURL="http://localhost:8000/proxy"
9+
10+# we use binary, as there's some size
11+TESTFILE="$(cabal-plan list-bin servant-machines:test:example)"
12+TMPFILE="$(mktemp)"
13+
14+CURLSTATS="time: %{time_total}, size: %{size_download}, download speed: %{speed_download}\n"
15+
16+# cleanup
17+
18+cleanup() {
19+ if [ ! -z "$MACHINES_PID" ]; then
20+ kill "$MACHINES_PID" || true
21+ fi
22+
23+ if [ ! -z "$CONDUIT_PID" ]; then
24+ kill "$CONDUIT_PID" || true
25+ fi
26+
27+ if [ ! -z "$PIPES_PID" ]; then
28+ kill "$PIPES_PID" || true
29+ fi
30+
31+ if [ ! -z "$COOKBOOK_PID" ]; then
32+ kill "$COOKBOOK_PID" || true
33+ fi
34+
35+ rm -f "$TMPFILE"
36+}
37+
38+trap cleanup EXIT
39+
40+bench() {
41+ echo "\e[36m=== $1 ===\e[0m"
42+}
43+
44+# Server
45+#######################################################################
46+
47+## Machines
48+
49+bench "server machines"
50+
51+$(cabal-plan list-bin servant-machines:test:example) server +RTS -sbench-machines-server-rts.txt &
52+MACHINES_PID=$!
53+echo "Starting servant-machines server. PID=$MACHINES_PID"
54+
55+# Time to startup
56+sleep 1
57+
58+# Run slow url to test & warm-up server
59+curl "$SLOWURL"
60+
61+curl --silent --show-error "$FASTURL" --output /dev/null --write-out "$CURLSTATS" > bench-machines-server.txt
62+
63+curl --silent --show-error "$PROXYURL" --request POST --data-binary @"$TESTFILE" --output "$TMPFILE" --write-out "$CURLSTATS" > bench-machines-server-proxy.txt
64+
65+kill -INT $MACHINES_PID
66+unset MACHINES_PID
67+
68+## Pipes
69+
70+bench "server pipes"
71+
72+$(cabal-plan list-bin servant-pipes:test:example) server +RTS -sbench-pipes-server-rts.txt &
73+PIPES_PID=$!
74+echo "Starting servant-pipes server. PID=$PIPES_PID"
75+
76+# Time to startup
77+sleep 1
78+
79+# Run slow url to test & warm-up server
80+curl "$SLOWURL"
81+
82+curl --silent --show-error "$FASTURL" --output /dev/null --write-out "$CURLSTATS" > bench-pipes-server.txt
83+
84+curl --silent --show-error "$PROXYURL" --request POST --data-binary @"$TESTFILE" --output "$TMPFILE" --write-out "$CURLSTATS" > bench-pipes-server-proxy.txt
85+
86+kill -INT $PIPES_PID
87+unset PIPES_PID
88+
89+## Cookbook
90+
91+bench "server cookbook"
92+
93+$(cabal-plan list-bin cookbook-basic-streaming) server +RTS -sbench-cookbook-server-rts.txt &
94+COOKBOOK_PID=$!
95+echo "Starting servant-cookbook server. PID=$COOKBOOK_PID"
96+
97+# Time to startup
98+sleep 1
99+
100+# Run slow url to test & warm-up server
101+curl "$SLOWURL"
102+
103+curl --silent --show-error "$FASTURL" --output /dev/null --write-out "$CURLSTATS" > bench-cookbook-server.txt
104+
105+curl --silent --show-error "$PROXYURL" --request POST --data-binary @"$TESTFILE" --output "$TMPFILE" --write-out "$CURLSTATS" > bench-cookbook-server-proxy.txt
106+
107+kill -INT $COOKBOOK_PID
108+unset COOKBOOK_PID
109+
110+## Conduit
111+
112+bench "server conduit"
113+
114+$(cabal-plan list-bin servant-conduit:test:example) server +RTS -sbench-conduit-server-rts.txt &
115+CONDUIT_PID=$!
116+echo "Starting servant-conduit server. PID=$CONDUIT_PID"
117+
118+# Time to startup
119+sleep 1
120+
121+# Run slow url to test & warm-up server
122+curl "$SLOWURL"
123+
124+curl --silent --show-error "$FASTURL" --output /dev/null --write-out "$CURLSTATS" > bench-conduit-server.txt
125+
126+curl --silent --show-error "$PROXYURL" --request POST --data-binary @"$TESTFILE" --output "$TMPFILE" --write-out "$CURLSTATS" > bench-conduit-server-proxy.txt
127+
128+# kill -INT $CONDUIT_PID
129+# unset CONDUIT_PID
130+
131+# Client
132+#######################################################################
133+
134+# Uses conduit as server
135+
136+## Machines
137+
138+bench "client machines"
139+
140+# Test run
141+$(cabal-plan list-bin servant-machines:test:example) client 10
142+
143+# Real run
144+/usr/bin/time --verbose --output bench-machines-client-time.txt \
145+ "$(cabal-plan list-bin servant-machines:test:example)" client "$SIZE" +RTS -sbench-machines-client-rts.txt
146+
147+## Pipes
148+
149+bench "server pipes"
150+
151+# Test run
152+$(cabal-plan list-bin servant-pipes:test:example) client 10
153+
154+# Real run
155+/usr/bin/time --verbose --output bench-pipes-client-time.txt \
156+ "$(cabal-plan list-bin servant-pipes:test:example)" client "$SIZE" +RTS -sbench-pipes-client-rts.txt
157+
158+## Conduit
159+
160+bench "client conduit"
161+
162+# Test run
163+$(cabal-plan list-bin servant-conduit:test:example) client 10
164+
165+# Real run
166+/usr/bin/time --verbose --output bench-conduit-client-time.txt \
167+ "$(cabal-plan list-bin servant-conduit:test:example)" client "$SIZE" +RTS -sbench-conduit-client-rts.txt
168+
169+## Cookbook
170+
171+bench "server cookbook"
172+
173+# Test run
174+$(cabal-plan list-bin cookbook-basic-streaming) client 10
175+
176+# Real run
177+/usr/bin/time --verbose --output bench-cookbook-client-time.txt \
178+ "$(cabal-plan list-bin cookbook-basic-streaming)" client "$SIZE" +RTS -sbench-cookbook-client-rts.txt
179+
180+## Kill server
181+
182+kill -INT $CONDUIT_PID
183+unset CONDUIT_PID
184+sleep 1
185+
186+# Exit
187+#######################################################################
188+
189+header() {
190+ { echo "$1 $2";
191+ echo ""
192+ } >> bench.md
193+}
194+
195+report() {
196+ echo "\`\`\`" >> bench.md
197+ cat "$1" >> bench.md
198+ echo "\`\`\`" >> bench.md
199+ echo "" >> bench.md
200+}
201+
202+report2() {
203+ echo "\`\`\`" >> bench.md
204+ cat "$1" | sed 's/^\s*//' >> bench.md
205+ echo "\`\`\`" >> bench.md
206+ echo "" >> bench.md
207+}
208+
209+note() {
210+ echo "$1" >> bench.md
211+ echo "" >> bench.md
212+}
213+
214+rm -f bench.md
215+
216+header "#" "Streaming test benchmark"
217+note "size parameter: $SIZE"
218+
219+header "##" Server
220+
221+note "- /fast/$SIZE\n- /proxy"
222+
223+header "###" machines
224+report bench-machines-server.txt
225+report bench-machines-server-proxy.txt
226+report bench-machines-server-rts.txt
227+
228+header "###" pipes
229+report bench-pipes-server.txt
230+report bench-pipes-server-proxy.txt
231+report bench-pipes-server-rts.txt
232+
233+header "###" conduit
234+note "Conduit server is also used for client tests below"
235+report bench-conduit-server.txt
236+report bench-conduit-server-proxy.txt
237+report bench-conduit-server-rts.txt
238+
239+header "###" cookbook
240+report bench-cookbook-server.txt
241+report bench-cookbook-server-proxy.txt
242+report bench-cookbook-server-rts.txt
243+
244+header "##" Client
245+
246+header "###" machines
247+report2 bench-machines-client-time.txt
248+report bench-machines-client-rts.txt
249+
250+header "###" pipes
251+report2 bench-pipes-client-time.txt
252+report bench-pipes-client-rts.txt
253+
254+header "###" conduit
255+report2 bench-conduit-client-time.txt
256+report bench-conduit-client-rts.txt
257+
258+header "###" cookbook
259+report2 bench-cookbook-client-time.txt
260+report bench-cookbook-client-rts.txt
261+
262+# Cleanup filepaths
263+sed -E -i 's/\/[^ ]*machines[^ ]*\/example/...machines:example/' bench.md
264+sed -E -i 's/\/[^ ]*conduit[^ ]*\/example/...conduit:example/' bench.md
265+sed -E -i 's/\/[^ ]*pipes[^ ]*\/example/...pipes:example/' bench.md
266+sed -E -i 's/\/[^ ]*\/cookbook-basic-streaming/...cookbook-basic-streaming/' bench.md
267+
268+sleep 3