From 7edd35c9f0c401f8cff5b9f1fb1a2c9bbcec3b90 Mon Sep 17 00:00:00 2001 From: gbaz Date: Mon, 11 Dec 2017 15:32:17 -0500 Subject: [PATCH] docs for streaming (#852) * docs for new streaming related combinators --- doc/tutorial/ApiType.lhs | 13 ++++++++++++ doc/tutorial/Client.lhs | 46 +++++++++++++++++++++++++++++++++++++++- doc/tutorial/Server.lhs | 27 ++++++++++++++++++++--- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/doc/tutorial/ApiType.lhs b/doc/tutorial/ApiType.lhs index f43b2231..6109c27b 100644 --- a/doc/tutorial/ApiType.lhs +++ b/doc/tutorial/ApiType.lhs @@ -127,6 +127,19 @@ type UserAPI4 = "users" :> Get '[JSON] [User] :<|> "admins" :> Get '[JSON] [User] ``` +### `StreamGet` and `StreamPost` + +The `StreamGet` and `StreamPost` combinators are defined in terms of the more general `Stream` + +``` haskell ignore +data Stream (method :: k1) (framing :: *) (contentType :: *) a +type StreamGet = Stream 'GET +type StreamPost = Stream 'POST +``` + +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 paremeter, but also a framing strategy -- this specifies how the individual results are deliniated from one another in the stream. The two standard strategies given with Servant are `NewlineFraming` and `NetstringFraming`, but others can be written to match other protocols. + + ### `Capture` URL captures are segments of the path of a URL that are variable and whose actual value is diff --git a/doc/tutorial/Client.lhs b/doc/tutorial/Client.lhs index b2279849..34891ed5 100644 --- a/doc/tutorial/Client.lhs +++ b/doc/tutorial/Client.lhs @@ -153,4 +153,48 @@ HelloMessage {msg = "Hello, servant"} Email {from = "great@company.com", to = "alp@foo.com", subject = "Hey Alp, we miss you!", body = "Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!"} ``` -The types of the arguments for the functions are the same as for (server-side) request handlers. You now know how to use **servant-client**! +The types of the arguments for the functions are the same as for (server-side) request handlers. + +## Querying Streaming APIs. + +Consider the following streaming API type: + +``` haskell +type StreamAPI = "positionStream" :> StreamGet NewlineFraming JSON (ResultStream Position) +``` + +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: + +``` haskell ignore +type StreamAPI f = "positionStream" :> StreamGet NewlineFraming JSON (f Position) +type ServerStreamAPI = StreamAPI StreamGenerator +type ClientStreamAPI = StreamAPI ResultStream +``` + +In any case, here's how we write a function to query our API: + +``` haskell +streamAPI :: Proxy StreamAPI +streamAPI = Proxy + +posStream :: ClientM (ResultStream Position) + +posStream = client streamAPI +``` + +And here's how to just print out all elements from a `ResultStream`, to give some idea of how to work with them. + +``` haskell +printResultStream :: Show a => ResultStream a -> IO () +printResultStream (ResultStream k) = k $ \getResult -> + let loop = do + r <- getResult + case r of + Nothing -> return () + Just x -> print x >> loop + in loop +``` + +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. + +You now know how to use **servant-client**! diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index 5bd51534..327ab29c 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -204,7 +204,7 @@ And that's it! You can run this example in the same way that we showed for Fine, we can write trivial webservices easily, but none of the two above use any "fancy" combinator from servant. Let's address this and use `QueryParam`, -`Capture` and `ReqBody` right away. You'll see how each occurence of these +`Capture` and `ReqBody` right away. You'll see how each occurrence of these combinators in an endpoint makes the corresponding handler receive an argument of the appropriate type automatically. You don't have to worry about manually looking up URL captures or query string parameters, or @@ -1092,7 +1092,7 @@ We can write some simple webservice with the handlers running in `Reader String` ``` haskell type ReaderAPI = "a" :> Get '[JSON] Int - :<|> "b" :> ReqBody '[JSON] Double :> Get '[JSON] Bool + :<|> "b" :> ReqBody '[JSON] Double :> Get '[JSON] Bool readerAPI :: Proxy ReaderAPI readerAPI = Proxy @@ -1114,7 +1114,7 @@ We unfortunately can't use `readerServerT` as an argument of `serve`, because That's right. We have just written `readerToHandler`, which is exactly what we would need to apply to all handlers to make the handlers have the right type for `serve`. Being cumbersome to do by hand, we provide a function -`hoistServer` which takes a natural transformation between two parametrized types `m` +`hoistServer` which takes a natural transformation between two parameterized types `m` and `n` and a `ServerT someapi m`, and returns a `ServerT someapi n`. In our case, we can wrap up our little webservice by using @@ -1164,6 +1164,27 @@ app5 :: Application app5 = serve readerAPI (hoistServer readerAPI funToHandler funServerT) ``` +## Streaming endpoints + +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. + +``` haskell +type StreamAPI = "userStream" :> StreamGet NewlineFraming JSON (StreamGenerator User) +streamAPI :: Proxy StreamAPI +streamAPI = Proxy + +streamUsers :: StreamGenerator User +streamUsers = StreamGenerator $ \sendFirst sendRest -> do + sendFirst isaac + sendRest albert + sendRest albert + +app6 :: Application +app6 = serve streamAPI (return streamUsers) +``` + +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. + ## Conclusion You're now equipped to write webservices/web-applications using