on master help sign in
Refactor Stream stuff
45c1cbdOleg Grenrus
.gitignore+3
@@ -29,3 +29,6 @@ doc/_build
29doc/venv
29doc/venv
30doc/tutorial/static/api.js
30doc/tutorial/static/api.js
31doc/tutorial/static/jq.js
31doc/tutorial/static/jq.js
32+
33+# local versions of things
34+servant-multipart
.travis.yml+17−5
@@ -69,11 +69,11 @@ install:
69 - rm -fv cabal.project cabal.project.local
69 - rm -fv cabal.project cabal.project.local
70 - "if [ $HCNUMVER -ge 70800 ]; then sed -i.bak 's/-- ghc-options:.*/ghc-options: -j2/' ${HOME}/.cabal/config; fi"
70 - "if [ $HCNUMVER -ge 70800 ]; then sed -i.bak 's/-- ghc-options:.*/ghc-options: -j2/' ${HOME}/.cabal/config; fi"
71 - grep -Ev -- '^\s*--' ${HOME}/.cabal/config | grep -Ev '^\s*$'
71 - 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"
73 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
73 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
74 - "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"
74 - "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"
75 - touch cabal.project.local
75 - 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"
77 - cat cabal.project || true
77 - cat cabal.project || true
78 - cat cabal.project.local || true
78 - cat cabal.project.local || true
79 - if [ -f "servant/configure.ac" ]; then
79 - if [ -f "servant/configure.ac" ]; then
@@ -97,12 +97,24 @@ install:
97 - if [ -f "doc/tutorial/configure.ac" ]; then
97 - if [ -f "doc/tutorial/configure.ac" ]; then
98 (cd "doc/tutorial" && autoreconf -i);
98 (cd "doc/tutorial" && autoreconf -i);
99 fi
99 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
100 - if [ -f "doc/cookbook/basic-auth/configure.ac" ]; then
109 - if [ -f "doc/cookbook/basic-auth/configure.ac" ]; then
101 (cd "doc/cookbook/basic-auth" && autoreconf -i);
110 (cd "doc/cookbook/basic-auth" && autoreconf -i);
102 fi
111 fi
103 - if [ -f "doc/cookbook/curl-mock/configure.ac" ]; then
112 - if [ -f "doc/cookbook/curl-mock/configure.ac" ]; then
104 (cd "doc/cookbook/curl-mock" && autoreconf -i);
113 (cd "doc/cookbook/curl-mock" && autoreconf -i);
105 fi
114 fi
115+ - if [ -f "doc/cookbook/basic-streaming/configure.ac" ]; then
116+ (cd "doc/cookbook/basic-streaming" && autoreconf -i);
117+ fi
106 - if [ -f "doc/cookbook/db-postgres-pool/configure.ac" ]; then
118 - if [ -f "doc/cookbook/db-postgres-pool/configure.ac" ]; then
107 (cd "doc/cookbook/db-postgres-pool" && autoreconf -i);
119 (cd "doc/cookbook/db-postgres-pool" && autoreconf -i);
108 fi
120 fi
@@ -134,7 +146,7 @@ install:
134 (cd "doc/cookbook/using-free-client" && autoreconf -i);
146 (cd "doc/cookbook/using-free-client" && autoreconf -i);
135 fi
147 fi
136 - rm -f cabal.project.freeze
148 - 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
138 - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
150 - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
139
151
140# Here starts the actual work to be performed for the package under test;
152# Here starts the actual work to be performed for the package under test;
@@ -148,11 +160,11 @@ script:
148 - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
160 - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
149 - cd ${DISTDIR} || false
161 - cd ${DISTDIR} || false
150 - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
162 - 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"
152 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
164 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
153 - "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"
165 - "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"
154 - touch cabal.project.local
166 - 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"
156 - cat cabal.project || true
168 - cat cabal.project || true
157 - cat cabal.project.local || true
169 - cat cabal.project.local || true
158 - echo -en 'travis_fold:end:unpack\\r'
170 - echo -en 'travis_fold:end:unpack\\r'
cabal.project+5
@@ -6,10 +6,15 @@ packages: servant/
6 servant-server/
6 servant-server/
7 doc/tutorial/
7 doc/tutorial/
8
8
9+ servant-machines/
10+ servant-conduit/
11+ servant-pipes/
12+
9 -- doc/cookbook/*/*.cabal
13 -- doc/cookbook/*/*.cabal
10
14
11 doc/cookbook/basic-auth
15 doc/cookbook/basic-auth
12 doc/cookbook/curl-mock
16 doc/cookbook/curl-mock
17+ doc/cookbook/basic-streaming
13 doc/cookbook/db-postgres-pool
18 doc/cookbook/db-postgres-pool
14 doc/cookbook/db-sqlite-simple
19 doc/cookbook/db-sqlite-simple
15 doc/cookbook/file-upload
20 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]
129
129
130### `StreamGet` and `StreamPost`
130### `StreamGet` and `StreamPost`
131
131
132+*Note*: Streaming has changed considerably in `servant-0.15`.
133+
132The `StreamGet` and `StreamPost` combinators are defined in terms of the more general `Stream`
134The `StreamGet` and `StreamPost` combinators are defined in terms of the more general `Stream`
133
135
134``` haskell ignore
136``` haskell ignore
135−data Stream (method :: k1) (framing :: *) (contentType :: *) a
137+data Stream (method :: k1) (status :: Nat) (framing :: *) (contentType :: *) (a :: *)
136−type StreamGet = Stream 'GET
137−type StreamPost = Stream 'POST
138−```
139
138
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+```
141
142
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.
142
149
143### `Capture`
150### `Capture`
144
151
doc/tutorial/Client.lhs+21−21
@@ -20,6 +20,8 @@ import GHC.Generics
20import Network.HTTP.Client (newManager, defaultManagerSettings)
20import Network.HTTP.Client (newManager, defaultManagerSettings)
21import Servant.API
21import Servant.API
22import Servant.Client
22import Servant.Client
23+import Servant.Types.SourceT (foreach)
24+import Control.Monad.Codensity (Codensity)
23```
25```
24
26
25Also, we need examples for some domain specific data types:
27Also, we need examples for some domain specific data types:
@@ -217,41 +219,39 @@ getClients clientEnv
217Consider the following streaming API type:
219Consider the following streaming API type:
218
220
219``` haskell
221``` haskell
220−type StreamAPI = "positionStream" :> StreamGet NewlineFraming JSON (ResultStream Position)
222+type StreamAPI = "positionStream" :> StreamGet NewlineFraming JSON (SourceIO Position)
221```
223```
222
224
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:
225+Note that we use the same `SourceIO` type as on the server-side
224−
226+(this is different from `servant-0.14`).
225−``` haskell ignore
226−type StreamAPI f = "positionStream" :> StreamGet NewlineFraming JSON (f Position)
227−type ServerStreamAPI = StreamAPI StreamGenerator
228−type ClientStreamAPI = StreamAPI ResultStream
229−```
230
227
231In any case, here's how we write a function to query our API:
228In any case, here's how we write a function to query our API:
232
229
233−``` haskell
230+```haskell
234streamAPI :: Proxy StreamAPI
231streamAPI :: Proxy StreamAPI
235streamAPI = Proxy
232streamAPI = Proxy
236
233
237−posStream :: ClientM (ResultStream Position)
234+posStream :: ClientM (Codensity IO (SourceIO Position))
238−
239posStream = client streamAPI
235posStream = client streamAPI
240```
236```
241
237
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
242−And here's how to just print out all elements from a `ResultStream`, to give some idea of how to work with them.
243+print out all elements from a `SourceIO`, to give some idea of how to work with
244+them.
243
245
244``` haskell
246``` haskell
245−printResultStream :: Show a => ResultStream a -> IO ()
247+printSourceIO :: Show a => ClientEnv -> ClientM (Codensity IO (SourceIO a)) -> IO ()
246−printResultStream (ResultStream k) = k $ \getResult ->
248+printSourceIO env c = withClientM c env $ \e -> case e of
247− let loop = do
249+ Left err -> putStrLn $ "Error: " ++ show err
248− r <- getResult
250+ Right rs -> foreach fail print rs
249− case r of
250− Nothing -> return ()
251− Just x -> print x >> loop
252− in loop
253```
251```
254
252
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.
256
256
257You now know how to use **servant-client**!
257You now know how to use **servant-client**!
doc/tutorial/Server.lhs+22−8
@@ -46,6 +46,7 @@ import Servant
46import System.Directory
46import System.Directory
47import Text.Blaze
47import Text.Blaze
48import Text.Blaze.Html.Renderer.Utf8
48import Text.Blaze.Html.Renderer.Utf8
49+import Servant.Types.SourceT (source)
49import qualified Data.Aeson.Parser
50import qualified Data.Aeson.Parser
50import qualified Text.Blaze.Html
51import qualified Text.Blaze.Html
51```
52```
@@ -1160,24 +1161,37 @@ app5 = serve readerAPI (hoistServer readerAPI funToHandler funServerT)
1160
1161
1161## Streaming endpoints
1162## Streaming endpoints
1162
1163
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.
1164
1174
1165``` haskell
1175``` haskell
1166−type StreamAPI = "userStream" :> StreamGet NewlineFraming JSON (StreamGenerator User)
1176+type StreamAPI = "userStream" :> StreamGet NewlineFraming JSON (SourceIO User)
1167streamAPI :: Proxy StreamAPI
1177streamAPI :: Proxy StreamAPI
1168streamAPI = Proxy
1178streamAPI = Proxy
1169
1179
1170−streamUsers :: StreamGenerator User
1180+streamUsers :: SourceIO User
1171−streamUsers = StreamGenerator $ \sendFirst sendRest -> do
1181+streamUsers = source [isaac, albert, albert]
1172− sendFirst isaac
1173− sendRest albert
1174− sendRest albert
1175
1182
1176app6 :: Application
1183app6 :: Application
1177app6 = serve streamAPI (return streamUsers)
1184app6 = serve streamAPI (return streamUsers)
1178```
1185```
1179
1186
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.
1181
1195
1182## Conclusion
1196## Conclusion
1183
1197
doc/tutorial/tutorial.cabal+1
@@ -46,6 +46,7 @@ library
46 , http-client
46 , http-client
47 , http-media
47 , http-media
48 , http-types
48 , http-types
49+ , kan-extensions
49 , mtl
50 , mtl
50 , string-conversions
51 , string-conversions
51 , text
52 , text
servant-client-core/servant-client-core.cabal+1
@@ -70,6 +70,7 @@ library
70 , http-api-data >= 0.3.8.1 && < 0.4
70 , http-api-data >= 0.3.8.1 && < 0.4
71 , http-media >= 0.7.1.2 && < 0.8
71 , http-media >= 0.7.1.2 && < 0.8
72 , http-types >= 0.12.1 && < 0.13
72 , http-types >= 0.12.1 && < 0.13
73+ , kan-extensions >= 5.2 && < 5.3
73 , network-uri >= 2.6.1.0 && < 2.7
74 , network-uri >= 2.6.1.0 && < 2.7
74 , safe >= 0.3.17 && < 0.4
75 , safe >= 0.3.17 && < 0.4
75
76
servant-client-core/src/Servant/Client/Core.hs+1−1
@@ -44,7 +44,7 @@ module Servant.Client.Core
44 , GenResponse (..)
44 , GenResponse (..)
45 , RunClient(..)
45 , RunClient(..)
46 , module Servant.Client.Core.Internal.BaseUrl
46 , module Servant.Client.Core.Internal.BaseUrl
47− , StreamingResponse(..)
47+ , StreamingResponse
48
48
49 -- * Writing HasClient instances
49 -- * Writing HasClient instances
50 -- | These functions need not be re-exported by backend libraries.
50 -- | 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
16import Prelude ()
16import Prelude ()
17import Prelude.Compat
17import Prelude.Compat
18
18
19−import Control.Concurrent
19+import qualified Data.ByteString as BS
20− (modifyMVar, newMVar)
21−import Control.Monad.IO.Class
22− (MonadIO (..))
23import qualified Data.ByteString.Lazy as BL
20import qualified Data.ByteString.Lazy as BL
24import Data.Foldable
21import Data.Foldable
25 (toList)
22 (toList)
@@ -27,8 +24,6 @@ import Data.List
27 (foldl')
24 (foldl')
28import Data.Proxy
25import Data.Proxy
29 (Proxy (Proxy))
26 (Proxy (Proxy))
30−import Data.Semigroup
31− ((<>))
32import Data.Sequence
27import Data.Sequence
33 (fromList)
28 (fromList)
34import Data.String
29import Data.String
@@ -40,19 +35,22 @@ import GHC.TypeLits
40import qualified Network.HTTP.Types as H
35import qualified Network.HTTP.Types as H
41import Servant.API
36import Servant.API
42 ((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth, BasicAuthData,
37 ((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth, BasicAuthData,
43− BuildHeadersTo (..), ByteStringParser (..), Capture',
38+ BuildHeadersTo (..), Capture', CaptureAll, Description,
44− CaptureAll, Description, EmptyAPI, FramingUnrender (..),
39+ EmptyAPI, FramingUnrender (..), FromSourceIO (..),
45− FromResultStream (..), Header', Headers (..), HttpVersion,
40+ Header', Headers (..), HttpVersion, IsSecure,
46− IsSecure, MimeRender (mimeRender),
41+ MimeRender (mimeRender), MimeUnrender (mimeUnrender),
47− MimeUnrender (mimeUnrender), NoContent (NoContent), QueryFlag,
42+ NoContent (NoContent), QueryFlag, QueryParam', QueryParams,
48− QueryParam', QueryParams, Raw, ReflectMethod (..), RemoteHost,
43+ Raw, ReflectMethod (..), RemoteHost, ReqBody', SBoolI, Stream,
49− ReqBody', ResultStream (..), SBoolI, Stream, Summary,
44+ StreamBody, Summary, ToHttpApiData, Vault, Verb,
50− ToHttpApiData, Vault, Verb, WithNamedContext, contentType,
45+ WithNamedContext, contentType, getHeadersHList, getResponse,
51− getHeadersHList, getResponse, toQueryParam, toUrlPiece)
46+ toQueryParam, toUrlPiece)
52import Servant.API.ContentTypes
47import Servant.API.ContentTypes
53 (contentTypes)
48 (contentTypes)
54import Servant.API.Modifiers
49import Servant.API.Modifiers
55 (FoldRequired, RequiredArgument, foldRequiredArgument)
50 (FoldRequired, RequiredArgument, foldRequiredArgument)
51+import Control.Monad.Codensity
52+ (Codensity (..))
53+import qualified Servant.Types.SourceT as S
56
54
57import Servant.Client.Core.Internal.Auth
55import Servant.Client.Core.Internal.Auth
58import Servant.Client.Core.Internal.BasicAuth
56import Servant.Client.Core.Internal.BasicAuth
@@ -274,54 +272,25 @@ instance {-# OVERLAPPING #-}
274 hoistClientMonad _ _ f ma = f ma
272 hoistClientMonad _ _ f ma = f ma
275
273
276instance {-# OVERLAPPABLE #-}
274instance {-# OVERLAPPABLE #-}
277− ( RunClient m, MonadIO m, MimeUnrender ct a, ReflectMethod method,
275+ ( RunClient m, MimeUnrender ct chunk, ReflectMethod method,
278− FramingUnrender framing a, FromResultStream a b
276+ FramingUnrender framing, FromSourceIO chunk a
279− ) => HasClient m (Stream method status framing ct b) where
277+ ) => HasClient m (Stream method status framing ct a) where
280
278
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
282
282
283 clientWithRoute _pm Proxy req = do
283 clientWithRoute _pm Proxy req = do
284− sresp <- streamingRequest req
284+ sresp <- streamingRequest req
285 { requestAccept = fromList [contentType (Proxy :: Proxy ct)]
285 { requestAccept = fromList [contentType (Proxy :: Proxy ct)]
286 , requestMethod = reflectMethod (Proxy :: Proxy method)
286 , requestMethod = reflectMethod (Proxy :: Proxy method)
287 }
287 }
288− liftIO $ fromResultStream $ ResultStream $ \k ->
288+ return $ do
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
289+ let mimeUnrender' = mimeUnrender (Proxy :: Proxy ct) :: BL.ByteString -> Either String chunk
319− Left err -> Left err
290+ framingUnrender' = framingUnrender (Proxy :: Proxy framing) mimeUnrender'
320− Right x -> Right x
291+ gres <- sresp
321− processResult (Left err, _) = Just (Left err)
292+ return $ fromSourceIO $ framingUnrender' $ S.fromAction BS.null (responseBody gres)
322− k go
323
293
324− hoistClientMonad _ _ f ma = f ma
325
294
326-- | If you use a 'Header' in one of your endpoints in your API,
295-- | If you use a 'Header' in one of your endpoints in your API,
327-- the corresponding querying function will automatically take
296-- the corresponding querying function will automatically take
@@ -573,6 +542,18 @@ instance (MimeRender ct a, HasClient m api)
573 hoistClientMonad pm _ f cl = \a ->
542 hoistClientMonad pm _ f cl = \a ->
574 hoistClientMonad pm (Proxy :: Proxy api) f (cl a)
543 hoistClientMonad pm (Proxy :: Proxy api) f (cl a)
575
544
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+
576-- | Make the querying function append @path@ to the request path.
557-- | Make the querying function append @path@ to the request path.
577instance (KnownSymbol path, HasClient m api) => HasClient m (path :> api) where
558instance (KnownSymbol path, HasClient m api) => HasClient m (path :> api) where
578 type Client m (path :> api) = Client m api
559 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
38import Network.HTTP.Types
38import Network.HTTP.Types
39 (Header, HeaderName, HttpVersion, Method, QueryItem, Status,
39 (Header, HeaderName, HttpVersion, Method, QueryItem, Status,
40 http11, methodGet)
40 http11, methodGet)
41+import Control.Monad.Codensity
42+ (Codensity (..))
41import Web.HttpApiData
43import Web.HttpApiData
42 (ToHttpApiData, toEncodedUrlPiece, toHeader)
44 (ToHttpApiData, toEncodedUrlPiece, toHeader)
43
45
@@ -89,7 +91,7 @@ data GenResponse a = Response
89 } deriving (Eq, Show, Generic, Typeable, Functor, Foldable, Traversable)
91 } deriving (Eq, Show, Generic, Typeable, Functor, Foldable, Traversable)
90
92
91type Response = GenResponse LBS.ByteString
93type 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))
93
95
94-- A GET request to the top-level path
96-- A GET request to the top-level path
95defaultRequest :: Request
97defaultRequest :: Request
servant-client-core/src/Servant/Client/Core/Internal/RunClient.hs+1−1
@@ -26,7 +26,7 @@ import Servant.API
26import Servant.Client.Core.Internal.ClientF
26import Servant.Client.Core.Internal.ClientF
27import Servant.Client.Core.Internal.Request
27import Servant.Client.Core.Internal.Request
28 (GenResponse (..), Request, Response, ServantError (..),
28 (GenResponse (..), Request, Response, ServantError (..),
29− StreamingResponse (..))
29+ StreamingResponse)
30
30
31class Monad m => RunClient m where
31class Monad m => RunClient m where
32 -- | How to make a request.
32 -- | How to make a request.
servant-client/servant-client.cabal+10−5
@@ -50,7 +50,8 @@ library
50
50
51 -- Servant dependencies
51 -- Servant dependencies
52 build-depends:
52 build-depends:
53+ servant == 0.14.*
53− servant-client-core == 0.14.*
54+ , servant-client-core == 0.14.*
54
55
55 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
56 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
56 -- Here can be exceptions if we really need features from the newer versions.
57 -- Here can be exceptions if we really need features from the newer versions.
@@ -60,6 +61,7 @@ library
60 , http-media >= 0.7.1.2 && < 0.8
61 , http-media >= 0.7.1.2 && < 0.8
61 , http-types >= 0.12.1 && < 0.13
62 , http-types >= 0.12.1 && < 0.13
62 , exceptions >= 0.10.0 && < 0.11
63 , exceptions >= 0.10.0 && < 0.11
64+ , kan-extensions >= 5.2 && < 5.3
63 , monad-control >= 1.0.2.3 && < 1.1
65 , monad-control >= 1.0.2.3 && < 1.1
64 , semigroupoids >= 5.2.2 && < 5.4
66 , semigroupoids >= 5.2.2 && < 5.4
65 , stm >= 2.4.5.0 && < 2.6
67 , stm >= 2.4.5.0 && < 2.6
@@ -72,7 +74,7 @@ library
72
74
73test-suite spec
75test-suite spec
74 type: exitcode-stdio-1.0
76 type: exitcode-stdio-1.0
75− ghc-options: -Wall -rtsopts -with-rtsopts=-T
77+ ghc-options: -Wall -rtsopts -threaded "-with-rtsopts=-T -N2"
76 default-language: Haskell2010
78 default-language: Haskell2010
77 hs-source-dirs: test
79 hs-source-dirs: test
78 main-is: Spec.hs
80 main-is: Spec.hs
@@ -90,6 +92,7 @@ test-suite spec
90 , http-client
92 , http-client
91 , http-types
93 , http-types
92 , mtl
94 , mtl
95+ , kan-extensions
93 , servant-client
96 , servant-client
94 , servant-client-core
97 , servant-client-core
95 , text
98 , text
@@ -100,16 +103,18 @@ test-suite spec
100
103
101 -- Additonal dependencies
104 -- Additonal dependencies
102 build-depends:
105 build-depends:
106+ entropy >= 0.4.1.3 && < 0.5
103− generics-sop >= 0.4.0.1 && < 0.5
107+ , generics-sop >= 0.4.0.1 && < 0.5
104− , hspec >= 2.5.1 && < 2.6
108+ , hspec >= 2.5.8 && < 2.6
105 , HUnit >= 1.6 && < 1.7
109 , HUnit >= 1.6 && < 1.7
106 , network >= 2.8.0.0 && < 2.9
110 , network >= 2.8.0.0 && < 2.9
107 , QuickCheck >= 2.12.6.1 && < 2.13
111 , QuickCheck >= 2.12.6.1 && < 2.13
108 , servant == 0.14.*
112 , servant == 0.14.*
109 , servant-server == 0.14.*
113 , servant-server == 0.14.*
114+ , tdigest >= 0.2 && < 0.3
110
115
111 build-tool-depends:
116 build-tool-depends:
112− hspec-discover:hspec-discover >= 2.5.1 && < 2.6
117+ hspec-discover:hspec-discover >= 2.5.8 && < 2.6
113
118
114test-suite readme
119test-suite readme
115 type: exitcode-stdio-1.0
120 type: exitcode-stdio-1.0
servant-client/src/Servant/Client.hs+1
@@ -7,6 +7,7 @@ module Servant.Client
7 ( client
7 ( client
8 , ClientM
8 , ClientM
9 , runClientM
9 , runClientM
10+ , withClientM
10 , ClientEnv(..)
11 , ClientEnv(..)
11 , mkClientEnv
12 , mkClientEnv
12 , hoistClient
13 , hoistClient
servant-client/src/Servant/Client/Internal/HttpClient.hs+14−1
@@ -21,6 +21,8 @@ import Control.Monad.Base
21 (MonadBase (..))
21 (MonadBase (..))
22import Control.Monad.Catch
22import Control.Monad.Catch
23 (MonadCatch, MonadThrow)
23 (MonadCatch, MonadThrow)
24+import Control.Monad.Codensity
25+ (Codensity (..))
24import Control.Monad.Error.Class
26import Control.Monad.Error.Class
25 (MonadError (..))
27 (MonadError (..))
26import Control.Monad.Reader
28import Control.Monad.Reader
@@ -141,6 +143,17 @@ instance ClientLike (ClientM a) (ClientM a) where
141runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
143runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
142runClientM cm env = runExceptT $ flip runReaderT env $ unClientM cm
144runClientM cm env = runExceptT $ flip runReaderT env $ unClientM cm
143
145
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+
144performRequest :: Request -> ClientM Response
157performRequest :: Request -> ClientM Response
145performRequest req = do
158performRequest req = do
146 ClientEnv m burl cookieJar' <- ask
159 ClientEnv m burl cookieJar' <- ask
@@ -178,7 +191,7 @@ performStreamingRequest req = do
178 m <- asks manager
191 m <- asks manager
179 burl <- asks baseUrl
192 burl <- asks baseUrl
180 let request = requestToClientRequest burl req
193 let request = requestToClientRequest burl req
181− return $ StreamingResponse $
194+ return $ Codensity $
182 \k -> Client.withResponse request m $
195 \k -> Client.withResponse request m $
183 \r -> do
196 \r -> do
184 let status = Client.responseStatus r
197 let status = Client.responseStatus r
servant-client/test/Servant/StreamSpec.hs+94−66
@@ -22,32 +22,38 @@
22module Servant.StreamSpec (spec) where
22module Servant.StreamSpec (spec) where
23
23
24import Control.Monad
24import Control.Monad
25− (replicateM_, void)
25+ (when)
26+import Control.Monad.Codensity
27+ (Codensity (..))
28+import Control.Monad.IO.Class
29+ (MonadIO (..))
30+import Control.Monad.Trans.Except
26−import qualified Data.ByteString as BS
31+import qualified Data.ByteString as BS
27import Data.Proxy
32import Data.Proxy
33+import qualified Data.TDigest as TD
28−import qualified Network.HTTP.Client as C
34+import qualified Network.HTTP.Client as C
29import Prelude ()
35import Prelude ()
30import Prelude.Compat
36import 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−
38import Servant.API
37import Servant.API
39 ((:<|>) ((:<|>)), (:>), JSON, NetstringFraming,
38 ((:<|>) ((:<|>)), (:>), JSON, NetstringFraming,
40− NewlineFraming, NoFraming, OctetStream, ResultStream (..),
39+ NewlineFraming, NoFraming, OctetStream, SourceIO, StreamGet)
41− StreamGenerator (..), StreamGet)
42import Servant.Client
40import Servant.Client
43import Servant.ClientSpec
41import Servant.ClientSpec
44 (Person (..))
42 (Person (..))
45−import qualified Servant.ClientSpec as CS
43+import qualified Servant.ClientSpec as CS
46import Servant.Server
44import 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
47
53
48#if MIN_VERSION_base(4,10,0)
54#if MIN_VERSION_base(4,10,0)
49import GHC.Stats
55import GHC.Stats
50− (gc, gcdetails_mem_in_use_bytes, getRTSStats)
56+ (gc, gcdetails_live_bytes, getRTSStats)
51#else
57#else
52import GHC.Stats
58import GHC.Stats
53 (currentBytesUsed, getGCStats)
59 (currentBytesUsed, getGCStats)
@@ -57,21 +63,17 @@ spec :: Spec
57spec = describe "Servant.Stream" $ do
63spec = describe "Servant.Stream" $ do
58 streamSpec
64 streamSpec
59
65
60−type StreamApi f =
66+type StreamApi =
61− "streamGetNewline" :> StreamGet NewlineFraming JSON (f Person)
67+ "streamGetNewline" :> StreamGet NewlineFraming JSON (SourceIO Person)
62− :<|> "streamGetNetstring" :> StreamGet NetstringFraming JSON (f Person)
68+ :<|> "streamGetNetstring" :> StreamGet NetstringFraming JSON (SourceIO Person)
63− :<|> "streamALot" :> StreamGet NoFraming OctetStream (f BS.ByteString)
69+ :<|> "streamALot" :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)
64−
65−
66−capi :: Proxy (StreamApi ResultStream)
67−capi = Proxy
68
70
69−sapi :: Proxy (StreamApi StreamGenerator)
71+api :: Proxy StreamApi
70−sapi = Proxy
72+api = Proxy
71
73
72−getGetNL, getGetNS :: ClientM (ResultStream Person)
74+getGetNL, getGetNS :: ClientM (Codensity IO (SourceIO Person))
73−getGetALot :: ClientM (ResultStream BS.ByteString)
75+getGetALot :: ClientM (Codensity IO (SourceIO BS.ByteString))
74−getGetNL :<|> getGetNS :<|> getGetALot = client capi
76+getGetNL :<|> getGetNS :<|> getGetALot = client api
75
77
76alice :: Person
78alice :: Person
77alice = Person "Alice" 42
79alice = Person "Alice" 42
@@ -80,25 +82,23 @@ bob :: Person
80bob = Person "Bob" 25
82bob = Person "Bob" 25
81
83
82server :: Application
84server :: Application
83−server = serve sapi
85+server = serve api
84− $ return (StreamGenerator (\f r -> f alice >> r bob >> r alice))
86+ $ return (source [alice, bob, alice])
85− :<|> return (StreamGenerator (\f r -> f alice >> r bob >> r alice))
87+ :<|> return (source [alice, bob, 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
100
88
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)))
101
99
100+powerOfTwo :: Int -> Int
101+powerOfTwo = (2 ^)
102
102
103{-# NOINLINE manager' #-}
103{-# NOINLINE manager' #-}
104manager' :: C.Manager
104manager' :: C.Manager
@@ -107,41 +107,69 @@ manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings
107runClient :: ClientM a -> BaseUrl -> IO (Either ServantError a)
107runClient :: ClientM a -> BaseUrl -> IO (Either ServantError a)
108runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')
108runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')
109
109
110−testRunResultStream :: ResultStream a
110+testRunSourceIO :: Codensity IO (SourceIO a)
111− -> IO ( Maybe (Either String a)
111+ -> IO (Either String [a])
112− , Maybe (Either String a)
112+testRunSourceIO = runExceptT . runSourceT . joinCodensitySourceT
113− , Maybe (Either String a)
113+
114− , Maybe (Either String a))
114+joinCodensitySourceT :: Codensity m (SourceT m a) -> SourceT m a
115−testRunResultStream (ResultStream k)
115+joinCodensitySourceT cod =
116− = k $ \act -> (,,,) <$> act <*> act <*> act <*> act
116+ SourceT $ \r ->
117+ runCodensity cod $ \src ->
118+ unSourceT src r
117
119
118streamSpec :: Spec
120streamSpec :: Spec
119streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do
121streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do
120−
121 it "works with Servant.API.StreamGet.Newline" $ \(_, baseUrl) -> do
122 it "works with Servant.API.StreamGet.Newline" $ \(_, baseUrl) -> do
122− Right res <- runClient getGetNL baseUrl
123+ Right res <- runClient getGetNL baseUrl
123− let jra = Just (Right alice)
124+ testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
124− jrb = Just (Right bob)
125− testRunResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
126
125
127 it "works with Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do
126 it "works with Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do
128− Right res <- runClient getGetNS baseUrl
127+ Right res <- runClient getGetNS baseUrl
129− let jra = Just (Right alice)
128+ testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
130− jrb = Just (Right bob)
131− testRunResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
132
129
133{-
130{-
134 it "streams in constant memory" $ \(_, baseUrl) -> do
131 it "streams in constant memory" $ \(_, baseUrl) -> do
135− Right (ResultStream res) <- runClient getGetALot baseUrl
132+ Right rs <- runClient getGetALot baseUrl
136− let consumeNChunks n = replicateM_ n (res void)
133+ performGC
137− consumeNChunks 900
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 .
138#if MIN_VERSION_base(4,10,0)
166#if MIN_VERSION_base(4,10,0)
139− memUsed <- gcdetails_mem_in_use_bytes . gc <$> getRTSStats
167+ gcdetails_live_bytes . gc <$> getRTSStats
140#else
168#else
141− memUsed <- currentBytesUsed <$> getGCStats
169+ currentBytesUsed <$> getGCStats
142#endif
170#endif
143 memUsed `shouldSatisfy` (< megabytes 22)
171 memUsed `shouldSatisfy` (< megabytes 22)
144−-}
145
172
146megabytes :: Num a => a -> a
173megabytes :: Num a => a -> a
147megabytes n = n * (1000 ^ (2 :: Int))
174megabytes 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−3
@@ -853,9 +853,10 @@ instance {-# OVERLAPPABLE #-}
853-- | TODO: mention the endpoint is streaming, its framing strategy
853-- | TODO: mention the endpoint is streaming, its framing strategy
854--
854--
855-- Also there are no samples.
855-- Also there are no samples.
856+--
857+-- TODO: AcceptFraming for content-type
856instance {-# OVERLAPPABLE #-}
858instance {-# OVERLAPPABLE #-}
857− (MimeRender ct a, KnownNat status
858− , ReflectMethod method)
859+ (Accept ct, KnownNat status, ReflectMethod method)
859 => HasDocs (Stream method status framing ct a) where
860 => HasDocs (Stream method status framing ct a) where
860 docsFor Proxy (endpoint, action) DocOptions{..} =
861 docsFor Proxy (endpoint, action) DocOptions{..} =
861 single endpoint' action'
862 single endpoint' action'
@@ -866,7 +867,6 @@ instance {-# OVERLAPPABLE #-}
866 t = Proxy :: Proxy '[ct]
867 t = Proxy :: Proxy '[ct]
867 method' = reflectMethod (Proxy :: Proxy method)
868 method' = reflectMethod (Proxy :: Proxy method)
868 status = fromInteger $ natVal (Proxy :: Proxy status)
869 status = fromInteger $ natVal (Proxy :: Proxy status)
869− p = Proxy :: Proxy a
870
870
871instance {-# OVERLAPPING #-}
871instance {-# OVERLAPPING #-}
872 (ToSample a, AllMimeRender (ct ': cts) a, KnownNat status
872 (ToSample a, AllMimeRender (ct ': cts) a, KnownNat status
@@ -969,6 +969,9 @@ instance (ToSample a, AllMimeRender (ct ': cts) a, HasDocs api)
969 t = Proxy :: Proxy (ct ': cts)
969 t = Proxy :: Proxy (ct ': cts)
970 p = Proxy :: Proxy a
970 p = Proxy :: Proxy a
971
971
972+instance HasDocs api => HasDocs (StreamBody framing ctype a :> api) where
973+ docsFor Proxy _ _ = error "HasDocs @StreamBody"
974+
972instance (KnownSymbol path, HasDocs api) => HasDocs (path :> api) where
975instance (KnownSymbol path, HasDocs api) => HasDocs (path :> api) where
973
976
974 docsFor Proxy (endpoint, action) =
977 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
322 foreignFor lang ftype (Proxy :: Proxy api) $
322 foreignFor lang ftype (Proxy :: Proxy api) $
323 req & reqBody .~ (Just $ typeFor lang ftype (Proxy :: Proxy a))
323 req & reqBody .~ (Just $ typeFor lang ftype (Proxy :: Proxy a))
324
324
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+
325instance (KnownSymbol path, HasForeign lang ftype api)
333instance (KnownSymbol path, HasForeign lang ftype api)
326 => HasForeign lang ftype (path :> api) where
334 => HasForeign lang ftype (path :> api) where
327 type Foreign ftype (path :> api) = Foreign ftype api
335 type Foreign ftype (path :> api) = Foreign ftype api
servant-foreign/test/Servant/ForeignSpec.hs+5
@@ -16,6 +16,8 @@ import Data.Monoid
16import Data.Proxy
16import Data.Proxy
17import Servant.API.Internal.Test.ComprehensiveAPI
17import Servant.API.Internal.Test.ComprehensiveAPI
18import Servant.Foreign
18import Servant.Foreign
19+import Servant.Types.SourceT
20+ (SourceT)
19
21
20import Test.Hspec
22import Test.Hspec
21
23
@@ -51,6 +53,9 @@ instance HasForeignType LangX String (Headers ctyps NoContent) where
51instance HasForeignType LangX String Int where
53instance HasForeignType LangX String Int where
52 typeFor _ _ _ = "intX"
54 typeFor _ _ _ = "intX"
53
55
56+instance HasForeignType LangX String (SourceT m a) where
57+ typeFor _ _ _ = "streamTX"
58+
54instance HasForeignType LangX String Bool where
59instance HasForeignType LangX String Bool where
55 typeFor _ _ _ = "boolX"
60 typeFor _ _ _ = "boolX"
56
61
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
61 (SockAddr)
61 (SockAddr)
62import Network.Wai
62import Network.Wai
63 (Application, Request, httpVersion, isSecure, lazyRequestBody,
63 (Application, Request, httpVersion, isSecure, lazyRequestBody,
64− rawQueryString, remoteHost, requestHeaders, requestMethod,
64+ rawQueryString, remoteHost, requestBody, requestHeaders,
65− responseLBS, responseStream, vault)
65+ requestMethod, responseLBS, responseStream, vault)
66import Prelude ()
66import Prelude ()
67import Prelude.Compat
67import Prelude.Compat
68import Servant.API
68import Servant.API
69− ((:<|>) (..), (:>), Accept (..), BasicAuth,
69+ ((:<|>) (..), (:>), Accept (..), BasicAuth, Capture',
70− BoundaryStrategy (..), Capture', CaptureAll, Description,
71− EmptyAPI, FramingRender (..), Header', If, IsSecure (..),
70+ CaptureAll, Description, EmptyAPI, FramingRender (..),
71+ FramingUnrender (..), FromSourceIO (..), Header', If,
72− QueryFlag, QueryParam', QueryParams, Raw,
72+ IsSecure (..), QueryFlag, QueryParam', QueryParams, Raw,
73 ReflectMethod (reflectMethod), RemoteHost, ReqBody',
73 ReflectMethod (reflectMethod), RemoteHost, ReqBody',
74− SBool (..), SBoolI (..), Stream, StreamGenerator (..),
74+ SBool (..), SBoolI (..), SourceIO, Stream, StreamBody,
75− Summary, ToStreamGenerator (..), Vault, Verb,
76− WithNamedContext)
75+ Summary, ToSourceIO (..), Vault, Verb, WithNamedContext)
77import Servant.API.ContentTypes
76import Servant.API.ContentTypes
78 (AcceptHeader (..), AllCTRender (..), AllCTUnrender (..),
77 (AcceptHeader (..), AllCTRender (..), AllCTUnrender (..),
79− AllMime, MimeRender (..), canHandleAcceptH)
78+ AllMime, MimeRender (..), MimeUnrender (..), canHandleAcceptH)
80import Servant.API.Modifiers
79import Servant.API.Modifiers
81 (FoldLenient, FoldRequired, RequestArgument,
80 (FoldLenient, FoldRequired, RequestArgument,
82 unfoldRequestArgument)
81 unfoldRequestArgument)
83import Servant.API.ResponseHeaders
82import Servant.API.ResponseHeaders
84 (GetHeaders, Headers, getHeaders, getResponse)
83 (GetHeaders, Headers, getHeaders, getResponse)
84+import qualified Servant.Types.SourceT as S
85import Web.HttpApiData
85import Web.HttpApiData
86 (FromHttpApiData, parseHeader, parseQueryParam,
86 (FromHttpApiData, parseHeader, parseQueryParam,
87 parseUrlPieceMaybe, parseUrlPieces)
87 parseUrlPieceMaybe, parseUrlPieces)
@@ -279,24 +279,25 @@ instance {-# OVERLAPPING #-}
279
279
280
280
281instance {-# OVERLAPPABLE #-}
281instance {-# OVERLAPPABLE #-}
282− ( MimeRender ctype a, ReflectMethod method, KnownNat status,
282+ ( MimeRender ctype chunk, ReflectMethod method, KnownNat status,
283− FramingRender framing ctype, ToStreamGenerator b a
283+ FramingRender framing, ToSourceIO chunk a
284− ) => HasServer (Stream method status framing ctype b) context where
284+ ) => HasServer (Stream method status framing ctype a) context where
285
285
286− type ServerT (Stream method status framing ctype b) m = m b
286+ type ServerT (Stream method status framing ctype a) m = m a
287 hoistServerWithContext _ _ nt s = nt s
287 hoistServerWithContext _ _ nt s = nt s
288
288
289 route Proxy _ = streamRouter ([],) method status (Proxy :: Proxy framing) (Proxy :: Proxy ctype)
289 route Proxy _ = streamRouter ([],) method status (Proxy :: Proxy framing) (Proxy :: Proxy ctype)
290 where method = reflectMethod (Proxy :: Proxy method)
290 where method = reflectMethod (Proxy :: Proxy method)
291 status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
291 status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
292
292
293+
293instance {-# OVERLAPPING #-}
294instance {-# OVERLAPPING #-}
294− ( MimeRender ctype a, ReflectMethod method, KnownNat status,
295+ ( MimeRender ctype chunk, ReflectMethod method, KnownNat status,
295− FramingRender framing ctype, ToStreamGenerator b a,
296+ FramingRender framing, ToSourceIO chunk a,
296− GetHeaders (Headers h b)
297+ GetHeaders (Headers h a)
297− ) => HasServer (Stream method status framing ctype (Headers h b)) context where
298+ ) => HasServer (Stream method status framing ctype (Headers h a)) context where
298
299
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)
300 hoistServerWithContext _ _ nt s = nt s
301 hoistServerWithContext _ _ nt s = nt s
301
302
302 route Proxy _ = streamRouter (\x -> (getHeaders x, getResponse x)) method status (Proxy :: Proxy framing) (Proxy :: Proxy ctype)
303 route Proxy _ = streamRouter (\x -> (getHeaders x, getResponse x)) method status (Proxy :: Proxy framing) (Proxy :: Proxy ctype)
@@ -304,8 +305,8 @@ instance {-# OVERLAPPING #-}
304 status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
305 status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
305
306
306
307
307−streamRouter :: (MimeRender ctype a, FramingRender framing ctype, ToStreamGenerator b a) =>
308+streamRouter :: forall ctype a c chunk env framing. (MimeRender ctype chunk, FramingRender framing, ToSourceIO chunk a) =>
308− (c -> ([(HeaderName, B.ByteString)], b))
309+ (c -> ([(HeaderName, B.ByteString)], a))
309 -> Method
310 -> Method
310 -> Status
311 -> Status
311 -> Proxy framing
312 -> Proxy framing
@@ -321,28 +322,19 @@ streamRouter splitHeaders method status framingproxy ctypeproxy action = leafRou
321 `addAcceptCheck` accCheck
322 `addAcceptCheck` accCheck
322 ) env request respond $ \ output ->
323 ) env request respond $ \ output ->
323 let (headers, fa) = splitHeaders output
324 let (headers, fa) = splitHeaders output
324− k = getStreamGenerator . toStreamGenerator $ fa in
325+ sourceT = toSourceIO fa
326+ S.SourceT kStepLBS = framingRender framingproxy (mimeRender ctypeproxy :: chunk -> BL.ByteString) sourceT
325− Route $ responseStream status (contentHeader : headers) $ \write flush -> do
327+ in Route $ responseStream status (contentHeader : headers) $ \write flush -> do
326− write . BB.lazyByteString $ header framingproxy ctypeproxy
328+ let loop S.Stop = flush
327− case boundary framingproxy ctypeproxy of
329+ loop (S.Error err) = fail err -- TODO: throw better error
328− BoundaryStrategyBracket f ->
330+ loop (S.Skip s) = loop s
329− let go x = let bs = mimeRender ctypeproxy x
331+ loop (S.Effect ms) = ms >>= loop
330− (before, after) = f bs
332+ loop (S.Yield lbs s) = do
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
333+ write (BB.lazyByteString lbs)
341− flush)
334+ flush
342− BoundaryStrategyGeneral f ->
335+ loop s
343− let go = (>> flush) . write . BB.lazyByteString . f . mimeRender ctypeproxy
336+
344− in k go go
337+ kStepLBS loop
345− write . BB.lazyByteString $ trailer framingproxy ctypeproxy
346
338
347-- | If you use 'Header' in one of the endpoints for your API,
339-- | If you use 'Header' in one of the endpoints for your API,
348-- this automatically requires your server-side handler to be a function
340-- 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
615 Left e -> delayedFailFatal err400 { errBody = cs e }
607 Left e -> delayedFailFatal err400 { errBody = cs e }
616 Right v -> return v
608 Right v -> return v
617
609
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+
618-- | Make sure the incoming request starts with @"/path"@, strip it and
635-- | Make sure the incoming request starts with @"/path"@, strip it and
619-- pass the rest of the request path to @api@.
636-- pass the rest of the request path to @api@.
620instance (KnownSymbol path, HasServer api context) => HasServer (path :> api) context where
637instance (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
42import Network.Wai.Test
42import Network.Wai.Test
43 (defaultRequest, request, runSession, simpleBody,
43 (defaultRequest, request, runSession, simpleBody,
44 simpleHeaders, simpleStatus)
44 simpleHeaders, simpleStatus)
45+import qualified Servant.Types.SourceT as S
45import Servant.API
46import Servant.API
46 ((:<|>) (..), (:>), AuthProtect, BasicAuth,
47 ((:<|>) (..), (:>), AuthProtect, BasicAuth,
47 BasicAuthData (BasicAuthData), Capture, CaptureAll, Delete,
48 BasicAuthData (BasicAuthData), Capture, CaptureAll, Delete,
@@ -49,7 +50,7 @@ import Servant.API
49 JSON, NoContent (..), NoFraming, OctetStream, Patch,
50 JSON, NoContent (..), NoFraming, OctetStream, Patch,
50 PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw,
51 PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw,
51 RemoteHost, ReqBody, StdMethod (..), Stream,
52 RemoteHost, ReqBody, StdMethod (..), Stream,
52− StreamGenerator (..), Verb, addHeader)
53+ SourceIO, Verb, addHeader)
53import Servant.API.Internal.Test.ComprehensiveAPI
54import Servant.API.Internal.Test.ComprehensiveAPI
54import Servant.Server
55import Servant.Server
55 (Context ((:.), EmptyContext), Handler, Server, Tagged (..),
56 (Context ((:.), EmptyContext), Handler, Server, Tagged (..),
@@ -106,7 +107,7 @@ type VerbApi method status
106 :<|> "accept" :> ( Verb method status '[JSON] Person
107 :<|> "accept" :> ( Verb method status '[JSON] Person
107 :<|> Verb method status '[PlainText] String
108 :<|> Verb method status '[PlainText] String
108 )
109 )
109− :<|> "stream" :> Stream method status NoFraming OctetStream (StreamGenerator BS.ByteString)
110+ :<|> "stream" :> Stream method status NoFraming OctetStream (SourceIO BS.ByteString)
110
111
111verbSpec :: Spec
112verbSpec :: Spec
112verbSpec = describe "Servant.API.Verb" $ do
113verbSpec = describe "Servant.API.Verb" $ do
@@ -116,7 +117,7 @@ verbSpec = describe "Servant.API.Verb" $ do
116 :<|> return (addHeader 5 alice)
117 :<|> return (addHeader 5 alice)
117 :<|> return (addHeader 10 NoContent)
118 :<|> return (addHeader 10 NoContent)
118 :<|> (return alice :<|> return "B")
119 :<|> (return alice :<|> return "B")
119− :<|> return (StreamGenerator $ \f _ -> f "bytestring")
120+ :<|> return (S.source ["bytestring"])
120
121
121 get200 = Proxy :: Proxy (VerbApi 'GET 200)
122 get200 = Proxy :: Proxy (VerbApi 'GET 200)
122 post210 = Proxy :: Proxy (VerbApi 'POST 210)
123 post210 = Proxy :: Proxy (VerbApi 'POST 210)
servant/servant.cabal+15−4
@@ -24,6 +24,7 @@ tested-with:
24 GHC==8.6.1
24 GHC==8.6.1
25extra-source-files:
25extra-source-files:
26 CHANGELOG.md
26 CHANGELOG.md
27+
27source-repository head
28source-repository head
28 type: git
29 type: git
29 location: http://github.com/haskell-servant/servant.git
30 location: http://github.com/haskell-servant/servant.git
@@ -61,6 +62,13 @@ library
61 Servant.API.Vault
62 Servant.API.Vault
62 Servant.API.Verbs
63 Servant.API.Verbs
63 Servant.API.WithNamedContext
64 Servant.API.WithNamedContext
65+
66+ -- Types
67+ exposed-modules:
68+ Servant.Types.SourceT
69+
70+ -- Safe links
71+ exposed-modules:
64 Servant.Links
72 Servant.Links
65
73
66 -- Deprecated modules, to be removed in late 2019
74 -- Deprecated modules, to be removed in late 2019
@@ -75,6 +83,7 @@ library
75 base >= 4.9 && < 4.13
83 base >= 4.9 && < 4.13
76 , bytestring >= 0.10.8.1 && < 0.11
84 , bytestring >= 0.10.8.1 && < 0.11
77 , mtl >= 2.1 && < 2.3
85 , mtl >= 2.1 && < 2.3
86+ , transformers >= 0.3.0.0 && < 0.6
78 , text >= 1.2.3.0 && < 1.3
87 , text >= 1.2.3.0 && < 1.3
79
88
80 if !impl(ghc >= 8.0)
89 if !impl(ghc >= 8.0)
@@ -84,19 +93,19 @@ library
84 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
93 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
85 -- Here can be exceptions if we really need features from the newer versions.
94 -- Here can be exceptions if we really need features from the newer versions.
86 build-depends:
95 build-depends:
87− base-compat >= 0.10.4 && < 0.11
96+ base-compat >= 0.10.5 && < 0.11
88− , bifunctors >= 5.5.3 && < 5.6
89 , aeson >= 1.3.1.1 && < 1.5
97 , aeson >= 1.3.1.1 && < 1.5
90 , attoparsec >= 0.13.2.2 && < 0.14
98 , attoparsec >= 0.13.2.2 && < 0.14
99+ , bifunctors >= 5.5.3 && < 5.6
91 , case-insensitive >= 1.2.0.11 && < 1.3
100 , case-insensitive >= 1.2.0.11 && < 1.3
92 , http-api-data >= 0.3.8.1 && < 0.4
101 , http-api-data >= 0.3.8.1 && < 0.4
93 , http-media >= 0.7.1.2 && < 0.8
102 , http-media >= 0.7.1.2 && < 0.8
94 , http-types >= 0.12.1 && < 0.13
103 , http-types >= 0.12.1 && < 0.13
95 , mmorph >= 1.1.2 && < 1.2
104 , mmorph >= 1.1.2 && < 1.2
96− , tagged >= 0.8.5 && < 0.9
105+ , network-uri >= 2.6.1.0 && < 2.7
97 , singleton-bool >= 0.1.4 && < 0.2
106 , singleton-bool >= 0.1.4 && < 0.2
98 , string-conversions >= 0.4.0.1 && < 0.5
107 , string-conversions >= 0.4.0.1 && < 0.5
99− , network-uri >= 2.6.1.0 && < 2.7
108+ , tagged >= 0.8.5 && < 0.9
100 , vault >= 0.3.1.1 && < 0.4
109 , vault >= 0.3.1.1 && < 0.4
101
110
102 hs-source-dirs: src
111 hs-source-dirs: src
@@ -132,6 +141,7 @@ test-suite spec
132 other-modules:
141 other-modules:
133 Servant.API.ContentTypesSpec
142 Servant.API.ContentTypesSpec
134 Servant.API.ResponseHeadersSpec
143 Servant.API.ResponseHeadersSpec
144+ Servant.API.StreamSpec
135 Servant.LinksSpec
145 Servant.LinksSpec
136
146
137 -- Dependencies inherited from the library. No need to specify bounds.
147 -- Dependencies inherited from the library. No need to specify bounds.
@@ -143,6 +153,7 @@ test-suite spec
143 , servant
153 , servant
144 , string-conversions
154 , string-conversions
145 , text
155 , text
156+ , transformers
146
157
147 -- Additonal dependencies
158 -- Additonal dependencies
148 build-depends:
159 build-depends:
servant/src/Servant/API.hs+4−5
@@ -114,11 +114,10 @@ import Servant.API.ResponseHeaders
114 ResponseHeader (..), addHeader, getHeadersHList, getResponse,
114 ResponseHeader (..), addHeader, getHeadersHList, getResponse,
115 noHeader)
115 noHeader)
116import Servant.API.Stream
116import Servant.API.Stream
117− (BoundaryStrategy (..), ByteStringParser (..),
118− FramingRender (..), FramingUnrender (..),
117+ (FramingRender (..), FramingUnrender (..),
119− FromResultStream (..), NetstringFraming, NewlineFraming,
118+ FromSourceIO (..), NetstringFraming, NewlineFraming,
120− NoFraming, ResultStream (..), Stream, StreamGenerator (..),
119+ NoFraming, SourceIO, Stream, StreamBody, SourceIO,
121− StreamGet, StreamPost, ToStreamGenerator (..))
120+ StreamGet, StreamPost, ToSourceIO (..))
122import Servant.API.Sub
121import Servant.API.Sub
123 ((:>))
122 ((:>))
124import Servant.API.Vault
123import Servant.API.Vault
servant/src/Servant/API/Internal/Test/ComprehensiveAPI.hs+3−1
@@ -9,6 +9,8 @@ module Servant.API.Internal.Test.ComprehensiveAPI where
9import Data.Proxy
9import Data.Proxy
10 (Proxy (..))
10 (Proxy (..))
11import Servant.API
11import Servant.API
12+import Servant.Types.SourceT
13+ (SourceT)
12
14
13type GET = Get '[JSON] NoContent
15type GET = Get '[JSON] NoContent
14
16
@@ -39,7 +41,7 @@ type ComprehensiveAPIWithoutRaw =
39 Vault :> GET :<|>
41 Vault :> GET :<|>
40 Verb 'POST 204 '[JSON] NoContent :<|>
42 Verb 'POST 204 '[JSON] NoContent :<|>
41 Verb 'POST 204 '[JSON] Int :<|>
43 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) :<|>
43 WithNamedContext "foo" '[] GET :<|>
45 WithNamedContext "foo" '[] GET :<|>
44 CaptureAll "foo" Int :> GET :<|>
46 CaptureAll "foo" Int :> GET :<|>
45 Summary "foo" :> GET :<|>
47 Summary "foo" :> GET :<|>
servant/src/Servant/API/Stream.hs+201−171
@@ -1,6 +1,7 @@
1{-# LANGUAGE DataKinds #-}
1{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE DeriveDataTypeable #-}
2{-# LANGUAGE DeriveDataTypeable #-}
3{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE DeriveGeneric #-}
4+{-# LANGUAGE FlexibleContexts #-}
4{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE FunctionalDependencies #-}
6{-# LANGUAGE FunctionalDependencies #-}
6{-# LANGUAGE KindSignatures #-}
7{-# LANGUAGE KindSignatures #-}
@@ -8,39 +9,41 @@
8{-# LANGUAGE OverloadedStrings #-}
9{-# LANGUAGE OverloadedStrings #-}
9{-# LANGUAGE PolyKinds #-}
10{-# LANGUAGE PolyKinds #-}
10{-# LANGUAGE RankNTypes #-}
11{-# LANGUAGE RankNTypes #-}
11−{-# LANGUAGE TupleSections #-}
12+{-# LANGUAGE ScopedTypeVariables #-}
12−{-# OPTIONS_HADDOCK not-home #-}
13+{-# OPTIONS_HADDOCK not-home #-}
13
14
14module Servant.API.Stream (
15module Servant.API.Stream (
15− Stream,
16+ Stream,
16− StreamGet,
17+ StreamGet,
17− StreamPost,
18+ StreamPost,
18− -- * Sources
19+ StreamBody,
20+ -- * Source
19− --
21+ --
20− -- | Both 'StreamGenerator' and 'ResultStream' are equivalent
21− -- to some *source* in streaming libraries.
22+ -- | 'SourceIO' are equivalent to some *source* in streaming libraries.
22− StreamGenerator (..),
23+ SourceIO,
23− ToStreamGenerator (..),
24+ ToSourceIO (..),
24− ResultStream (..),
25+ FromSourceIO (..),
25− FromResultStream (..),
26+ -- ** Auxiliary classes
27+ SourceToSourceIO (..),
26− -- * Framing
28+ -- * Framing
27− FramingRender (..),
29+ FramingRender (..),
28− FramingUnrender (..),
30+ FramingUnrender (..),
29− BoundaryStrategy (..),
30− ByteStringParser (..),
31− -- ** Strategies
31+ -- ** Strategies
32− NoFraming,
32+ NoFraming,
33− NewlineFraming,
33+ NewlineFraming,
34− NetstringFraming,
34+ NetstringFraming,
35− ) where
35+ ) where
36−
36+
37+
37−import Control.Arrow
38+import Control.Applicative
38− (first)
39+ ((<|>))
39−import Data.ByteString.Lazy
40+import Control.Monad.IO.Class
40− (ByteString, empty)
41+ (MonadIO (..))
41−import qualified Data.ByteString.Lazy.Char8 as LB
42+import qualified Data.Attoparsec.ByteString as A
42−import Data.Foldable
43+import qualified Data.Attoparsec.ByteString.Char8 as A8
43− (traverse_)
44+import qualified Data.ByteString as BS
45+import qualified Data.ByteString.Lazy as LBS
46+import qualified Data.ByteString.Lazy.Char8 as LBS8
44import Data.List.NonEmpty
47import Data.List.NonEmpty
45 (NonEmpty (..))
48 (NonEmpty (..))
46import Data.Monoid
49import Data.Monoid
@@ -55,158 +58,185 @@ import GHC.TypeLits
55 (Nat)
58 (Nat)
56import Network.HTTP.Types.Method
59import Network.HTTP.Types.Method
57 (StdMethod (..))
60 (StdMethod (..))
58−import System.IO.Unsafe
61+import Servant.Types.SourceT
59− (unsafeInterleaveIO)
60−import Text.Read
61− (readMaybe)
62
62
63-- | A Stream endpoint for a given method emits a stream of encoded values at a
63-- | 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
64+-- given @Content-Type@, delimited by a @framing@ strategy.
65−-- return response code 200 on success. Type synonyms are provided for standard
65+-- Type synonyms are provided for standard methods.
66−-- methods.
66+--
67data Stream (method :: k1) (status :: Nat) (framing :: *) (contentType :: *) (a :: *)
67data Stream (method :: k1) (status :: Nat) (framing :: *) (contentType :: *) (a :: *)
68 deriving (Typeable, Generic)
68 deriving (Typeable, Generic)
69
69
70type StreamGet = Stream 'GET 200
70type StreamGet = Stream 'GET 200
71type StreamPost = Stream 'POST 200
71type StreamPost = Stream 'POST 200
72
72
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+
73−-- | Stream endpoints may be implemented as producing a @StreamGenerator@ a
83+-- | Stream endpoints may be implemented as producing a @'SourceIO' chunk@.
74−-- function that itself takes two emit functions the first to be used on the
84+--
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
85+-- Clients reading from streaming endpoints can be implemented as consuming a
94−-- @ResultStream@ that captures the setup, takedown, and incremental logic for
86+-- @'SourceIO' chunk@.
95−-- a read, being an IO continuation that takes a producer of Just either values
87+--
96−-- or errors that terminates with a Nothing.
88+type SourceIO = SourceT IO
97−newtype ResultStream a = ResultStream { runResultStream :: forall b. (IO (Maybe (Either String a)) -> IO b) -> IO b }
89+
98−
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,
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.
114+-- Pipe, etc. By implementing this class, all such streaming abstractions can
100−class FromResultStream a b | b -> a where
115+-- be used directly on the client side for talking to streaming endpoints.
101− fromResultStream :: ResultStream a -> IO b
116+class FromSourceIO chunk a | a -> chunk where
102−
117+ fromSourceIO :: SourceIO chunk -> a
103−instance FromResultStream a (ResultStream a) where
118+
104− fromResultStream = return
119+instance MonadIO m => FromSourceIO a (SourceT m a) where
105−
120+ fromSourceIO = sourceFromSourceIO
106−-- | Uses 'unsafeInterleaveIO'
121+
107−instance FromResultStream a [a] where
122+sourceFromSourceIO :: forall m a. MonadIO m => SourceT IO a -> SourceT m a
108− fromResultStream x = runResultStream x lazyRead
123+sourceFromSourceIO src =
109−
124+ SourceT $ \k ->
110−-- | Uses 'unsafeInterleaveIO'
125+ k $ Effect $ liftIO $ unSourceT src (return . go)
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
123 where
126 where
124− go = unsafeInterleaveIO loop
127+ go :: StepT IO a -> StepT m a
125−
128+ go Stop = Stop
126− loop = do
129+ go (Error err) = Error err
127− e <- r
130+ go (Skip s) = Skip (go s)
128− case e of
131+ go (Effect ms) = Effect (liftIO (fmap go ms))
129− Nothing -> return []
132+ go (Yield x s) = Yield x (go s)
130− Just (Left er) -> fail er
133+
131− Just (Right y) -> do
134+-- This fires e.g. in Client.lhs
132− ys <- go
135+-- {-# OPTIONS_GHC -ddump-simpl -ddump-rule-firings -ddump-to-file #-}
133− return (y : ys)
136+{-# NOINLINE [2] sourceFromSourceIO #-}
134−
137+{-# RULES "sourceFromSourceIO @IO" sourceFromSourceIO = id :: SourceT IO a -> SourceT IO a #-}
138+
139+-------------------------------------------------------------------------------
140+-- Framing
141+-------------------------------------------------------------------------------
142+
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.
143+-- | The 'FramingRender' class provides the logic for emitting a framing strategy.
136−class FramingRender strategy a where
144+-- The strategy transforms a @'SourceT' m a@ into @'SourceT' m 'LBS.ByteString'@,
137− header :: Proxy strategy -> Proxy a -> ByteString
145+-- therefore it can prepend, append and intercalate /framing/ structure
138− boundary :: Proxy strategy -> Proxy a -> BoundaryStrategy
146+-- around chunks.
139− trailer :: Proxy strategy -> Proxy a -> ByteString
147+--
140−
148+-- /Note:/ as the @'Monad' m@ is generic, this is pure transformation.
141−-- | The bracketing strategy generates things to precede and follow the content, as with netstrings.
149+--
142−-- The intersperse strategy inserts seperators between things, as with newline framing.
150+class FramingRender strategy where
143−-- Finally, the general strategy performs an arbitrary rewrite on the content, to allow escaping rules and such.
151+ framingRender :: Monad m => Proxy strategy -> (a -> LBS.ByteString) -> SourceT m a -> SourceT m LBS.ByteString
144−data BoundaryStrategy = BoundaryStrategyBracket (ByteString -> (ByteString,ByteString))
152+
145− | BoundaryStrategyIntersperse ByteString
153+-- | The 'FramingUnrender' class provides the logic for parsing a framing
146− | BoundaryStrategyGeneral (ByteString -> ByteString)
154+-- strategy.
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
155+class FramingUnrender strategy where
156− unrenderFrames :: Proxy strategy -> Proxy a -> ByteStringParser (ByteStringParser (Either String ByteString))
156+ framingUnrender :: Monad m => Proxy strategy -> (LBS.ByteString -> Either String a) -> SourceT m BS.ByteString -> SourceT m a
157−
157+
158+-------------------------------------------------------------------------------
159+-- NoFraming
160+-------------------------------------------------------------------------------
161+
158−-- | A framing strategy that does not do any framing at all, it just passes the input data
162+-- | A framing strategy that does not do any framing at all, it just passes the
159−-- This will be used most of the time with binary data, such as files
163+-- input data This will be used most of the time with binary data, such as
164+-- files
160data NoFraming
165data NoFraming
161
166
162−instance FramingRender NoFraming a where
167+instance FramingRender NoFraming where
163− header _ _ = empty
168+ framingRender _ = fmap
164− boundary _ _ = BoundaryStrategyGeneral id
169+
165− trailer _ _ = empty
170+-- | As 'NoFraming' doesn't have frame separators, we take the chunks
166−
171+-- as given and try to convert them one by one.
172+--
173+-- That works well when @a@ is a 'ByteString'.
167−instance FramingUnrender NoFraming a where
174+instance FramingUnrender NoFraming where
168− unrenderFrames _ _ = ByteStringParser (Just . (go,)) (go,)
175+ framingUnrender _ f = mapStepT go
169− where go = ByteStringParser (Just . (, empty) . Right) ((, empty) . Right)
176+ where
170−
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+
171−-- | A simple framing strategy that has no header or termination, and inserts a newline character between each frame.
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
172−-- This assumes that it is used with a Content-Type that encodes without newlines (e.g. JSON).
191+-- Content-Type that encodes without newlines (e.g. JSON).
173data NewlineFraming
192data NewlineFraming
174
193
175−instance FramingRender NewlineFraming a where
194+instance FramingRender NewlineFraming where
176− header _ _ = empty
195+ framingRender _ f = mapStepT go0 where
177− boundary _ _ = BoundaryStrategyIntersperse "\n"
196+ go0 Stop = Stop
178− trailer _ _ = empty
197+ go0 (Error err) = Error err
179−
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+
180−instance FramingUnrender NewlineFraming a where
204+instance FramingUnrender NewlineFraming where
181− unrenderFrames _ _ = ByteStringParser (Just . (go,)) (go,)
205+ framingUnrender _ f = transformWithAtto $ do
182− where go = ByteStringParser
206+ bs <- A.takeWhile (/= 10)
183− (\x -> case LB.break (== '\n') x of
207+ () <$ A.word8 10 <|> A.endOfInput
184− (h,r) -> if not (LB.null r) then Just (Right h, LB.drop 1 r) else Nothing
208+ either fail pure (f (LBS.fromStrict bs))
185− )
209+
186− (\x -> case LB.break (== '\n') x of
210+-------------------------------------------------------------------------------
187− (h,r) -> (Right h, LB.drop 1 r)
211+-- NetstringFraming
188− )
212+-------------------------------------------------------------------------------
213+
189−-- | The netstring framing strategy as defined by djb: <http://cr.yp.to/proto/netstrings.txt>
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+--
190data NetstringFraming
229data NetstringFraming
191
230
192−instance FramingRender NetstringFraming a where
231+instance FramingRender NetstringFraming where
193− header _ _ = empty
232+ framingRender _ f = fmap $ \x ->
194− boundary _ _ = BoundaryStrategyBracket $ \b -> ((<> ":") . LB.pack . show . LB.length $ b, ",")
233+ let bs = f x
195− trailer _ _ = empty
234+ in LBS8.pack (show (LBS8.length bs)) <> ":" <> bs <> ","
196−
235+
197−
198−instance FramingUnrender NetstringFraming a where
236+instance FramingUnrender NetstringFraming where
199− unrenderFrames _ _ = ByteStringParser (Just . (go,)) (go,)
237+ framingUnrender _ f = transformWithAtto $ do
200− where go = ByteStringParser
238+ len <- A8.decimal
201− (\b -> let (i,r) = LB.break (==':') b
239+ _ <- A8.char ':'
202− in case readMaybe (LB.unpack i) of
240+ bs <- A.take len
203− Just len -> if LB.length r > len
241+ _ <- A8.char ','
204− then Just . first Right . fmap (LB.drop 1) $ LB.splitAt len . LB.drop 1 $ r
242+ either fail pure (f (LBS.fromStrict bs))
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))
servant/src/Servant/Links.hs+5−1
@@ -170,7 +170,7 @@ import Servant.API.RemoteHost
170import Servant.API.ReqBody
170import Servant.API.ReqBody
171 (ReqBody')
171 (ReqBody')
172import Servant.API.Stream
172import Servant.API.Stream
173− (Stream)
173+ (Stream, StreamBody)
174import Servant.API.Sub
174import Servant.API.Sub
175 (type (:>))
175 (type (:>))
176import Servant.API.TypeLevel
176import Servant.API.TypeLevel
@@ -482,6 +482,10 @@ instance HasLink sub => HasLink (ReqBody' mods ct a :> sub) where
482 type MkLink (ReqBody' mods ct a :> sub) r = MkLink sub r
482 type MkLink (ReqBody' mods ct a :> sub) r = MkLink sub r
483 toLink toA _ = toLink toA (Proxy :: Proxy sub)
483 toLink toA _ = toLink toA (Proxy :: Proxy sub)
484
484
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+
485instance (ToHttpApiData v, HasLink sub)
489instance (ToHttpApiData v, HasLink sub)
486 => HasLink (Capture' mods sym v :> sub)
490 => HasLink (Capture' mods sym v :> sub)
487 where
491 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:
8- servant-server/
8- servant-server/
9- servant/
9- servant/
10
10
11+- servant-conduit
12+- servant-machines/
13+- servant-pipes/
14+
11# allow-newer: true # ignores all bounds, that's a sledgehammer
15# allow-newer: true # ignores all bounds, that's a sledgehammer
12# - doc/tutorial/
16# - doc/tutorial/
13
17
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