tutorial: read through Server.lhs
This commit is contained in:
parent
e68cf28750
commit
e84fea334a
3 changed files with 109 additions and 221 deletions
|
@ -228,7 +228,7 @@ server = Server.server3 :<|> serveDocs
|
||||||
plain = ("Content-Type", "text/plain")
|
plain = ("Content-Type", "text/plain")
|
||||||
|
|
||||||
app :: Application
|
app :: Application
|
||||||
app = serve api EmptyConfig server
|
app = serve api server
|
||||||
```
|
```
|
||||||
|
|
||||||
And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type.
|
And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type.
|
||||||
|
|
|
@ -134,7 +134,7 @@ server' = server
|
||||||
:<|> serveDirectory "tutorial/t9"
|
:<|> serveDirectory "tutorial/t9"
|
||||||
|
|
||||||
app :: Application
|
app :: Application
|
||||||
app = serve api' EmptyConfig server'
|
app = serve api' server'
|
||||||
```
|
```
|
||||||
|
|
||||||
Why two different API types, proxies and servers though? Simply because we don't want to generate javascript functions for the `Raw` part of our API type, so we need a `Proxy` for our API type `API'` without its `Raw` endpoint.
|
Why two different API types, proxies and servers though? Simply because we don't want to generate javascript functions for the `Raw` part of our API type, so we need a `Proxy` for our API type `API'` without its `Raw` endpoint.
|
||||||
|
|
|
@ -5,7 +5,7 @@ type. Can we have a webservice already?
|
||||||
|
|
||||||
## A first example
|
## A first example
|
||||||
|
|
||||||
Equipped with some basic knowledge about the way we represent API, let's now
|
Equipped with some basic knowledge about the way we represent APIs, let's now
|
||||||
write our first webservice.
|
write our first webservice.
|
||||||
|
|
||||||
The source for this tutorial section is a literate haskell file, so first we
|
The source for this tutorial section is a literate haskell file, so first we
|
||||||
|
@ -26,7 +26,7 @@ module Server where
|
||||||
import Prelude ()
|
import Prelude ()
|
||||||
import Prelude.Compat
|
import Prelude.Compat
|
||||||
|
|
||||||
import Control.Monad.IO.Class
|
import Control.Monad.Except
|
||||||
import Control.Monad.Reader
|
import Control.Monad.Reader
|
||||||
import Control.Monad.Trans.Except
|
import Control.Monad.Trans.Except
|
||||||
import Data.Aeson.Compat
|
import Data.Aeson.Compat
|
||||||
|
@ -34,6 +34,7 @@ import Data.Aeson.Types
|
||||||
import Data.Attoparsec.ByteString
|
import Data.Attoparsec.ByteString
|
||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
import Data.List
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
import Data.String.Conversions
|
import Data.String.Conversions
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
|
@ -49,16 +50,12 @@ import qualified Data.Aeson.Parser
|
||||||
import qualified Text.Blaze.Html
|
import qualified Text.Blaze.Html
|
||||||
```
|
```
|
||||||
|
|
||||||
``` haskell ignore
|
**Important**: the `Servant` module comes from the **servant-server** package,
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: the `Servant` module comes from the *servant-server* package,
|
|
||||||
the one that lets us run webservers that implement a particular API type. It
|
the one that lets us run webservers that implement a particular API type. It
|
||||||
reexports all the types from the *servant* package that let you declare API
|
reexports all the types from the **servant** package that let you declare API
|
||||||
types as well as everything you need to turn your request handlers into a
|
types as well as everything you need to turn your request handlers into a
|
||||||
fully-fledged webserver. This means that in your applications, you can just add
|
fully-fledged webserver. This means that in your applications, you can just add
|
||||||
*servant-server* as a dependency, import `Servant` and not worry about anything
|
**servant-server** as a dependency, import `Servant` and not worry about anything
|
||||||
else.
|
else.
|
||||||
|
|
||||||
We will write a server that will serve the following API.
|
We will write a server that will serve the following API.
|
||||||
|
@ -154,9 +151,8 @@ main = run 8081 app1
|
||||||
|
|
||||||
You can put this all into a file or just grab [servant's
|
You can put this all into a file or just grab [servant's
|
||||||
repo](http://github.com/haskell-servant/servant) and look at the
|
repo](http://github.com/haskell-servant/servant) and look at the
|
||||||
*servant-examples* directory. The code we have just explored is in
|
*doc/tutorial* directory. This code (the source of this web page) is in
|
||||||
*tutorial/T1.hs*, runnable with
|
*doc/tutorial/Server.lhs*.
|
||||||
`dist/build/tutorial/tutorial 1`.
|
|
||||||
|
|
||||||
If you run it, you can go to `http://localhost:8081/users` in your browser or
|
If you run it, you can go to `http://localhost:8081/users` in your browser or
|
||||||
query it with curl and you see:
|
query it with curl and you see:
|
||||||
|
@ -192,7 +188,7 @@ users2 = [isaac, albert]
|
||||||
|
|
||||||
Now, just like we separate the various endpoints in `UserAPI` with `:<|>`, we
|
Now, just like we separate the various endpoints in `UserAPI` with `:<|>`, we
|
||||||
are going to separate the handlers with `:<|>` too! They must be provided in
|
are going to separate the handlers with `:<|>` too! They must be provided in
|
||||||
the same order as the one they appear in in the API type.
|
the same order as in in the API type.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
server2 :: Server UserAPI2
|
server2 :: Server UserAPI2
|
||||||
|
@ -201,9 +197,8 @@ server2 = return users2
|
||||||
:<|> return isaac
|
:<|> return isaac
|
||||||
```
|
```
|
||||||
|
|
||||||
And that's it! You can run this example with
|
And that's it! You can run this example in the same way that we showed for
|
||||||
`dist/build/tutorial/tutorial 2` and check out the data available
|
`server1` and check out the data available at `/users`, `/albert` and `/isaac`.
|
||||||
at `/users`, `/albert` and `/isaac`.
|
|
||||||
|
|
||||||
## From combinators to handler arguments
|
## From combinators to handler arguments
|
||||||
|
|
||||||
|
@ -298,8 +293,7 @@ parameter might not always be there);
|
||||||
|
|
||||||
- a `ReqBody contentTypeList a` becomes an argument of type `a`;
|
- a `ReqBody contentTypeList a` becomes an argument of type `a`;
|
||||||
|
|
||||||
And that's it. You can see this example in action by running
|
And that's it. Here's the example in action:
|
||||||
`dist/build/tutorial/tutorial 3`.
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
$ curl http://localhost:8081/position/1/2
|
$ curl http://localhost:8081/position/1/2
|
||||||
|
@ -312,19 +306,18 @@ $ curl -X POST -d '{"name":"Alp Mestanogullari", "email" : "alp@foo.com", "age":
|
||||||
{"subject":"Hey Alp Mestanogullari, we miss you!","body":"Hi Alp Mestanogullari,\n\nSince you've recently turned 25, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"}
|
{"subject":"Hey Alp Mestanogullari, we miss you!","body":"Hi Alp Mestanogullari,\n\nSince you've recently turned 25, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"}
|
||||||
```
|
```
|
||||||
|
|
||||||
For reference, here's a list of some combinators from *servant* and for those
|
For reference, here's a list of some combinators from **servant**:
|
||||||
that get turned into arguments to the handlers, the type of the argument.
|
|
||||||
|
|
||||||
> - `Delete`, `Get`, `Patch`, `Post`, `Put`: these do not become arguments. They provide the return type of handlers, which usually is `ExceptT ServantErr IO <something>`.
|
> - `Delete`, `Get`, `Patch`, `Post`, `Put`: these do not become arguments. They provide the return type of handlers, which usually is `ExceptT ServantErr IO <something>`.
|
||||||
> - `Capture "something" a` becomes an argument of type `a`.
|
> - `Capture "something" a` becomes an argument of type `a`.
|
||||||
> - `QueryParam "something" a`, `MatrixParam "something" a`, `Header "something" a` all become arguments of type `Maybe a`, because there might be no value at all specified by the client for these.
|
> - `QueryParam "something" a`, `Header "something" a` all become arguments of type `Maybe a`, because there might be no value at all specified by the client for these.
|
||||||
> - `QueryFlag "something"` and `MatrixFlag "something"` get turned into arguments of type `Bool`.
|
> - `QueryFlag "something"` gets turned into an argument of type `Bool`.
|
||||||
> - `QueryParams "something" a` and `MatrixParams "something" a` get turned into arguments of type `[a]`.
|
> - `QueryParams "something" a` gets turned into an argument of type `[a]`.
|
||||||
> - `ReqBody contentTypes a` gets turned into an argument of type `a`.
|
> - `ReqBody contentTypes a` gets turned into an argument of type `a`.
|
||||||
|
|
||||||
## The `FromHttpApiData`/`ToHttpApiData` classes
|
## The `FromHttpApiData`/`ToHttpApiData` classes
|
||||||
|
|
||||||
Wait... How does *servant* know how to decode the `Int`s from the URL? Or how
|
Wait... How does **servant** know how to decode the `Int`s from the URL? Or how
|
||||||
to decode a `ClientInfo` value from the request body? This is what this and the
|
to decode a `ClientInfo` value from the request body? This is what this and the
|
||||||
following two sections address.
|
following two sections address.
|
||||||
|
|
||||||
|
@ -333,7 +326,7 @@ following two sections address.
|
||||||
corresponding (textual) value in the request's "metadata". How types are
|
corresponding (textual) value in the request's "metadata". How types are
|
||||||
decoded from headers, captures, and query params is expressed in a class
|
decoded from headers, captures, and query params is expressed in a class
|
||||||
`FromHttpApiData` (from the package
|
`FromHttpApiData` (from the package
|
||||||
[*http-api-data*](http://hackage.haskell.org/package/http-api-data)):
|
[**http-api-data**](http://hackage.haskell.org/package/http-api-data)):
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
class FromHttpApiData a where
|
class FromHttpApiData a where
|
||||||
|
@ -355,15 +348,15 @@ As you can see, as long as you provide either `parseUrlPiece` (for `Capture`s)
|
||||||
or `parseQueryParam` (for `QueryParam`s), the other methods will be defined in
|
or `parseQueryParam` (for `QueryParam`s), the other methods will be defined in
|
||||||
terms of this.
|
terms of this.
|
||||||
|
|
||||||
*http-api-data* provides a decent number of instances, helpers for defining new
|
**http-api-data** provides a decent number of instances, helpers for defining new
|
||||||
ones, and wonderful documentation.
|
ones, and wonderful documentation.
|
||||||
|
|
||||||
There's not much else to say about these classes. You will need instances for
|
There's not much else to say about these classes. You will need instances for
|
||||||
them when using `Capture`, `QueryParam`, `QueryParams`, and `Header` with your
|
them when using `Capture`, `QueryParam`, `QueryParams`, and `Header` with your
|
||||||
types. You will need `FromHttpApiData` instances for server-side request
|
types. You will need `FromHttpApiData` instances for server-side request
|
||||||
handlers and `ToHttpApiData` instances only when using
|
handlers and `ToHttpApiData` instances only when using
|
||||||
*servant-client*, as described in the [section about deriving haskell
|
**servant-client**, as described in the [section about deriving haskell
|
||||||
functions to query an API](/tutorial/client.html).
|
functions to query an API](Client.html).
|
||||||
|
|
||||||
## Using content-types with your data types
|
## Using content-types with your data types
|
||||||
|
|
||||||
|
@ -371,14 +364,15 @@ The same principle was operating when decoding request bodies from JSON, and
|
||||||
responses *into* JSON. (JSON is just the running example - you can do this with
|
responses *into* JSON. (JSON is just the running example - you can do this with
|
||||||
any content-type.)
|
any content-type.)
|
||||||
|
|
||||||
This section introduces a couple of typeclasses provided by *servant* that make
|
This section introduces a couple of typeclasses provided by **servant** that make
|
||||||
all of this work.
|
all of this work.
|
||||||
|
|
||||||
### The truth behind `JSON`
|
### The truth behind `JSON`
|
||||||
|
|
||||||
|
|
||||||
What exactly is `JSON`? Like the 3 other content types provided out of the box
|
What exactly is `JSON` (the type as used in `Get '[JSON] User`)? Like the 3
|
||||||
by *servant*, it's a really dumb data type.
|
other content-types provided out of the box by **servant**, it's a really dumb
|
||||||
|
data type.
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
data JSON
|
data JSON
|
||||||
|
@ -388,14 +382,15 @@ data OctetStream
|
||||||
```
|
```
|
||||||
|
|
||||||
Obviously, this is not all there is to `JSON`, otherwise it would be quite
|
Obviously, this is not all there is to `JSON`, otherwise it would be quite
|
||||||
pointless. Like most of the data types in *servant*, `JSON` is mostly there as
|
pointless. Like most of the data types in **servant**, `JSON` is mostly there as
|
||||||
a special *symbol* that's associated with encoding (resp. decoding) to (resp.
|
a special *symbol* that's associated with encoding (resp. decoding) to (resp.
|
||||||
from) the *JSON* format. The way this association is performed can be
|
from) the *JSON* format. The way this association is performed can be
|
||||||
decomposed into two steps.
|
decomposed into two steps.
|
||||||
|
|
||||||
The first step is to provide a proper
|
The first step is to provide a proper
|
||||||
[`MediaType`](https://hackage.haskell.org/package/http-media-0.6.2/docs/Network-HTTP-Media.html)
|
`MediaType` (from
|
||||||
representation for `JSON`, or for your own content types. If you look at the
|
[**http-media**](https://hackage.haskell.org/package/http-media-0.6.2/docs/Network-HTTP-Media.html))
|
||||||
|
representation for `JSON`, or for your own content-types. If you look at the
|
||||||
haddocks from this link, you can see that we just have to specify
|
haddocks from this link, you can see that we just have to specify
|
||||||
`application/json` using the appropriate functions. In our case, we can just
|
`application/json` using the appropriate functions. In our case, we can just
|
||||||
use `(//) :: ByteString -> ByteString -> MediaType`. The precise way to specify
|
use `(//) :: ByteString -> ByteString -> MediaType`. The precise way to specify
|
||||||
|
@ -411,14 +406,14 @@ instance Accept JSON where
|
||||||
```
|
```
|
||||||
|
|
||||||
The second step is centered around the `MimeRender` and `MimeUnrender` classes.
|
The second step is centered around the `MimeRender` and `MimeUnrender` classes.
|
||||||
These classes just let you specify a way to respectively encode and decode
|
These classes just let you specify a way to encode and decode
|
||||||
values respectively into or from your content-type's representation.
|
values into or from your content-type's representation.
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
class Accept ctype => MimeRender ctype a where
|
class Accept ctype => MimeRender ctype a where
|
||||||
mimeRender :: Proxy ctype -> a -> ByteString
|
mimeRender :: Proxy ctype -> a -> ByteString
|
||||||
-- alternatively readable as:
|
-- alternatively readable as:
|
||||||
mimeRender :: Proxy ctype -> (a -> ByteString)
|
mimeRender :: Proxy ctype -> (a -> ByteString)
|
||||||
```
|
```
|
||||||
|
|
||||||
Given a content-type and some user type, `MimeRender` provides a function that
|
Given a content-type and some user type, `MimeRender` provides a function that
|
||||||
|
@ -444,7 +439,7 @@ class Accept ctype => MimeUnrender ctype a where
|
||||||
We don't have much work to do there either, `Data.Aeson.eitherDecode` is
|
We don't have much work to do there either, `Data.Aeson.eitherDecode` is
|
||||||
precisely what we need. However, it only allows arrays and objects as toplevel
|
precisely what we need. However, it only allows arrays and objects as toplevel
|
||||||
JSON values and this has proven to get in our way more than help us so we wrote
|
JSON values and this has proven to get in our way more than help us so we wrote
|
||||||
our own little function around *aeson* and *attoparsec* that allows any type of
|
our own little function around **aeson** and **attoparsec** that allows any type of
|
||||||
JSON value at the toplevel of a "JSON document". Here's the definition in case
|
JSON value at the toplevel of a "JSON document". Here's the definition in case
|
||||||
you are curious.
|
you are curious.
|
||||||
|
|
||||||
|
@ -462,20 +457,20 @@ instance FromJSON a => MimeUnrender JSON a where
|
||||||
mimeUnrender _ = eitherDecodeLenient
|
mimeUnrender _ = eitherDecodeLenient
|
||||||
```
|
```
|
||||||
|
|
||||||
And this is all the code that lets you use `JSON` for with `ReqBody`, `Get`,
|
And this is all the code that lets you use `JSON` with `ReqBody`, `Get`,
|
||||||
`Post` and friends. We can check our understanding by implementing support
|
`Post` and friends. We can check our understanding by implementing support
|
||||||
for an `HTML` content type, so that users of your webservice can access an
|
for an `HTML` content-type, so that users of your webservice can access an
|
||||||
HTML representation of the data they want, ready to be included in any HTML
|
HTML representation of the data they want, ready to be included in any HTML
|
||||||
document, e.g. using [jQuery's `load` function](https://api.jquery.com/load/),
|
document, e.g. using [jQuery's `load` function](https://api.jquery.com/load/),
|
||||||
simply by adding `Accept: text/html` to their request headers.
|
simply by adding `Accept: text/html` to their request headers.
|
||||||
|
|
||||||
### Case-studies: *servant-blaze* and *servant-lucid*
|
### Case-studies: **servant-blaze** and **servant-lucid**
|
||||||
|
|
||||||
These days, most of the haskellers who write their HTML UIs directly from
|
These days, most of the haskellers who write their HTML UIs directly from
|
||||||
Haskell use either [blaze-html](http://hackage.haskell.org/package/blaze-html)
|
Haskell use either [**blaze-html**](http://hackage.haskell.org/package/blaze-html)
|
||||||
or [lucid](http://hackage.haskell.org/package/lucid). The best option for
|
or [**lucid**](http://hackage.haskell.org/package/lucid). The best option for
|
||||||
*servant* is obviously to support both (and hopefully other templating
|
**servant** is obviously to support both (and hopefully other templating
|
||||||
solutions!).
|
solutions!). We're first going to look at **lucid**:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
data HTMLLucid
|
data HTMLLucid
|
||||||
|
@ -483,24 +478,20 @@ data HTMLLucid
|
||||||
|
|
||||||
Once again, the data type is just there as a symbol for the encoding/decoding
|
Once again, the data type is just there as a symbol for the encoding/decoding
|
||||||
functions, except that this time we will only worry about encoding since
|
functions, except that this time we will only worry about encoding since
|
||||||
*blaze-html* and *lucid* don't provide a way to extract data from HTML.
|
**lucid** doesn't provide a way to extract data from HTML.
|
||||||
|
|
||||||
Both packages also have the same `Accept` instance for their `HTMLLucid` type.
|
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
instance Accept HTMLLucid where
|
instance Accept HTMLLucid where
|
||||||
contentType _ = "text" // "html" /: ("charset", "utf-8")
|
contentType _ = "text" // "html" /: ("charset", "utf-8")
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that this instance uses the `(/:)` operator from *http-media* which lets
|
Note that this instance uses the `(/:)` operator from **http-media** which lets
|
||||||
us specify additional information about a content-type, like the charset here.
|
us specify additional information about a content-type, like the charset here.
|
||||||
|
|
||||||
The rendering instances for both packages both call similar functions that take
|
The rendering instances call similar functions that take
|
||||||
types with an appropriate instance to an "abstract" HTML representation and
|
types with an appropriate instance to an "abstract" HTML representation and
|
||||||
then write that to a `ByteString`.
|
then write that to a `ByteString`.
|
||||||
|
|
||||||
For *lucid*:
|
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
instance ToHtml a => MimeRender HTMLLucid a where
|
instance ToHtml a => MimeRender HTMLLucid a where
|
||||||
mimeRender _ = renderBS . toHtml
|
mimeRender _ = renderBS . toHtml
|
||||||
|
@ -511,7 +502,7 @@ instance MimeRender HTMLLucid (Html a) where
|
||||||
mimeRender _ = renderBS
|
mimeRender _ = renderBS
|
||||||
```
|
```
|
||||||
|
|
||||||
For *blaze-html*:
|
For **blaze-html** everything works very similarly:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
-- For this tutorial to compile 'HTMLLucid' and 'HTMLBlaze' have to be
|
-- For this tutorial to compile 'HTMLLucid' and 'HTMLBlaze' have to be
|
||||||
|
@ -531,15 +522,13 @@ instance MimeRender HTMLBlaze Text.Blaze.Html.Html where
|
||||||
mimeRender _ = renderHtml
|
mimeRender _ = renderHtml
|
||||||
```
|
```
|
||||||
|
|
||||||
Both [servant-blaze](http://hackage.haskell.org/package/servant-blaze) and
|
Both [**servant-blaze**](http://hackage.haskell.org/package/servant-blaze) and
|
||||||
[servant-lucid](http://hackage.haskell.org/package/servant-lucid) let you use
|
[**servant-lucid**](http://hackage.haskell.org/package/servant-lucid) let you use
|
||||||
`HTMLLucid` in any content type list as long as you provide an instance of the
|
`HTMLLucid` and `HTMLBlaze` in any content-type list as long as you provide an instance of the
|
||||||
appropriate class (`ToMarkup` for *blaze-html*, `ToHtml` for *lucid*).
|
appropriate class (`ToMarkup` for **blaze-html**, `ToHtml` for **lucid**).
|
||||||
|
|
||||||
We can now write webservice that uses *servant-lucid* to show the `HTMLLucid`
|
We can now write a webservice that uses **servant-lucid** to show the `HTMLLucid`
|
||||||
content type in action. First off, imports and pragmas as usual.
|
content-type in action. We will be serving the following API:
|
||||||
|
|
||||||
We will be serving the following API:
|
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
type PersonAPI = "persons" :> Get '[JSON, HTMLLucid] [Person]
|
type PersonAPI = "persons" :> Get '[JSON, HTMLLucid] [Person]
|
||||||
|
@ -556,7 +545,7 @@ data Person = Person
|
||||||
instance ToJSON Person
|
instance ToJSON Person
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, let's teach *lucid* how to render a `Person` as a row in a table, and then
|
Now, let's teach **lucid** how to render a `Person` as a row in a table, and then
|
||||||
a list of `Person`s as a table with a row per person.
|
a list of `Person`s as a table with a row per person.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
|
@ -600,10 +589,10 @@ server4 :: Server PersonAPI
|
||||||
server4 = return people
|
server4 = return people
|
||||||
|
|
||||||
app2 :: Application
|
app2 :: Application
|
||||||
app2 = serve personAPI EmptyConfig server4
|
app2 = serve personAPI server4
|
||||||
```
|
```
|
||||||
|
|
||||||
And we're good to go. You can run this example with `dist/build/tutorial/tutorial 4`.
|
And we're good to go:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
$ curl http://localhost:8081/persons
|
$ curl http://localhost:8081/persons
|
||||||
|
@ -616,23 +605,21 @@ And we're good to go. You can run this example with `dist/build/tutorial/tutoria
|
||||||
## The `ExceptT ServantErr IO` monad
|
## The `ExceptT ServantErr IO` monad
|
||||||
|
|
||||||
At the heart of the handlers is the monad they run in, namely `ExceptT
|
At the heart of the handlers is the monad they run in, namely `ExceptT
|
||||||
ServantErr IO`. One might wonder: why this monad? The answer is that it is the
|
ServantErr IO`
|
||||||
|
([haddock documentation for `ExceptT`](http://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Except.html#t:ExceptT)).
|
||||||
|
One might wonder: why this monad? The answer is that it is the
|
||||||
simplest monad with the following properties:
|
simplest monad with the following properties:
|
||||||
|
|
||||||
- it lets us both return a successful result (with the `Right` branch of
|
- it lets us both return a successful result (using `return`)
|
||||||
`Either`) or "fail" with a descriptive error (with the `Left` branch of
|
or "fail" with a descriptive error (using `throwError`);
|
||||||
`Either`);
|
|
||||||
- it lets us perform IO, which is absolutely vital since most webservices exist
|
- it lets us perform IO, which is absolutely vital since most webservices exist
|
||||||
as interfaces to databases that we interact with in `IO`;
|
as interfaces to databases that we interact with in `IO`.
|
||||||
|
|
||||||
Let's recall some definitions.
|
Let's recall some definitions.
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
-- from the Prelude
|
|
||||||
data Either e a = Left e | Right a
|
|
||||||
|
|
||||||
-- from the 'mtl' package at
|
-- from the 'mtl' package at
|
||||||
newtype ExceptT e m a = ExceptT ( m (Either e a) )
|
newtype ExceptT e m a = ExceptT (m (Either e a))
|
||||||
```
|
```
|
||||||
|
|
||||||
In short, this means that a handler of type `ExceptT ServantErr IO a` is simply
|
In short, this means that a handler of type `ExceptT ServantErr IO a` is simply
|
||||||
|
@ -654,14 +641,14 @@ kind and abort early. The next two sections cover how to do just that.
|
||||||
Another important instance from the list above is `MonadIO m => MonadIO
|
Another important instance from the list above is `MonadIO m => MonadIO
|
||||||
(ExceptT e m)`.
|
(ExceptT e m)`.
|
||||||
[`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html)
|
[`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html)
|
||||||
is a class from the *transformers* package defined as:
|
is a class from the **transformers** package defined as:
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
class Monad m => MonadIO m where
|
class Monad m => MonadIO m where
|
||||||
liftIO :: IO a -> m a
|
liftIO :: IO a -> m a
|
||||||
```
|
```
|
||||||
|
|
||||||
Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type
|
The `IO` monad provides a `MonadIO` instance. Hence for any type
|
||||||
`e`, `ExceptT e IO` has a `MonadIO` instance. So if you want to run any kind of
|
`e`, `ExceptT e IO` has a `MonadIO` instance. So if you want to run any kind of
|
||||||
IO computation in your handlers, just use `liftIO`:
|
IO computation in your handlers, just use `liftIO`:
|
||||||
|
|
||||||
|
@ -684,7 +671,7 @@ server5 = do
|
||||||
|
|
||||||
If you want to explicitly fail at providing the result promised by an endpoint
|
If you want to explicitly fail at providing the result promised by an endpoint
|
||||||
using the appropriate HTTP status code (not found, unauthorized, etc) and some
|
using the appropriate HTTP status code (not found, unauthorized, etc) and some
|
||||||
error message, all you have to do is use the `left` function mentioned above
|
error message, all you have to do is use the `throwError` function mentioned above
|
||||||
and provide it with the appropriate value of type `ServantErr`, which is
|
and provide it with the appropriate value of type `ServantErr`, which is
|
||||||
defined as:
|
defined as:
|
||||||
|
|
||||||
|
@ -703,7 +690,7 @@ use record update syntax:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
failingHandler :: ExceptT ServantErr IO ()
|
failingHandler :: ExceptT ServantErr IO ()
|
||||||
failingHandler = throwE myerr
|
failingHandler = throwError myerr
|
||||||
|
|
||||||
where myerr :: ServantErr
|
where myerr :: ServantErr
|
||||||
myerr = err503 { errBody = "Sorry dear user." }
|
myerr = err503 { errBody = "Sorry dear user." }
|
||||||
|
@ -718,13 +705,12 @@ server6 = do
|
||||||
exists <- liftIO (doesFileExist "myfile.txt")
|
exists <- liftIO (doesFileExist "myfile.txt")
|
||||||
if exists
|
if exists
|
||||||
then liftIO (readFile "myfile.txt") >>= return . FileContent
|
then liftIO (readFile "myfile.txt") >>= return . FileContent
|
||||||
else throwE custom404Err
|
else throwError custom404Err
|
||||||
|
|
||||||
where custom404Err = err404 { errBody = "myfile.txt just isn't there, please leave this server alone." }
|
where custom404Err = err404 { errBody = "myfile.txt just isn't there, please leave this server alone." }
|
||||||
```
|
```
|
||||||
|
|
||||||
Let's run this server (`dist/build/tutorial/tutorial 5`) and
|
Here's how that server looks in action:
|
||||||
query it, first without the file and then with the file.
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
$ curl --verbose http://localhost:8081/myfile.txt
|
$ curl --verbose http://localhost:8081/myfile.txt
|
||||||
|
@ -773,10 +759,10 @@ Note that the type of `addHeader x` is different than the type of `x`!
|
||||||
|
|
||||||
## Serving static files
|
## Serving static files
|
||||||
|
|
||||||
*servant-server* also provides a way to just serve the content of a directory
|
**servant-server** also provides a way to just serve the content of a directory
|
||||||
under some path in your web API. As mentioned earlier in this document, the
|
under some path in your web API. As mentioned earlier in this document, the
|
||||||
`Raw` combinator can be used in your APIs to mean "plug here any WAI
|
`Raw` combinator can be used in your APIs to mean "plug here any WAI
|
||||||
application". Well, servant-server provides a function to get a file and
|
application". Well, **servant-server** provides a function to get a file and
|
||||||
directory serving WAI application, namely:
|
directory serving WAI application, namely:
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
|
@ -784,136 +770,36 @@ directory serving WAI application, namely:
|
||||||
serveDirectory :: FilePath -> Server Raw
|
serveDirectory :: FilePath -> Server Raw
|
||||||
```
|
```
|
||||||
|
|
||||||
`serveDirectory`'s argument must be a path to a valid directory. You can see an
|
`serveDirectory`'s argument must be a path to a valid directory.
|
||||||
example below, runnable with `dist/build/tutorial/tutorial 6`
|
|
||||||
(you **must** run it from within the *servant-examples/* directory!), which is
|
|
||||||
a webserver that serves the various bits of code covered in this
|
|
||||||
getting-started.
|
|
||||||
|
|
||||||
The API type will be the following.
|
Here's an example API that will serve some static files:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
type CodeAPI = "code" :> Raw
|
type StaticAPI = "static" :> Raw
|
||||||
```
|
```
|
||||||
|
|
||||||
And the server:
|
And the server:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
codeAPI :: Proxy CodeAPI
|
staticAPI :: Proxy StaticAPI
|
||||||
codeAPI = Proxy
|
staticAPI = Proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
server7 :: Server CodeAPI
|
server7 :: Server StaticAPI
|
||||||
server7 = serveDirectory "tutorial"
|
server7 = serveDirectory "static-files"
|
||||||
|
|
||||||
app3 :: Application
|
app3 :: Application
|
||||||
app3 = serve codeAPI EmptyConfig server7
|
app3 = serve staticAPI server7
|
||||||
```
|
```
|
||||||
|
|
||||||
This server will match any request whose path starts with `/code` and will look
|
This server will match any request whose path starts with `/static` and will look
|
||||||
for a file at the path described by the rest of the request path, inside the
|
for a file at the path described by the rest of the request path, inside the
|
||||||
*tutorial/* directory of the path you run the program from.
|
*static-files/* directory of the path you run the program from.
|
||||||
|
|
||||||
In other words:
|
In other words: If a client requests `/static/foo.txt`, the server will look for a file at
|
||||||
|
`./static-files/foo.txt`. If that file exists it'll succeed and serve the file.
|
||||||
- If a client requests `/code/foo.txt`, the server will look for a file at
|
If it doesn't exist, the handler will fail with a `404` status code.
|
||||||
`./tutorial/foo.txt` (and fail)
|
|
||||||
- If a client requests `/code/T1.hs`, the server will look for a file at
|
|
||||||
`./tutorial/T1.hs` (and succeed)
|
|
||||||
- If a client requests `/code/foo/bar/baz/movie.mp4`, the server will look for
|
|
||||||
a file at `./tutorial/foo/bar/baz/movie.mp4` (and fail)
|
|
||||||
|
|
||||||
Here is our little server in action.
|
|
||||||
|
|
||||||
``` haskell ignore
|
|
||||||
$ curl http://localhost:8081/code/T1.hs
|
|
||||||
{-# LANGUAGE DataKinds #-}
|
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
|
||||||
{-# LANGUAGE TypeOperators #-}
|
|
||||||
module T1 where
|
|
||||||
|
|
||||||
import Data.Aeson
|
|
||||||
import Data.Time.Calendar
|
|
||||||
import GHC.Generics
|
|
||||||
import Network.Wai
|
|
||||||
import Servant
|
|
||||||
|
|
||||||
data User = User
|
|
||||||
{ name :: String
|
|
||||||
, age :: Int
|
|
||||||
, email :: String
|
|
||||||
, registration_date :: Day
|
|
||||||
} deriving (Eq, Show, Generic)
|
|
||||||
|
|
||||||
-- orphan ToJSON instance for Day. necessary to derive one for User
|
|
||||||
instance ToJSON Day where
|
|
||||||
-- display a day in YYYY-mm-dd format
|
|
||||||
toJSON d = toJSON (showGregorian d)
|
|
||||||
|
|
||||||
instance ToJSON User
|
|
||||||
|
|
||||||
type UserAPI = "users" :> Get '[JSON] [User]
|
|
||||||
|
|
||||||
users :: [User]
|
|
||||||
users =
|
|
||||||
[ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1)
|
|
||||||
, User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
userAPI :: Proxy UserAPI
|
|
||||||
userAPI = Proxy
|
|
||||||
|
|
||||||
server :: Server UserAPI
|
|
||||||
server = return users
|
|
||||||
|
|
||||||
app :: Application
|
|
||||||
app = serve userAPI server
|
|
||||||
$ curl http://localhost:8081/code/tutorial.hs
|
|
||||||
import Network.Wai
|
|
||||||
import Network.Wai.Handler.Warp
|
|
||||||
import System.Environment
|
|
||||||
|
|
||||||
import qualified T1
|
|
||||||
import qualified T2
|
|
||||||
import qualified T3
|
|
||||||
import qualified T4
|
|
||||||
import qualified T5
|
|
||||||
import qualified T6
|
|
||||||
import qualified T7
|
|
||||||
import qualified T9
|
|
||||||
import qualified T10
|
|
||||||
|
|
||||||
app :: String -> (Application -> IO ()) -> IO ()
|
|
||||||
app n f = case n of
|
|
||||||
"1" -> f T1.app
|
|
||||||
"2" -> f T2.app
|
|
||||||
"3" -> f T3.app
|
|
||||||
"4" -> f T4.app
|
|
||||||
"5" -> f T5.app
|
|
||||||
"6" -> f T6.app
|
|
||||||
"7" -> f T7.app
|
|
||||||
"8" -> f T3.app
|
|
||||||
"9" -> T9.writeJSFiles >> f T9.app
|
|
||||||
"10" -> f T10.app
|
|
||||||
_ -> usage
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
args <- getArgs
|
|
||||||
case args of
|
|
||||||
[n] -> app n (run 8081)
|
|
||||||
_ -> usage
|
|
||||||
|
|
||||||
usage :: IO ()
|
|
||||||
usage = do
|
|
||||||
putStrLn "Usage:\t tutorial N"
|
|
||||||
putStrLn "\t\twhere N is the number of the example you want to run."
|
|
||||||
|
|
||||||
$ curl http://localhost:8081/foo
|
|
||||||
not found
|
|
||||||
```
|
|
||||||
|
|
||||||
## Nested APIs
|
## Nested APIs
|
||||||
|
|
||||||
|
@ -1123,7 +1009,7 @@ type Server api = ServerT api (ExceptT ServantErr IO)
|
||||||
|
|
||||||
`ServerT` is the actual type family that computes the required types for the
|
`ServerT` is the actual type family that computes the required types for the
|
||||||
handlers that's part of the `HasServer` class. It's like `Server` except that
|
handlers that's part of the `HasServer` class. It's like `Server` except that
|
||||||
it takes a third parameter which is the monad you want your handlers to run in,
|
it takes another parameter which is the monad you want your handlers to run in,
|
||||||
or more generally the return types of your handlers. This third parameter is
|
or more generally the return types of your handlers. This third parameter is
|
||||||
used for specifying the return type of the handler for an endpoint, e.g when
|
used for specifying the return type of the handler for an endpoint, e.g when
|
||||||
computing `ServerT (Get '[JSON] Person) SomeMonad`. The result would be
|
computing `ServerT (Get '[JSON] Person) SomeMonad`. The result would be
|
||||||
|
@ -1131,7 +1017,7 @@ computing `ServerT (Get '[JSON] Person) SomeMonad`. The result would be
|
||||||
|
|
||||||
The first and main question one might have then is: how do we write handlers
|
The first and main question one might have then is: how do we write handlers
|
||||||
that run in another monad? How can we "bring back" the value from a given monad
|
that run in another monad? How can we "bring back" the value from a given monad
|
||||||
into something *servant* can understand?
|
into something **servant** can understand?
|
||||||
|
|
||||||
### Natural transformations
|
### Natural transformations
|
||||||
|
|
||||||
|
@ -1140,11 +1026,15 @@ do we have?
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
newtype m :~> n = Nat { unNat :: forall a. m a -> n a}
|
newtype m :~> n = Nat { unNat :: forall a. m a -> n a}
|
||||||
|
|
||||||
-- For example
|
|
||||||
-- listToMaybeNat ::`[] :~> Maybe`
|
|
||||||
-- listToMaybeNat = Nat listToMaybe -- from Data.Maybe
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
listToMaybeNat :: [] :~> Maybe
|
||||||
|
listToMaybeNat = Nat listToMaybe -- from Data.Maybe
|
||||||
|
```
|
||||||
|
|
||||||
(`Nat` comes from "natural transformation", in case you're wondering.)
|
(`Nat` comes from "natural transformation", in case you're wondering.)
|
||||||
|
|
||||||
So if you want to write handlers using another monad/type than `ExceptT
|
So if you want to write handlers using another monad/type than `ExceptT
|
||||||
|
@ -1152,20 +1042,20 @@ ServantErr IO`, say the `Reader String` monad, the first thing you have to
|
||||||
prepare is a function:
|
prepare is a function:
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
readerToEither :: Reader String :~> ExceptT ServantErr IO
|
readerToHandler :: Reader String :~> ExceptT ServantErr IO
|
||||||
```
|
```
|
||||||
|
|
||||||
Let's start with `readerToEither'`. We obviously have to run the `Reader`
|
Let's start with `readerToHandler'`. We obviously have to run the `Reader`
|
||||||
computation by supplying it with a `String`, like `"hi"`. We get an `a` out
|
computation by supplying it with a `String`, like `"hi"`. We get an `a` out
|
||||||
from that and can then just `return` it into `ExceptT`. We can then just wrap
|
from that and can then just `return` it into `ExceptT`. We can then just wrap
|
||||||
that function with the `Nat` constructor to make it have the fancier type.
|
that function with the `Nat` constructor to make it have the fancier type.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
readerToEither' :: forall a. Reader String a -> ExceptT ServantErr IO a
|
readerToHandler' :: forall a. Reader String a -> ExceptT ServantErr IO a
|
||||||
readerToEither' r = return (runReader r "hi")
|
readerToHandler' r = return (runReader r "hi")
|
||||||
|
|
||||||
readerToEither :: Reader String :~> ExceptT ServantErr IO
|
readerToHandler :: Reader String :~> ExceptT ServantErr IO
|
||||||
readerToEither = Nat readerToEither'
|
readerToHandler = Nat readerToHandler'
|
||||||
```
|
```
|
||||||
|
|
||||||
We can write some simple webservice with the handlers running in `Reader String`.
|
We can write some simple webservice with the handlers running in `Reader String`.
|
||||||
|
@ -1193,25 +1083,24 @@ ServantErr IO`. But there's a simple solution to this.
|
||||||
|
|
||||||
### Enter `enter`
|
### Enter `enter`
|
||||||
|
|
||||||
That's right. We have just written `readerToEither`, which is exactly what we
|
That's right. We have just written `readerToHandler`, which is exactly what we
|
||||||
would need to apply to the results of all handlers to make the handlers have the
|
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
|
right type for `serve`. Being cumbersome to do by hand, we provide a function
|
||||||
`enter` which takes a natural transformation between two parametrized types `m`
|
`enter` which takes a natural transformation between two parametrized types `m`
|
||||||
and `n` and a `ServerT someapi m`, and returns a `ServerT someapi n`.
|
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 `enter
|
In our case, we can wrap up our little webservice by using `enter
|
||||||
readerToEither` on our handlers.
|
readerToHandler` on our handlers.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
readerServer :: Server ReaderAPI
|
readerServer :: Server ReaderAPI
|
||||||
readerServer = enter readerToEither readerServerT
|
readerServer = enter readerToHandler readerServerT
|
||||||
|
|
||||||
app4 :: Application
|
app4 :: Application
|
||||||
app4 = serve readerAPI EmptyConfig readerServer
|
app4 = serve readerAPI readerServer
|
||||||
```
|
```
|
||||||
|
|
||||||
And we can indeed see this webservice in action by running
|
This is the webservice in action:
|
||||||
`dist/build/tutorial/tutorial 7`.
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
$ curl http://localhost:8081/a
|
$ curl http://localhost:8081/a
|
||||||
|
@ -1222,7 +1111,6 @@ $ curl http://localhost:8081/b
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
You're now equipped to write any kind of webservice/web-application using
|
You're now equipped to write webservices/web-applications using
|
||||||
*servant*. One thing not covered here is how to incorporate your own
|
**servant**. The rest of this document focuses on **servant-client**,
|
||||||
combinators and will be the topic of a page on the website. The rest of this
|
**servant-js** and **servant-docs**.
|
||||||
document focuses on *servant-client*, *servant-jquery* and *servant-docs*.
|
|
||||||
|
|
Loading…
Reference in a new issue