more consistent line breaks
This commit is contained in:
parent
4dbd721d39
commit
571fcc1f26
1 changed files with 64 additions and 50 deletions
|
@ -3,29 +3,10 @@
|
||||||
Enough chit-chat about type-level combinators and representing an API as a
|
Enough chit-chat about type-level combinators and representing an API as a
|
||||||
type. Can we have a webservice already?
|
type. Can we have a webservice already?
|
||||||
|
|
||||||
If you want to follow along with the code and run the examples while you read this guide:
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
cabal get servant-examples
|
|
||||||
cd servant-examples-<VERSION>
|
|
||||||
cabal sandbox init
|
|
||||||
cabal install --dependencies-only
|
|
||||||
cabal configure && cabal build
|
|
||||||
```
|
|
||||||
|
|
||||||
This will produce a `tutorial` executable in the
|
|
||||||
`dist/build/tutorial` directory that just runs the example corresponding
|
|
||||||
to the number specified as a command line argument:
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
$ dist/build/tutorial/tutorial
|
|
||||||
Usage: tutorial N
|
|
||||||
where N is the number of the example you want to run.
|
|
||||||
```
|
|
||||||
|
|
||||||
## A first example
|
## A first example
|
||||||
|
|
||||||
Equipped with some basic knowledge about the way we represent API, let's now write our first webservice.
|
Equipped with some basic knowledge about the way we represent API, let's now
|
||||||
|
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
|
||||||
need to have some language extensions and imports:
|
need to have some language extensions and imports:
|
||||||
|
@ -72,7 +53,13 @@ import qualified Text.Blaze.Html
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# 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 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 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 else.
|
**Important**: the `Servant` module comes from the *servant-server* package,
|
||||||
|
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
|
||||||
|
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
|
||||||
|
*servant-server* as a dependency, import `Servant` and not worry about anything
|
||||||
|
else.
|
||||||
|
|
||||||
We will write a server that will serve the following API.
|
We will write a server that will serve the following API.
|
||||||
|
|
||||||
|
@ -95,7 +82,7 @@ data User = User
|
||||||
{ name :: String
|
{ name :: String
|
||||||
, age :: Int
|
, age :: Int
|
||||||
, email :: String
|
, email :: String
|
||||||
, registrationDate :: Day
|
, registration_date :: Day
|
||||||
} deriving (Eq, Show, Generic)
|
} deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
instance ToJSON User
|
instance ToJSON User
|
||||||
|
@ -140,7 +127,9 @@ server1 :: Server UserAPI1
|
||||||
server1 = return users1
|
server1 = return users1
|
||||||
```
|
```
|
||||||
|
|
||||||
That's it. Now we can turn `server` into an actual webserver using [wai](http://hackage.haskell.org/package/wai) and [warp](http://hackage.haskell.org/package/warp):
|
That's it. Now we can turn `server` into an actual webserver using
|
||||||
|
[wai](http://hackage.haskell.org/package/wai) and
|
||||||
|
[warp](http://hackage.haskell.org/package/warp):
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
userAPI :: Proxy UserAPI1
|
userAPI :: Proxy UserAPI1
|
||||||
|
@ -179,7 +168,8 @@ $ curl http://localhost:8081/users
|
||||||
|
|
||||||
## More endpoints
|
## More endpoints
|
||||||
|
|
||||||
What if we want more than one endpoint? Let's add `/albert` and `/isaac` to view the corresponding users encoded in JSON.
|
What if we want more than one endpoint? Let's add `/albert` and `/isaac` to
|
||||||
|
view the corresponding users encoded in JSON.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
type UserAPI2 = "users" :> Get '[JSON] [User]
|
type UserAPI2 = "users" :> Get '[JSON] [User]
|
||||||
|
@ -225,7 +215,8 @@ argument of the appropriate type automatically. You don't have to worry about
|
||||||
manually looking up URL captures or query string parameters, or
|
manually looking up URL captures or query string parameters, or
|
||||||
decoding/encoding data from/to JSON. Never.
|
decoding/encoding data from/to JSON. Never.
|
||||||
|
|
||||||
We are going to use the following data types and functions to implement a server for `API`.
|
We are going to use the following data types and functions to implement a
|
||||||
|
server for `API`.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
|
type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
|
||||||
|
@ -307,7 +298,8 @@ 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 `dist/build/tutorial/tutorial 3`.
|
And that's it. You can see this example in action by running
|
||||||
|
`dist/build/tutorial/tutorial 3`.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
$ curl http://localhost:8081/position/1/2
|
$ curl http://localhost:8081/position/1/2
|
||||||
|
@ -474,8 +466,8 @@ And this is all the code that lets you use `JSON` for 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/), simply by adding `Accept:
|
document, e.g. using [jQuery's `load` function](https://api.jquery.com/load/),
|
||||||
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*
|
||||||
|
|
||||||
|
@ -766,7 +758,8 @@ query it, first without the file and then with the file.
|
||||||
|
|
||||||
## Response headers
|
## Response headers
|
||||||
|
|
||||||
To add headers to your response, use [addHeader](http://hackage.haskell.org/package/servant-0.4.4/docs/Servant-API-ResponseHeaders.html).
|
To add headers to your response, use
|
||||||
|
[addHeader](http://hackage.haskell.org/package/servant/docs/Servant-API-ResponseHeaders.html).
|
||||||
Note that this changes the type of your API, as we can see in the following example:
|
Note that this changes the type of your API, as we can see in the following example:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
|
@ -824,9 +817,12 @@ for a file at the path described by the rest of the request path, inside the
|
||||||
|
|
||||||
In other words:
|
In other words:
|
||||||
|
|
||||||
- If a client requests `/code/foo.txt`, the server will look for a file at `./tutorial/foo.txt` (and fail)
|
- If a client requests `/code/foo.txt`, the server will look for a file at
|
||||||
- If a client requests `/code/T1.hs`, the server will look for a file at `./tutorial/T1.hs` (and succeed)
|
`./tutorial/foo.txt` (and fail)
|
||||||
- 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)
|
- 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.
|
Here is our little server in action.
|
||||||
|
|
||||||
|
@ -921,7 +917,8 @@ not found
|
||||||
|
|
||||||
## Nested APIs
|
## Nested APIs
|
||||||
|
|
||||||
Let's see how you can define APIs in a modular way, while avoiding repetition. Consider this simple example:
|
Let's see how you can define APIs in a modular way, while avoiding repetition.
|
||||||
|
Consider this simple example:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
type UserAPI3 = -- view the user with given userid, in JSON
|
type UserAPI3 = -- view the user with given userid, in JSON
|
||||||
|
@ -940,7 +937,8 @@ type UserAPI4 = Capture "userid" Int :>
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
However, you have to be aware that this has an effect on the type of the corresponding `Server`:
|
However, you have to be aware that this has an effect on the type of the
|
||||||
|
corresponding `Server`:
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
Server UserAPI3 = (Int -> ExceptT ServantErr IO User)
|
Server UserAPI3 = (Int -> ExceptT ServantErr IO User)
|
||||||
|
@ -952,7 +950,8 @@ Server UserAPI4 = Int -> ( ExceptT ServantErr IO User
|
||||||
```
|
```
|
||||||
|
|
||||||
In the first case, each handler receives the *userid* argument. In the latter,
|
In the first case, each handler receives the *userid* argument. In the latter,
|
||||||
the whole `Server` takes the *userid* and has handlers that are just computations in `ExceptT`, with no arguments. In other words:
|
the whole `Server` takes the *userid* and has handlers that are just
|
||||||
|
computations in `ExceptT`, with no arguments. In other words:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
server8 :: Server UserAPI3
|
server8 :: Server UserAPI3
|
||||||
|
@ -977,7 +976,10 @@ server9 userid = getUser userid :<|> deleteUser userid
|
||||||
deleteUser = error "..."
|
deleteUser = error "..."
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that there's nothing special about `Capture` that lets you "factor it out": this can be done with any combinator. Here are a few examples of APIs with a combinator factored out for which we can write a perfectly valid `Server`.
|
Note that there's nothing special about `Capture` that lets you "factor it
|
||||||
|
out": this can be done with any combinator. Here are a few examples of APIs
|
||||||
|
with a combinator factored out for which we can write a perfectly valid
|
||||||
|
`Server`.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
-- we just factor out the "users" path fragment
|
-- we just factor out the "users" path fragment
|
||||||
|
@ -1002,7 +1004,8 @@ newtype Token = Token ByteString
|
||||||
newtype SecretData = SecretData ByteString
|
newtype SecretData = SecretData ByteString
|
||||||
```
|
```
|
||||||
|
|
||||||
This approach lets you define APIs modularly and assemble them all into one big API type only at the end.
|
This approach lets you define APIs modularly and assemble them all into one big
|
||||||
|
API type only at the end.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
type UsersAPI =
|
type UsersAPI =
|
||||||
|
@ -1080,7 +1083,8 @@ server10 :: Server CombinedAPI
|
||||||
server10 = usersServer :<|> productsServer
|
server10 = usersServer :<|> productsServer
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, we can realize the user and product APIs are quite similar and abstract that away:
|
Finally, we can realize the user and product APIs are quite similar and
|
||||||
|
abstract that away:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
-- API for values of type 'a'
|
-- API for values of type 'a'
|
||||||
|
@ -1109,15 +1113,25 @@ serverFor = error "..."
|
||||||
|
|
||||||
## Using another monad for your handlers
|
## Using another monad for your handlers
|
||||||
|
|
||||||
Remember how `Server` turns combinators for HTTP methods into `ExceptT ServantErr IO`? Well, actually, there's more to that. `Server` is actually a simple type synonym.
|
Remember how `Server` turns combinators for HTTP methods into `ExceptT
|
||||||
|
ServantErr IO`? Well, actually, there's more to that. `Server` is actually a
|
||||||
|
simple type synonym.
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
type Server api = ServerT api (ExceptT ServantErr IO)
|
type Server api = ServerT api (ExceptT ServantErr IO)
|
||||||
```
|
```
|
||||||
|
|
||||||
`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 it takes a third 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 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 `SomeMonad Person`.
|
`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
|
||||||
|
it takes a third 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
|
||||||
|
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
|
||||||
|
`SomeMonad Person`.
|
||||||
|
|
||||||
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 into something *servant* can understand?
|
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
|
||||||
|
into something *servant* can understand?
|
||||||
|
|
||||||
### Natural transformations
|
### Natural transformations
|
||||||
|
|
||||||
|
@ -1185,7 +1199,8 @@ 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 readerToEither` on our handlers.
|
In our case, we can wrap up our little webservice by using `enter
|
||||||
|
readerToEither` on our handlers.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
readerServer :: Server ReaderAPI
|
readerServer :: Server ReaderAPI
|
||||||
|
@ -1195,7 +1210,8 @@ app4 :: Application
|
||||||
app4 = serve readerAPI EmptyConfig readerServer
|
app4 = serve readerAPI EmptyConfig readerServer
|
||||||
```
|
```
|
||||||
|
|
||||||
And we can indeed see this webservice in action by running `dist/build/tutorial/tutorial 7`.
|
And we can indeed see this webservice in action by running
|
||||||
|
`dist/build/tutorial/tutorial 7`.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
$ curl http://localhost:8081/a
|
$ curl http://localhost:8081/a
|
||||||
|
@ -1206,9 +1222,7 @@ $ curl http://localhost:8081/b
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
You're now equipped to write any kind of webservice/web-application using *servant*. One thing not covered here is how to incorporate your own combinators and will be the topic of a page on the website. The rest of this document focuses on *servant-client*, *servant-jquery* and *servant-docs*.
|
You're now equipped to write any kind of webservice/web-application using
|
||||||
|
*servant*. One thing not covered here is how to incorporate your own
|
||||||
<div style="text-align: center;">
|
combinators and will be the topic of a page on the website. The rest of this
|
||||||
<p><a href="/tutorial/api-type.html">Previous page: A web API as a type</a></p>
|
document focuses on *servant-client*, *servant-jquery* and *servant-docs*.
|
||||||
<p><a href="/tutorial/client.html">Next page: Deriving Haskell functions to query an API</a></p>
|
|
||||||
</div>
|
|
||||||
|
|
Loading…
Reference in a new issue