hopefully solved the merge conflict
This commit is contained in:
commit
7fbcca9dfb
29 changed files with 512 additions and 603 deletions
1
.ghci
1
.ghci
|
@ -1 +0,0 @@
|
||||||
:set -itest -isrc -packagehspec2
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
While defining handlers that serve an API has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. Except that we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate a bunch of client-side functions.
|
While defining handlers that serve an API has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. Except that we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate a bunch of client-side functions.
|
||||||
|
|
||||||
*servant* however has a way to inspect API, because APIs are just Haskell types and (GHC) Haskell lets us do quite a few things with types. In the same way that we look at an API type to deduce the types the handlers should have, we can inspect the structure of the API to *derive* Haskell functions that take one argument for each occurence of `Capture`, `ReqBody`, `QueryParam`
|
**servant** however has a way to inspect APIs, because APIs are just Haskell types and (GHC) Haskell lets us do quite a few things with types. In the same way that we look at an API type to deduce the types the handlers should have, we can inspect the structure of the API to *derive* Haskell functions that take one argument for each occurence of `Capture`, `ReqBody`, `QueryParam`
|
||||||
and friends. By *derive*, we mean that there's no code generation involved, the functions are defined just by the structure of the API type.
|
and friends. By *derive*, we mean that there's no code generation involved, the functions are defined just by the structure of the API type.
|
||||||
|
|
||||||
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
|
||||||
|
@ -67,7 +67,7 @@ type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Posit
|
||||||
:<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
|
:<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
|
||||||
```
|
```
|
||||||
|
|
||||||
What we are going to get with *servant-client* here is 3 functions, one to query each endpoint:
|
What we are going to get with **servant-client** here is 3 functions, one to query each endpoint:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
position :: Int -- ^ value for "x"
|
position :: Int -- ^ value for "x"
|
||||||
|
@ -81,7 +81,15 @@ marketing :: ClientInfo -- ^ value for the request body
|
||||||
-> ExceptT ServantError IO Email
|
-> ExceptT ServantError IO Email
|
||||||
```
|
```
|
||||||
|
|
||||||
Each function makes available as an argument any value that the response may depend on, as evidenced in the API type. How do we get these functions? Just give a `Proxy` to your API and a host to make the requests to:
|
Each function makes available as an argument any value that the response may
|
||||||
|
depend on, as evidenced in the API type. How do we get these functions? By calling
|
||||||
|
the function `client`. It takes three arguments:
|
||||||
|
|
||||||
|
- a `Proxy` to your API,
|
||||||
|
- a `BaseUrl`, consisting of the protocol, the host, the port and an optional subpath --
|
||||||
|
this basically tells `client` where the service that you want to query is hosted,
|
||||||
|
- a `Manager`, (from [http-client](http://hackage.haskell.org/package/http-client))
|
||||||
|
which manages http connections.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
api :: Proxy API
|
api :: Proxy API
|
||||||
|
@ -95,6 +103,9 @@ position :<|> hello :<|> marketing =
|
||||||
client api (BaseUrl Http "localhost" 8081 "") __manager
|
client api (BaseUrl Http "localhost" 8081 "") __manager
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(Yes, the usage of `unsafePerformIO` is very ugly, we know. Hopefully soon it'll
|
||||||
|
be possible to do without.)
|
||||||
|
|
||||||
As you can see in the code above, we just "pattern match our way" to these functions. If we try to derive less or more functions than there are endpoints in the API, we obviously get an error. The `BaseUrl` value there is just:
|
As you can see in the code above, we just "pattern match our way" to these functions. If we try to derive less or more functions than there are endpoints in the API, we obviously get an error. The `BaseUrl` value there is just:
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
|
@ -134,16 +145,12 @@ run = do
|
||||||
print em
|
print em
|
||||||
```
|
```
|
||||||
|
|
||||||
You can now run `dist/build/tutorial/tutorial 8` (the server) and
|
Here's the output of the above code running against the appropriate server:
|
||||||
`dist/build/t8-main/t8-main` (the client) to see them both in action.
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
$ dist/build/tutorial/tutorial 8
|
Position {x = 10, y = 10}
|
||||||
# and in another terminal:
|
HelloMessage {msg = "Hello, servant"}
|
||||||
$ dist/build/t8-main/t8-main
|
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!"}
|
||||||
Position {x = 10, y = 10}
|
|
||||||
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. You now know how to use **servant-client**!
|
||||||
|
|
|
@ -26,7 +26,7 @@ import Servant.Server
|
||||||
```
|
```
|
||||||
|
|
||||||
And we'll import some things from one of our earlier modules
|
And we'll import some things from one of our earlier modules
|
||||||
([Serving an API](/tutorial/server.html)):
|
([Serving an API](Server.html)):
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..),
|
import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..),
|
||||||
|
@ -35,7 +35,7 @@ import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..),
|
||||||
|
|
||||||
Like client function generation, documentation generation amounts to inspecting the API type and extracting all the data we need to then present it in some format to users of your API.
|
Like client function generation, documentation generation amounts to inspecting the API type and extracting all the data we need to then present it in some format to users of your API.
|
||||||
|
|
||||||
This time however, we have to assist *servant*. While it is able to deduce a lot of things about our API, it can't magically come up with descriptions of the various pieces of our APIs that are human-friendly and explain what's going on "at the business-logic level". A good example to study for documentation generation is our webservice with the `/position`, `/hello` and `/marketing` endpoints from earlier:
|
This time however, we have to assist **servant**. While it is able to deduce a lot of things about our API, it can't magically come up with descriptions of the various pieces of our APIs that are human-friendly and explain what's going on "at the business-logic level". A good example to study for documentation generation is our webservice with the `/position`, `/hello` and `/marketing` endpoints from earlier:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
|
type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
|
||||||
|
@ -46,7 +46,7 @@ exampleAPI :: Proxy ExampleAPI
|
||||||
exampleAPI = Proxy
|
exampleAPI = Proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
While *servant* can see e.g. that there are 3 endpoints and that the response bodies will be in JSON, it doesn't know what influence the captures, parameters, request bodies and other combinators have on the webservice. This is where some manual work is required.
|
While **servant** can see e.g. that there are 3 endpoints and that the response bodies will be in JSON, it doesn't know what influence the captures, parameters, request bodies and other combinators have on the webservice. This is where some manual work is required.
|
||||||
|
|
||||||
For every capture, request body, response body, query param, we have to give some explanations about how it influences the response, what values are possible and the likes. Here's how it looks like for the parameters we have above.
|
For every capture, request body, response body, query param, we have to give some explanations about how it influences the response, what values are possible and the likes. Here's how it looks like for the parameters we have above.
|
||||||
|
|
||||||
|
@ -97,9 +97,9 @@ apiDocs :: API
|
||||||
apiDocs = docs exampleAPI
|
apiDocs = docs exampleAPI
|
||||||
```
|
```
|
||||||
|
|
||||||
`API` is a type provided by *servant-docs* that stores all the information one needs about a web API in order to generate documentation in some format. Out of the box, *servant-docs* only provides a pretty documentation printer that outputs [Markdown](http://en.wikipedia.org/wiki/Markdown), but the [servant-pandoc](http://hackage.haskell.org/package/servant-pandoc) package can be used to target many useful formats.
|
`API` is a type provided by **servant-docs** that stores all the information one needs about a web API in order to generate documentation in some format. Out of the box, **servant-docs** only provides a pretty documentation printer that outputs [Markdown](http://en.wikipedia.org/wiki/Markdown), but the [**servant-pandoc**](http://hackage.haskell.org/package/servant-pandoc) package can be used to target many useful formats.
|
||||||
|
|
||||||
*servant*'s markdown pretty printer is a function named `markdown`.
|
**servant**'s markdown pretty printer is a function named `markdown`.
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
markdown :: API -> String
|
markdown :: API -> String
|
||||||
|
@ -107,97 +107,97 @@ markdown :: API -> String
|
||||||
|
|
||||||
That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`.
|
That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`.
|
||||||
|
|
||||||
``` text
|
````````` text
|
||||||
## Welcome
|
## Welcome
|
||||||
|
|
||||||
This is our super webservice's API.
|
This is our super webservice's API.
|
||||||
|
|
||||||
Enjoy!
|
Enjoy!
|
||||||
|
|
||||||
## GET /hello
|
## GET /hello
|
||||||
|
|
||||||
#### GET Parameters:
|
#### GET Parameters:
|
||||||
|
|
||||||
- name
|
- name
|
||||||
- **Values**: *Alp, John Doe, ...*
|
- **Values**: *Alp, John Doe, ...*
|
||||||
- **Description**: Name of the person to say hello to.
|
- **Description**: Name of the person to say hello to.
|
||||||
|
|
||||||
|
|
||||||
#### Response:
|
#### Response:
|
||||||
|
|
||||||
- Status code 200
|
- Status code 200
|
||||||
- Headers: []
|
- Headers: []
|
||||||
|
|
||||||
- Supported content types are:
|
- Supported content types are:
|
||||||
|
|
||||||
- `application/json`
|
- `application/json`
|
||||||
|
|
||||||
- When a value is provided for 'name'
|
- When a value is provided for 'name'
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{"msg":"Hello, Alp"}
|
{"msg":"Hello, Alp"}
|
||||||
```
|
```
|
||||||
|
|
||||||
- When 'name' is not specified
|
- When 'name' is not specified
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{"msg":"Hello, anonymous coward"}
|
{"msg":"Hello, anonymous coward"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## POST /marketing
|
## POST /marketing
|
||||||
|
|
||||||
#### Request:
|
#### Request:
|
||||||
|
|
||||||
- Supported content types are:
|
- Supported content types are:
|
||||||
|
|
||||||
- `application/json`
|
- `application/json`
|
||||||
|
|
||||||
- Example: `application/json`
|
- Example: `application/json`
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"}
|
{"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Response:
|
#### Response:
|
||||||
|
|
||||||
- Status code 201
|
- Status code 201
|
||||||
- Headers: []
|
- Headers: []
|
||||||
|
|
||||||
- Supported content types are:
|
- Supported content types are:
|
||||||
|
|
||||||
- `application/json`
|
- `application/json`
|
||||||
|
|
||||||
- Response body as below.
|
- Response body as below.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{"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!","to":"alp@foo.com","from":"great@company.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!","to":"alp@foo.com","from":"great@company.com"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## GET /position/:x/:y
|
## GET /position/:x/:y
|
||||||
|
|
||||||
#### Captures:
|
#### Captures:
|
||||||
|
|
||||||
- *x*: (integer) position on the x axis
|
- *x*: (integer) position on the x axis
|
||||||
- *y*: (integer) position on the y axis
|
- *y*: (integer) position on the y axis
|
||||||
|
|
||||||
#### Response:
|
#### Response:
|
||||||
|
|
||||||
- Status code 200
|
- Status code 200
|
||||||
- Headers: []
|
- Headers: []
|
||||||
|
|
||||||
- Supported content types are:
|
- Supported content types are:
|
||||||
|
|
||||||
- `application/json`
|
- `application/json`
|
||||||
|
|
||||||
- Response body as below.
|
- Response body as below.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{"x":3,"y":14}
|
{"x":3,"y":14}
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
`````````
|
||||||
|
|
||||||
However, we can also add one or more introduction sections to the document. We just need to tweak the way we generate `apiDocs`. We will also convert the content to a lazy `ByteString` since this is what *wai* expects for `Raw` endpoints.
|
However, we can also add one or more introduction sections to the document. We just need to tweak the way we generate `apiDocs`. We will also convert the content to a lazy `ByteString` since this is what **wai** expects for `Raw` endpoints.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
docsBS :: ByteString
|
docsBS :: ByteString
|
||||||
|
@ -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 and request anything 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
|
||||||
|
@ -25,8 +25,7 @@ module Server where
|
||||||
|
|
||||||
import Prelude ()
|
import Prelude ()
|
||||||
import Prelude.Compat
|
import Prelude.Compat
|
||||||
|
import Control.Monad.Except
|
||||||
import Control.Monad.IO.Class
|
|
||||||
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 +33,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 +49,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 +150,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 +187,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 +196,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,33 +292,31 @@ 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
|
||||||
{"x":1,"y":2}
|
{"xCoord":1,"yCoord":2}
|
||||||
$ curl http://localhost:8081/hello
|
$ curl http://localhost:8081/hello
|
||||||
{"msg":"Hello, anonymous coward"}
|
{"msg":"Hello, anonymous coward"}
|
||||||
$ curl http://localhost:8081/hello?name=Alp
|
$ curl http://localhost:8081/hello?name=Alp
|
||||||
{"msg":"Hello, Alp"}
|
{"msg":"Hello, Alp"}
|
||||||
$ curl -X POST -d '{"name":"Alp Mestanogullari", "email" : "alp@foo.com", "age": 25, "interested_in": ["haskell", "mathematics"]}' -H 'Accept: application/json' -H 'Content-type: application/json' http://localhost:8081/marketing
|
$ curl -X POST -d '{"clientName":"Alp Mestanogullari", "clientEmail" : "alp@foo.com", "clientAge": 25, "clientInterestedIn": ["haskell", "mathematics"]}' -H 'Accept: application/json' -H 'Content-type: application/json' http://localhost:8081/marketing
|
||||||
{"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 +325,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 +347,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 +363,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 +381,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 +405,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 +438,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 +456,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 +477,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 +501,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 +521,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 +544,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,39 +588,37 @@ 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
|
||||||
[{"lastName":"Newton","firstName":"Isaac"},{"lastName":"Einstein","firstName":"Albert"}]
|
[{"lastName":"Newton","firstName":"Isaac"},{"lastName":"Einstein","firstName":"Albert"}]
|
||||||
$ curl -H 'Accept: text/html' http://localhost:8081/persons
|
$ curl -H 'Accept: text/html' http://localhost:8081/persons
|
||||||
<table><tr><td>first name</td><td>last name</td></tr><tr><td>Isaac</td><td>Newton</td></tr><tr><td>Albert</td><td>Einstein</td></tr></table>
|
<table><tr><td>first name</td><td>last name</td></tr><tr><td>Isaac</td><td>Newton</td></tr><tr><td>Albert</td><td>Einstein</td></tr></table>
|
||||||
# or just point your browser to http://localhost:8081/persons
|
# or just point your browser to http://localhost:8081/persons
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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 +640,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 +670,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 +689,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,42 +704,41 @@ 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
|
||||||
[snip]
|
[snip]
|
||||||
* Connected to localhost (127.0.0.1) port 8081 (#0)
|
* Connected to localhost (127.0.0.1) port 8081 (#0)
|
||||||
> GET /myfile.txt HTTP/1.1
|
> GET /myfile.txt HTTP/1.1
|
||||||
> User-Agent: curl/7.30.0
|
> User-Agent: curl/7.30.0
|
||||||
> Host: localhost:8081
|
> Host: localhost:8081
|
||||||
> Accept: */*
|
> Accept: */*
|
||||||
>
|
>
|
||||||
< HTTP/1.1 404 Not Found
|
< HTTP/1.1 404 Not Found
|
||||||
[snip]
|
[snip]
|
||||||
myfile.txt just isnt there, please leave this server alone.
|
myfile.txt just isnt there, please leave this server alone.
|
||||||
|
|
||||||
$ echo Hello > myfile.txt
|
$ echo Hello > myfile.txt
|
||||||
|
|
||||||
$ curl --verbose http://localhost:8081/myfile.txt
|
$ curl --verbose http://localhost:8081/myfile.txt
|
||||||
[snip]
|
[snip]
|
||||||
* Connected to localhost (127.0.0.1) port 8081 (#0)
|
* Connected to localhost (127.0.0.1) port 8081 (#0)
|
||||||
> GET /myfile.txt HTTP/1.1
|
> GET /myfile.txt HTTP/1.1
|
||||||
> User-Agent: curl/7.30.0
|
> User-Agent: curl/7.30.0
|
||||||
> Host: localhost:8081
|
> Host: localhost:8081
|
||||||
> Accept: */*
|
> Accept: */*
|
||||||
>
|
>
|
||||||
< HTTP/1.1 200 OK
|
< HTTP/1.1 200 OK
|
||||||
[snip]
|
[snip]
|
||||||
< Content-Type: application/json
|
< Content-Type: application/json
|
||||||
[snip]
|
[snip]
|
||||||
{"content":"Hello\n"}
|
{"content":"Hello\n"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Response headers
|
## Response headers
|
||||||
|
@ -773,10 +758,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 +769,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 +1008,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 +1016,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 +1025,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 +1041,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 +1082,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 +1110,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*.
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ HEAD
|
||||||
* Added support for `path` on `BaseUrl`.
|
* Added support for `path` on `BaseUrl`.
|
||||||
* `client` now takes an explicit `Manager` argument.
|
* `client` now takes an explicit `Manager` argument.
|
||||||
* Use `http-api-data` instead of `Servant.Common.Text`
|
* Use `http-api-data` instead of `Servant.Common.Text`
|
||||||
* Client functions now consider any 2xx succesful.
|
* Client functions now consider any 2xx successful.
|
||||||
* Remove matrix params.
|
* Remove matrix params.
|
||||||
|
|
||||||
0.4.1
|
0.4.1
|
||||||
|
|
|
@ -115,7 +115,7 @@ api :: Proxy Api
|
||||||
api = Proxy
|
api = Proxy
|
||||||
|
|
||||||
server :: Application
|
server :: Application
|
||||||
server = serve api EmptyConfig (
|
server = serve api (
|
||||||
return alice
|
return alice
|
||||||
:<|> return NoContent
|
:<|> return NoContent
|
||||||
:<|> (\ name -> return $ Person name 0)
|
:<|> (\ name -> return $ Person name 0)
|
||||||
|
@ -142,7 +142,7 @@ failApi :: Proxy FailApi
|
||||||
failApi = Proxy
|
failApi = Proxy
|
||||||
|
|
||||||
failServer :: Application
|
failServer :: Application
|
||||||
failServer = serve failApi EmptyConfig (
|
failServer = serve failApi (
|
||||||
(\ _request respond -> respond $ responseLBS ok200 [] "")
|
(\ _request respond -> respond $ responseLBS ok200 [] "")
|
||||||
:<|> (\ _capture _request respond -> respond $ responseLBS ok200 [("content-type", "application/json")] "")
|
:<|> (\ _capture _request respond -> respond $ responseLBS ok200 [("content-type", "application/json")] "")
|
||||||
:<|> (\_request respond -> respond $ responseLBS ok200 [("content-type", "fooooo")] "")
|
:<|> (\_request respond -> respond $ responseLBS ok200 [("content-type", "fooooo")] "")
|
||||||
|
@ -232,7 +232,7 @@ sucessSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
|
||||||
|
|
||||||
wrappedApiSpec :: Spec
|
wrappedApiSpec :: Spec
|
||||||
wrappedApiSpec = describe "error status codes" $ do
|
wrappedApiSpec = describe "error status codes" $ do
|
||||||
let serveW api = serve api EmptyConfig $ throwE $ ServantErr 500 "error message" "" []
|
let serveW api = serve api $ throwE $ ServantErr 500 "error message" "" []
|
||||||
context "are correctly handled by the client" $
|
context "are correctly handled by the client" $
|
||||||
let test :: (WrappedApi, String) -> Spec
|
let test :: (WrappedApi, String) -> Spec
|
||||||
test (WrappedApi api, desc) =
|
test (WrappedApi api, desc) =
|
||||||
|
|
|
@ -2,3 +2,4 @@ HEAD
|
||||||
-----
|
-----
|
||||||
* Use the `text` package instead of `String`.
|
* Use the `text` package instead of `String`.
|
||||||
* Extract javascript-oblivious types and helpers to *servant-foreign*
|
* Extract javascript-oblivious types and helpers to *servant-foreign*
|
||||||
|
* Typed-languages support
|
||||||
|
|
|
@ -1,36 +1,50 @@
|
||||||
-- | Generalizes all the data needed to make code generation work with
|
-- | Generalizes all the data needed to make code generation work with
|
||||||
-- arbitrary programming languages.
|
-- arbitrary programming languages.
|
||||||
module Servant.Foreign
|
module Servant.Foreign
|
||||||
( HasForeign(..)
|
( ArgType(..)
|
||||||
, HasForeignType(..)
|
, HeaderArg(..)
|
||||||
|
, QueryArg(..)
|
||||||
|
, Req(..)
|
||||||
, Segment(..)
|
, Segment(..)
|
||||||
, SegmentType(..)
|
, SegmentType(..)
|
||||||
|
, Url(..)
|
||||||
|
-- aliases
|
||||||
|
, Path
|
||||||
|
, ForeignType
|
||||||
|
, Arg
|
||||||
, FunctionName
|
, FunctionName
|
||||||
, QueryArg(..)
|
-- lenses
|
||||||
, HeaderArg(..)
|
, reqUrl
|
||||||
, ArgType(..)
|
, reqMethod
|
||||||
, Req
|
, reqHeaders
|
||||||
|
, reqBody
|
||||||
|
, reqReturnType
|
||||||
|
, reqFuncName
|
||||||
|
, path
|
||||||
|
, queryStr
|
||||||
|
, argName
|
||||||
|
, argType
|
||||||
|
-- prisms
|
||||||
|
, _HeaderArg
|
||||||
|
, _ReplaceHeaderArg
|
||||||
|
, _Static
|
||||||
|
, _Cap
|
||||||
|
, _Normal
|
||||||
|
, _Flag
|
||||||
|
, _List
|
||||||
|
-- rest of it
|
||||||
|
, HasForeign(..)
|
||||||
|
, HasForeignType(..)
|
||||||
|
, HasNoForeignType
|
||||||
|
, GenerateList(..)
|
||||||
|
, NoTypes
|
||||||
, captureArg
|
, captureArg
|
||||||
, defReq
|
, isCapture
|
||||||
, concatCase
|
, concatCase
|
||||||
, snakeCase
|
, snakeCase
|
||||||
, camelCase
|
, camelCase
|
||||||
-- lenses
|
, defReq
|
||||||
, argType
|
|
||||||
, argName
|
|
||||||
, isCapture
|
|
||||||
, funcName
|
|
||||||
, path
|
|
||||||
, reqUrl
|
|
||||||
, reqBody
|
|
||||||
, reqHeaders
|
|
||||||
, reqMethod
|
|
||||||
, reqReturnType
|
|
||||||
, segment
|
|
||||||
, queryStr
|
|
||||||
, listFromAPI
|
, listFromAPI
|
||||||
, GenerateList(..)
|
|
||||||
, NoTypes
|
|
||||||
-- re-exports
|
-- re-exports
|
||||||
, module Servant.API
|
, module Servant.API
|
||||||
) where
|
) where
|
||||||
|
|
|
@ -19,17 +19,19 @@
|
||||||
-- arbitrary programming languages.
|
-- arbitrary programming languages.
|
||||||
module Servant.Foreign.Internal where
|
module Servant.Foreign.Internal where
|
||||||
|
|
||||||
import Control.Lens (makeLenses, (%~), (&), (.~), (<>~))
|
import Control.Lens (makeLenses, makePrisms, (%~), (&), (.~), (<>~))
|
||||||
import qualified Data.Char as C
|
import qualified Data.Char as C
|
||||||
import Data.Proxy
|
import Data.Proxy
|
||||||
import Data.Text
|
import Data.Text
|
||||||
import Data.Text.Encoding (decodeUtf8)
|
import Data.Text.Encoding (decodeUtf8)
|
||||||
import GHC.Exts (Constraint)
|
import GHC.Exts (Constraint)
|
||||||
import GHC.TypeLits
|
import GHC.TypeLits
|
||||||
import qualified Network.HTTP.Types as HTTP
|
import qualified Network.HTTP.Types as HTTP
|
||||||
import Prelude hiding (concat)
|
import Prelude hiding (concat)
|
||||||
import Servant.API
|
import Servant.API
|
||||||
|
|
||||||
|
type FunctionName = [Text]
|
||||||
|
|
||||||
-- | Function name builder that simply concat each part together
|
-- | Function name builder that simply concat each part together
|
||||||
concatCase :: FunctionName -> Text
|
concatCase :: FunctionName -> Text
|
||||||
concatCase = concat
|
concatCase = concat
|
||||||
|
@ -49,36 +51,50 @@ camelCase = camelCase' . Prelude.map (replace "-" "")
|
||||||
capitalize name = C.toUpper (Data.Text.head name) `cons` Data.Text.tail name
|
capitalize name = C.toUpper (Data.Text.head name) `cons` Data.Text.tail name
|
||||||
|
|
||||||
type ForeignType = Text
|
type ForeignType = Text
|
||||||
|
|
||||||
type Arg = (Text, ForeignType)
|
type Arg = (Text, ForeignType)
|
||||||
|
|
||||||
newtype Segment = Segment { _segment :: SegmentType }
|
data SegmentType
|
||||||
|
= Static Text
|
||||||
|
-- ^ a static path segment. like "/foo"
|
||||||
|
| Cap Arg
|
||||||
|
-- ^ a capture. like "/:userid"
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
data SegmentType = Static Text -- ^ a static path segment. like "/foo"
|
makePrisms ''SegmentType
|
||||||
| Cap Arg -- ^ a capture. like "/:userid"
|
|
||||||
|
newtype Segment = Segment { unSegment :: SegmentType }
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
makePrisms ''Segment
|
||||||
|
|
||||||
type Path = [Segment]
|
type Path = [Segment]
|
||||||
|
|
||||||
data ArgType =
|
data ArgType
|
||||||
Normal
|
= Normal
|
||||||
| Flag
|
| Flag
|
||||||
| List
|
| List
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
makePrisms ''ArgType
|
||||||
|
|
||||||
data QueryArg = QueryArg
|
data QueryArg = QueryArg
|
||||||
{ _argName :: Arg
|
{ _argName :: Arg
|
||||||
, _argType :: ArgType
|
, _argType :: ArgType
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
data HeaderArg = HeaderArg
|
makeLenses ''QueryArg
|
||||||
{ headerArg :: Arg
|
|
||||||
}
|
|
||||||
| ReplaceHeaderArg
|
|
||||||
{ headerArg :: Arg
|
|
||||||
, headerPattern :: Text
|
|
||||||
} deriving (Eq, Show)
|
|
||||||
|
|
||||||
|
data HeaderArg = HeaderArg
|
||||||
|
{ headerArg :: Arg }
|
||||||
|
| ReplaceHeaderArg
|
||||||
|
{ headerArg :: Arg
|
||||||
|
, headerPattern :: Text
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
makeLenses ''HeaderArg
|
||||||
|
|
||||||
|
makePrisms ''HeaderArg
|
||||||
|
|
||||||
data Url = Url
|
data Url = Url
|
||||||
{ _path :: Path
|
{ _path :: Path
|
||||||
|
@ -88,7 +104,7 @@ data Url = Url
|
||||||
defUrl :: Url
|
defUrl :: Url
|
||||||
defUrl = Url [] []
|
defUrl = Url [] []
|
||||||
|
|
||||||
type FunctionName = [Text]
|
makeLenses ''Url
|
||||||
|
|
||||||
data Req = Req
|
data Req = Req
|
||||||
{ _reqUrl :: Url
|
{ _reqUrl :: Url
|
||||||
|
@ -96,12 +112,9 @@ data Req = Req
|
||||||
, _reqHeaders :: [HeaderArg]
|
, _reqHeaders :: [HeaderArg]
|
||||||
, _reqBody :: Maybe ForeignType
|
, _reqBody :: Maybe ForeignType
|
||||||
, _reqReturnType :: ForeignType
|
, _reqReturnType :: ForeignType
|
||||||
, _funcName :: FunctionName
|
, _reqFuncName :: FunctionName
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
makeLenses ''QueryArg
|
|
||||||
makeLenses ''Segment
|
|
||||||
makeLenses ''Url
|
|
||||||
makeLenses ''Req
|
makeLenses ''Req
|
||||||
|
|
||||||
isCapture :: Segment -> Bool
|
isCapture :: Segment -> Bool
|
||||||
|
@ -155,66 +168,66 @@ type family Elem (a :: *) (ls::[*]) :: Constraint where
|
||||||
-- >
|
-- >
|
||||||
--
|
--
|
||||||
class HasForeignType lang a where
|
class HasForeignType lang a where
|
||||||
typeFor :: Proxy lang -> Proxy a -> ForeignType
|
typeFor :: Proxy lang -> Proxy a -> ForeignType
|
||||||
|
|
||||||
data NoTypes
|
data NoTypes
|
||||||
|
|
||||||
instance HasForeignType NoTypes a where
|
instance HasForeignType NoTypes ftype where
|
||||||
typeFor _ _ = empty
|
typeFor _ _ = empty
|
||||||
|
|
||||||
|
type HasNoForeignType = HasForeignType NoTypes
|
||||||
|
|
||||||
class HasForeign lang (layout :: *) where
|
class HasForeign lang (layout :: *) where
|
||||||
type Foreign layout :: *
|
type Foreign layout :: *
|
||||||
foreignFor :: Proxy lang -> Proxy layout -> Req -> Foreign layout
|
foreignFor :: Proxy lang -> Proxy layout -> Req -> Foreign layout
|
||||||
|
|
||||||
instance (HasForeign lang a, HasForeign lang b)
|
instance (HasForeign lang a, HasForeign lang b)
|
||||||
=> HasForeign lang (a :<|> b) where
|
=> HasForeign lang (a :<|> b) where
|
||||||
type Foreign (a :<|> b) = Foreign a :<|> Foreign b
|
type Foreign (a :<|> b) = Foreign a :<|> Foreign b
|
||||||
|
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
foreignFor lang (Proxy :: Proxy a) req
|
foreignFor lang (Proxy :: Proxy a) req
|
||||||
:<|> foreignFor lang (Proxy :: Proxy b) req
|
:<|> foreignFor lang (Proxy :: Proxy b) req
|
||||||
|
|
||||||
instance (KnownSymbol sym, HasForeignType lang a, HasForeign lang sublayout)
|
instance (KnownSymbol sym, HasForeignType lang ftype, HasForeign lang sublayout)
|
||||||
=> HasForeign lang (Capture sym a :> sublayout) where
|
=> HasForeign lang (Capture sym ftype :> sublayout) where
|
||||||
type Foreign (Capture sym a :> sublayout) = Foreign sublayout
|
type Foreign (Capture sym a :> sublayout) = Foreign sublayout
|
||||||
|
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
foreignFor lang (Proxy :: Proxy sublayout) $
|
foreignFor lang (Proxy :: Proxy sublayout) $
|
||||||
req & reqUrl.path <>~ [Segment (Cap arg)]
|
req & reqUrl.path <>~ [Segment (Cap arg)]
|
||||||
& funcName %~ (++ ["by", str])
|
& reqFuncName %~ (++ ["by", str])
|
||||||
|
|
||||||
where
|
where
|
||||||
str = pack . symbolVal $ (Proxy :: Proxy sym)
|
str = pack . symbolVal $ (Proxy :: Proxy sym)
|
||||||
arg = (str, typeFor lang (Proxy :: Proxy a))
|
arg = (str, typeFor lang (Proxy :: Proxy ftype))
|
||||||
|
|
||||||
instance (Elem JSON list, HasForeignType lang a, ReflectMethod method)
|
instance (Elem JSON list, HasForeignType lang a, ReflectMethod method)
|
||||||
=> HasForeign lang (Verb method status list a) where
|
=> HasForeign lang (Verb method status list a) where
|
||||||
type Foreign (Verb method status list a) = Req
|
type Foreign (Verb method status list a) = Req
|
||||||
|
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
req & funcName %~ (methodLC :)
|
req & reqFuncName %~ (methodLC :)
|
||||||
& reqMethod .~ method
|
& reqMethod .~ method
|
||||||
& reqReturnType .~ retType
|
& reqReturnType .~ retType
|
||||||
where
|
where
|
||||||
retType = typeFor lang (Proxy :: Proxy a)
|
retType = typeFor lang (Proxy :: Proxy a)
|
||||||
method = reflectMethod (Proxy :: Proxy method)
|
method = reflectMethod (Proxy :: Proxy method)
|
||||||
methodLC = toLower $ decodeUtf8 method
|
methodLC = toLower $ decodeUtf8 method
|
||||||
|
|
||||||
instance (KnownSymbol sym, HasForeignType lang a, HasForeign lang sublayout)
|
instance (KnownSymbol sym, HasForeignType lang a, HasForeign lang sublayout)
|
||||||
=> HasForeign lang (Header sym a :> sublayout) where
|
=> HasForeign lang (Header sym a :> sublayout) where
|
||||||
type Foreign (Header sym a :> sublayout) = Foreign sublayout
|
type Foreign (Header sym a :> sublayout) = Foreign sublayout
|
||||||
|
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
foreignFor lang subP $ req
|
foreignFor lang subP $ req
|
||||||
& reqHeaders <>~ [HeaderArg arg]
|
& reqHeaders <>~ [HeaderArg arg]
|
||||||
|
|
||||||
where
|
where
|
||||||
hname = pack . symbolVal $ (Proxy :: Proxy sym)
|
hname = pack . symbolVal $ (Proxy :: Proxy sym)
|
||||||
arg = (hname, typeFor lang (Proxy :: Proxy a))
|
arg = (hname, typeFor lang (Proxy :: Proxy a))
|
||||||
subP = Proxy :: Proxy sublayout
|
subP = Proxy :: Proxy sublayout
|
||||||
|
|
||||||
instance (KnownSymbol sym, HasForeignType lang a, HasForeign lang sublayout)
|
instance (KnownSymbol sym, HasForeignType lang a, HasForeign lang sublayout)
|
||||||
=> HasForeign lang (QueryParam sym a :> sublayout) where
|
=> HasForeign lang (QueryParam sym a :> sublayout) where
|
||||||
type Foreign (QueryParam sym a :> sublayout) = Foreign sublayout
|
type Foreign (QueryParam sym a :> sublayout) = Foreign sublayout
|
||||||
|
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
|
@ -222,38 +235,37 @@ instance (KnownSymbol sym, HasForeignType lang a, HasForeign lang sublayout)
|
||||||
req & reqUrl.queryStr <>~ [QueryArg arg Normal]
|
req & reqUrl.queryStr <>~ [QueryArg arg Normal]
|
||||||
|
|
||||||
where
|
where
|
||||||
str = pack . symbolVal $ (Proxy :: Proxy sym)
|
str = pack . symbolVal $ (Proxy :: Proxy sym)
|
||||||
arg = (str, typeFor lang (Proxy :: Proxy a))
|
arg = (str, typeFor lang (Proxy :: Proxy a))
|
||||||
|
|
||||||
instance (KnownSymbol sym, HasForeignType lang [a], HasForeign lang sublayout)
|
instance
|
||||||
=> HasForeign lang (QueryParams sym a :> sublayout) where
|
(KnownSymbol sym, HasForeignType lang [a], HasForeign lang sublayout)
|
||||||
|
=> HasForeign lang (QueryParams sym a :> sublayout) where
|
||||||
type Foreign (QueryParams sym a :> sublayout) = Foreign sublayout
|
type Foreign (QueryParams sym a :> sublayout) = Foreign sublayout
|
||||||
|
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
foreignFor lang (Proxy :: Proxy sublayout) $
|
foreignFor lang (Proxy :: Proxy sublayout) $
|
||||||
req & reqUrl.queryStr <>~ [QueryArg arg List]
|
req & reqUrl.queryStr <>~ [QueryArg arg List]
|
||||||
|
|
||||||
where
|
where
|
||||||
str = pack . symbolVal $ (Proxy :: Proxy sym)
|
str = pack . symbolVal $ (Proxy :: Proxy sym)
|
||||||
arg = (str, typeFor lang (Proxy :: Proxy [a]))
|
arg = (str, typeFor lang (Proxy :: Proxy [a]))
|
||||||
|
|
||||||
instance (KnownSymbol sym, HasForeignType lang a, a ~ Bool, HasForeign lang sublayout)
|
instance
|
||||||
=> HasForeign lang (QueryFlag sym :> sublayout) where
|
(KnownSymbol sym, HasForeignType lang Bool, HasForeign lang sublayout)
|
||||||
|
=> HasForeign lang (QueryFlag sym :> sublayout) where
|
||||||
type Foreign (QueryFlag sym :> sublayout) = Foreign sublayout
|
type Foreign (QueryFlag sym :> sublayout) = Foreign sublayout
|
||||||
|
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
foreignFor lang (Proxy :: Proxy sublayout) $
|
foreignFor lang (Proxy :: Proxy sublayout) $
|
||||||
req & reqUrl.queryStr <>~ [QueryArg arg Flag]
|
req & reqUrl.queryStr <>~ [QueryArg arg Flag]
|
||||||
|
|
||||||
where
|
where
|
||||||
str = pack . symbolVal $ (Proxy :: Proxy sym)
|
str = pack . symbolVal $ (Proxy :: Proxy sym)
|
||||||
arg = (str, typeFor lang (Proxy :: Proxy a))
|
arg = (str, typeFor lang (Proxy :: Proxy Bool))
|
||||||
|
|
||||||
instance HasForeign lang Raw where
|
instance HasForeign lang Raw where
|
||||||
type Foreign Raw = HTTP.Method -> Req
|
type Foreign Raw = HTTP.Method -> Req
|
||||||
|
|
||||||
foreignFor _ Proxy req method =
|
foreignFor _ Proxy req method =
|
||||||
req & funcName %~ ((toLower $ decodeUtf8 method) :)
|
req & reqFuncName %~ ((toLower $ decodeUtf8 method) :)
|
||||||
& reqMethod .~ method
|
& reqMethod .~ method
|
||||||
|
|
||||||
instance (Elem JSON list, HasForeignType lang a, HasForeign lang sublayout)
|
instance (Elem JSON list, HasForeignType lang a, HasForeign lang sublayout)
|
||||||
|
@ -271,19 +283,21 @@ instance (KnownSymbol path, HasForeign lang sublayout)
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
foreignFor lang (Proxy :: Proxy sublayout) $
|
foreignFor lang (Proxy :: Proxy sublayout) $
|
||||||
req & reqUrl.path <>~ [Segment (Static str)]
|
req & reqUrl.path <>~ [Segment (Static str)]
|
||||||
& funcName %~ (++ [str])
|
& reqFuncName %~ (++ [str])
|
||||||
|
|
||||||
where
|
where
|
||||||
str = Data.Text.map (\c -> if c == '.' then '_' else c)
|
str =
|
||||||
. pack . symbolVal $ (Proxy :: Proxy path)
|
Data.Text.map (\c -> if c == '.' then '_' else c)
|
||||||
|
. pack . symbolVal $ (Proxy :: Proxy path)
|
||||||
|
|
||||||
instance HasForeign lang sublayout => HasForeign lang (RemoteHost :> sublayout) where
|
instance HasForeign lang sublayout
|
||||||
|
=> HasForeign lang (RemoteHost :> sublayout) where
|
||||||
type Foreign (RemoteHost :> sublayout) = Foreign sublayout
|
type Foreign (RemoteHost :> sublayout) = Foreign sublayout
|
||||||
|
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
foreignFor lang (Proxy :: Proxy sublayout) req
|
foreignFor lang (Proxy :: Proxy sublayout) req
|
||||||
|
|
||||||
instance HasForeign lang sublayout => HasForeign lang (IsSecure :> sublayout) where
|
instance HasForeign lang sublayout
|
||||||
|
=> HasForeign lang (IsSecure :> sublayout) where
|
||||||
type Foreign (IsSecure :> sublayout) = Foreign sublayout
|
type Foreign (IsSecure :> sublayout) = Foreign sublayout
|
||||||
|
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
|
@ -302,7 +316,8 @@ instance HasForeign lang sublayout =>
|
||||||
|
|
||||||
foreignFor lang Proxy = foreignFor lang (Proxy :: Proxy sublayout)
|
foreignFor lang Proxy = foreignFor lang (Proxy :: Proxy sublayout)
|
||||||
|
|
||||||
instance HasForeign lang sublayout => HasForeign lang (HttpVersion :> sublayout) where
|
instance HasForeign lang sublayout
|
||||||
|
=> HasForeign lang (HttpVersion :> sublayout) where
|
||||||
type Foreign (HttpVersion :> sublayout) = Foreign sublayout
|
type Foreign (HttpVersion :> sublayout) = Foreign sublayout
|
||||||
|
|
||||||
foreignFor lang Proxy req =
|
foreignFor lang Proxy req =
|
||||||
|
@ -317,10 +332,15 @@ class GenerateList reqs where
|
||||||
instance GenerateList Req where
|
instance GenerateList Req where
|
||||||
generateList r = [r]
|
generateList r = [r]
|
||||||
|
|
||||||
instance (GenerateList start, GenerateList rest) => GenerateList (start :<|> rest) where
|
instance (GenerateList start, GenerateList rest)
|
||||||
|
=> GenerateList (start :<|> rest) where
|
||||||
generateList (start :<|> rest) = (generateList start) ++ (generateList rest)
|
generateList (start :<|> rest) = (generateList start) ++ (generateList rest)
|
||||||
|
|
||||||
-- | Generate the necessary data for codegen as a list, each 'Req'
|
-- | Generate the necessary data for codegen as a list, each 'Req'
|
||||||
-- describing one endpoint from your API type.
|
-- describing one endpoint from your API type.
|
||||||
listFromAPI :: (HasForeign lang api, GenerateList (Foreign api)) => Proxy lang -> Proxy api -> [Req]
|
listFromAPI
|
||||||
|
:: (HasForeign lang api, GenerateList (Foreign api))
|
||||||
|
=> Proxy lang
|
||||||
|
-> Proxy api
|
||||||
|
-> [Req]
|
||||||
listFromAPI lang p = generateList (foreignFor lang p defReq)
|
listFromAPI lang p = generateList (foreignFor lang p defReq)
|
||||||
|
|
|
@ -15,7 +15,6 @@ module Servant.ForeignSpec where
|
||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
import Data.Proxy
|
import Data.Proxy
|
||||||
import Servant.Foreign
|
import Servant.Foreign
|
||||||
import Servant.Foreign.Internal
|
|
||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
|
@ -35,15 +34,19 @@ camelCaseSpec = describe "camelCase" $ do
|
||||||
data LangX
|
data LangX
|
||||||
|
|
||||||
instance HasForeignType LangX () where
|
instance HasForeignType LangX () where
|
||||||
typeFor _ _ = "voidX"
|
typeFor _ _ = "voidX"
|
||||||
|
|
||||||
instance HasForeignType LangX Int where
|
instance HasForeignType LangX Int where
|
||||||
typeFor _ _ = "intX"
|
typeFor _ _ = "intX"
|
||||||
|
|
||||||
instance HasForeignType LangX Bool where
|
instance HasForeignType LangX Bool where
|
||||||
typeFor _ _ = "boolX"
|
typeFor _ _ = "boolX"
|
||||||
|
|
||||||
instance OVERLAPPING_ HasForeignType LangX String where
|
instance OVERLAPPING_ HasForeignType LangX String where
|
||||||
typeFor _ _ = "stringX"
|
typeFor _ _ = "stringX"
|
||||||
|
|
||||||
instance OVERLAPPABLE_ HasForeignType LangX a => HasForeignType LangX [a] where
|
instance OVERLAPPABLE_ HasForeignType LangX a => HasForeignType LangX [a] where
|
||||||
typeFor lang _ = "listX of " <> typeFor lang (Proxy :: Proxy a)
|
typeFor lang _ = "listX of " <> typeFor lang (Proxy :: Proxy a)
|
||||||
|
|
||||||
type TestApi
|
type TestApi
|
||||||
= "test" :> Header "header" [String] :> QueryFlag "flag" :> Get '[JSON] Int
|
= "test" :> Header "header" [String] :> QueryFlag "flag" :> Get '[JSON] Int
|
||||||
|
@ -56,58 +59,57 @@ testApi = listFromAPI (Proxy :: Proxy LangX) (Proxy :: Proxy TestApi)
|
||||||
|
|
||||||
listFromAPISpec :: Spec
|
listFromAPISpec :: Spec
|
||||||
listFromAPISpec = describe "listFromAPI" $ do
|
listFromAPISpec = describe "listFromAPI" $ do
|
||||||
it "generates 4 endpoints for TestApi" $ do
|
it "generates 4 endpoints for TestApi" $ do
|
||||||
length testApi `shouldBe` 4
|
length testApi `shouldBe` 4
|
||||||
|
|
||||||
let [getReq, postReq, putReq, deleteReq] = testApi
|
let [getReq, postReq, putReq, deleteReq] = testApi
|
||||||
|
|
||||||
it "collects all info for get request" $ do
|
it "collects all info for get request" $ do
|
||||||
shouldBe getReq $ defReq
|
shouldBe getReq $ defReq
|
||||||
{ _reqUrl = Url
|
{ _reqUrl = Url
|
||||||
[ Segment $ Static "test" ]
|
[ Segment $ Static "test" ]
|
||||||
[ QueryArg ("flag", "boolX") Flag ]
|
[ QueryArg ("flag", "boolX") Flag ]
|
||||||
, _reqMethod = "GET"
|
, _reqMethod = "GET"
|
||||||
, _reqHeaders = [HeaderArg ("header", "listX of stringX")]
|
, _reqHeaders = [HeaderArg ("header", "listX of stringX")]
|
||||||
, _reqBody = Nothing
|
, _reqBody = Nothing
|
||||||
, _reqReturnType = "intX"
|
, _reqReturnType = "intX"
|
||||||
, _funcName = ["get", "test"]
|
, _reqFuncName = ["get", "test"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "collects all info for post request" $ do
|
it "collects all info for post request" $ do
|
||||||
shouldBe postReq $ defReq
|
shouldBe postReq $ defReq
|
||||||
{ _reqUrl = Url
|
{ _reqUrl = Url
|
||||||
[ Segment $ Static "test" ]
|
[ Segment $ Static "test" ]
|
||||||
[ QueryArg ("param", "intX") Normal ]
|
[ QueryArg ("param", "intX") Normal ]
|
||||||
, _reqMethod = "POST"
|
, _reqMethod = "POST"
|
||||||
, _reqHeaders = []
|
, _reqHeaders = []
|
||||||
, _reqBody = Just "listX of stringX"
|
, _reqBody = Just "listX of stringX"
|
||||||
, _reqReturnType = "voidX"
|
, _reqReturnType = "voidX"
|
||||||
, _funcName = ["post", "test"]
|
, _reqFuncName = ["post", "test"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "collects all info for put request" $ do
|
it "collects all info for put request" $ do
|
||||||
shouldBe putReq $ defReq
|
shouldBe putReq $ defReq
|
||||||
{ _reqUrl = Url
|
{ _reqUrl = Url
|
||||||
[ Segment $ Static "test" ]
|
[ Segment $ Static "test" ]
|
||||||
-- Shoud this be |intX| or |listX of intX| ?
|
-- Shoud this be |intX| or |listX of intX| ?
|
||||||
[ QueryArg ("params", "listX of intX") List ]
|
[ QueryArg ("params", "listX of intX") List ]
|
||||||
, _reqMethod = "PUT"
|
, _reqMethod = "PUT"
|
||||||
, _reqHeaders = []
|
, _reqHeaders = []
|
||||||
, _reqBody = Just "stringX"
|
, _reqBody = Just "stringX"
|
||||||
, _reqReturnType = "voidX"
|
, _reqReturnType = "voidX"
|
||||||
, _funcName = ["put", "test"]
|
, _reqFuncName = ["put", "test"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "collects all info for delete request" $ do
|
|
||||||
shouldBe deleteReq $ defReq
|
|
||||||
{ _reqUrl = Url
|
|
||||||
[ Segment $ Static "test"
|
|
||||||
, Segment $ Cap ("id", "intX") ]
|
|
||||||
[]
|
|
||||||
, _reqMethod = "DELETE"
|
|
||||||
, _reqHeaders = []
|
|
||||||
, _reqBody = Nothing
|
|
||||||
, _reqReturnType = "voidX"
|
|
||||||
, _funcName = ["delete", "test", "by", "id"]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
it "collects all info for delete request" $ do
|
||||||
|
shouldBe deleteReq $ defReq
|
||||||
|
{ _reqUrl = Url
|
||||||
|
[ Segment $ Static "test"
|
||||||
|
, Segment $ Cap ("id", "intX") ]
|
||||||
|
[]
|
||||||
|
, _reqMethod = "DELETE"
|
||||||
|
, _reqHeaders = []
|
||||||
|
, _reqBody = Nothing
|
||||||
|
, _reqReturnType = "voidX"
|
||||||
|
, _reqFuncName = ["delete", "test", "by", "id"]
|
||||||
|
}
|
||||||
|
|
|
@ -128,7 +128,7 @@ generateAngularJSWith ngOptions opts req = "\n" <>
|
||||||
|
|
||||||
fsep = if hasService then ":" else " ="
|
fsep = if hasService then ":" else " ="
|
||||||
|
|
||||||
fname = namespace <> (functionNameBuilder opts $ req ^. funcName)
|
fname = namespace <> (functionNameBuilder opts $ req ^. reqFuncName)
|
||||||
|
|
||||||
method = req ^. reqMethod
|
method = req ^. reqMethod
|
||||||
url = if url' == "'" then "'/'" else url'
|
url = if url' == "'" then "'/'" else url'
|
||||||
|
|
|
@ -116,7 +116,7 @@ generateAxiosJSWith aopts opts req = "\n" <>
|
||||||
where
|
where
|
||||||
hasNoModule = moduleName opts == ""
|
hasNoModule = moduleName opts == ""
|
||||||
|
|
||||||
fname = namespace <> (functionNameBuilder opts $ req ^. funcName)
|
fname = namespace <> (functionNameBuilder opts $ req ^. reqFuncName)
|
||||||
|
|
||||||
method = T.toLower . decodeUtf8 $ req ^. reqMethod
|
method = T.toLower . decodeUtf8 $ req ^. reqMethod
|
||||||
url = if url' == "'" then "'/'" else url'
|
url = if url' == "'" then "'/'" else url'
|
||||||
|
|
|
@ -51,12 +51,19 @@ type JavaScriptGenerator = [Req] -> Text
|
||||||
-- customize the output
|
-- customize the output
|
||||||
data CommonGeneratorOptions = CommonGeneratorOptions
|
data CommonGeneratorOptions = CommonGeneratorOptions
|
||||||
{
|
{
|
||||||
functionNameBuilder :: FunctionName -> Text -- ^ function generating function names
|
functionNameBuilder :: FunctionName -> Text
|
||||||
, requestBody :: Text -- ^ name used when a user want to send the request body (to let you redefine it)
|
-- ^ function generating function names
|
||||||
, successCallback :: Text -- ^ name of the callback parameter when the request was successful
|
, requestBody :: Text
|
||||||
, errorCallback :: Text -- ^ name of the callback parameter when the request reported an error
|
-- ^ name used when a user want to send the request body
|
||||||
, moduleName :: Text -- ^ namespace on which we define the foreign function (empty mean local var)
|
-- (to let you redefine it)
|
||||||
, urlPrefix :: Text -- ^ a prefix we should add to the Url in the codegen
|
, successCallback :: Text
|
||||||
|
-- ^ name of the callback parameter when the request was successful
|
||||||
|
, errorCallback :: Text
|
||||||
|
-- ^ name of the callback parameter when the request reported an error
|
||||||
|
, moduleName :: Text
|
||||||
|
-- ^ namespace on which we define the foreign function (empty mean local var)
|
||||||
|
, urlPrefix :: Text
|
||||||
|
-- ^ a prefix we should add to the Url in the codegen
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Default options.
|
-- | Default options.
|
||||||
|
|
|
@ -81,7 +81,7 @@ generateJQueryJSWith opts req = "\n" <>
|
||||||
namespace = if (moduleName opts) == ""
|
namespace = if (moduleName opts) == ""
|
||||||
then "var "
|
then "var "
|
||||||
else (moduleName opts) <> "."
|
else (moduleName opts) <> "."
|
||||||
fname = namespace <> (functionNameBuilder opts $ req ^. funcName)
|
fname = namespace <> (functionNameBuilder opts $ req ^. reqFuncName)
|
||||||
|
|
||||||
method = req ^. reqMethod
|
method = req ^. reqMethod
|
||||||
url = if url' == "'" then "'/'" else url'
|
url = if url' == "'" then "'/'" else url'
|
||||||
|
|
|
@ -93,7 +93,7 @@ generateVanillaJSWith opts req = "\n" <>
|
||||||
namespace = if moduleName opts == ""
|
namespace = if moduleName opts == ""
|
||||||
then "var "
|
then "var "
|
||||||
else (moduleName opts) <> "."
|
else (moduleName opts) <> "."
|
||||||
fname = namespace <> (functionNameBuilder opts $ req ^. funcName)
|
fname = namespace <> (functionNameBuilder opts $ req ^. reqFuncName)
|
||||||
|
|
||||||
method = req ^. reqMethod
|
method = req ^. reqMethod
|
||||||
url = if url' == "'" then "'/'" else url'
|
url = if url' == "'" then "'/'" else url'
|
||||||
|
|
|
@ -20,4 +20,4 @@ api :: Proxy API
|
||||||
api = Proxy
|
api = Proxy
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = run 8080 (serve api EmptyConfig $ mock api Proxy)
|
main = run 8080 (serve api $ mock api Proxy)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
{-# LANGUAGE DataKinds #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE PolyKinds #-}
|
{-# LANGUAGE PolyKinds #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE TypeOperators #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
#include "overlapping-compat.h"
|
#include "overlapping-compat.h"
|
||||||
|
@ -67,7 +67,6 @@ import Network.HTTP.Types.Status
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
import Servant
|
import Servant
|
||||||
import Servant.API.ContentTypes
|
import Servant.API.ContentTypes
|
||||||
import Servant.Server.Internal.Config
|
|
||||||
import Test.QuickCheck.Arbitrary (Arbitrary (..), vector)
|
import Test.QuickCheck.Arbitrary (Arbitrary (..), vector)
|
||||||
import Test.QuickCheck.Gen (Gen, generate)
|
import Test.QuickCheck.Gen (Gen, generate)
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ spec = do
|
||||||
context "Get" $ do
|
context "Get" $ do
|
||||||
let api :: Proxy (Get '[JSON] Body)
|
let api :: Proxy (Get '[JSON] Body)
|
||||||
api = Proxy
|
api = Proxy
|
||||||
app = serve api EmptyConfig (mock api Proxy)
|
app = serve api (mock api Proxy)
|
||||||
with (return app) $ do
|
with (return app) $ do
|
||||||
it "serves arbitrary response bodies" $ do
|
it "serves arbitrary response bodies" $ do
|
||||||
get "/" `shouldRespondWith` 200{
|
get "/" `shouldRespondWith` 200{
|
||||||
|
@ -65,7 +65,7 @@ spec = do
|
||||||
withoutHeader :: Proxy (Get '[JSON] (Headers '[] Body))
|
withoutHeader :: Proxy (Get '[JSON] (Headers '[] Body))
|
||||||
withoutHeader = Proxy
|
withoutHeader = Proxy
|
||||||
toApp :: (HasMock api '[]) => Proxy api -> IO Application
|
toApp :: (HasMock api '[]) => Proxy api -> IO Application
|
||||||
toApp api = return $ serve api EmptyConfig (mock api (Proxy :: Proxy '[]))
|
toApp api = return $ serve api (mock api (Proxy :: Proxy '[]))
|
||||||
with (toApp withHeader) $ do
|
with (toApp withHeader) $ do
|
||||||
it "serves arbitrary response bodies" $ do
|
it "serves arbitrary response bodies" $ do
|
||||||
get "/" `shouldRespondWith` 200{
|
get "/" `shouldRespondWith` 200{
|
||||||
|
|
|
@ -59,7 +59,7 @@ server = helloH :<|> postGreetH :<|> deleteGreetH
|
||||||
-- Turn the server into a WAI app. 'serve' is provided by servant,
|
-- Turn the server into a WAI app. 'serve' is provided by servant,
|
||||||
-- more precisely by the Servant.Server module.
|
-- more precisely by the Servant.Server module.
|
||||||
test :: Application
|
test :: Application
|
||||||
test = serve testApi EmptyConfig server
|
test = serve testApi server
|
||||||
|
|
||||||
-- Run the server.
|
-- Run the server.
|
||||||
--
|
--
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{-# LANGUAGE ConstraintKinds #-}
|
{-# LANGUAGE ConstraintKinds #-}
|
||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
@ -8,6 +9,7 @@
|
||||||
module Servant.Server
|
module Servant.Server
|
||||||
( -- * Run a wai application from an API
|
( -- * Run a wai application from an API
|
||||||
serve
|
serve
|
||||||
|
, serveWithConfig
|
||||||
|
|
||||||
, -- * Construct a wai Application from an API
|
, -- * Construct a wai Application from an API
|
||||||
toApplication
|
toApplication
|
||||||
|
@ -104,18 +106,18 @@ import Servant.Server.Internal.Enter
|
||||||
-- > myApi :: Proxy MyApi
|
-- > myApi :: Proxy MyApi
|
||||||
-- > myApi = Proxy
|
-- > myApi = Proxy
|
||||||
-- >
|
-- >
|
||||||
-- > config :: Config '[]
|
|
||||||
-- > config = EmptyConfig
|
|
||||||
-- >
|
|
||||||
-- > app :: Application
|
-- > app :: Application
|
||||||
-- > app = serve myApi config server
|
-- > app = serve myApi server
|
||||||
-- >
|
-- >
|
||||||
-- > main :: IO ()
|
-- > main :: IO ()
|
||||||
-- > main = Network.Wai.Handler.Warp.run 8080 app
|
-- > main = Network.Wai.Handler.Warp.run 8080 app
|
||||||
--
|
--
|
||||||
serve :: (HasServer layout config)
|
serve :: (HasServer layout '[]) => Proxy layout -> Server layout -> Application
|
||||||
|
serve p = serveWithConfig p EmptyConfig
|
||||||
|
|
||||||
|
serveWithConfig :: (HasServer layout config)
|
||||||
=> Proxy layout -> Config config -> Server layout -> Application
|
=> Proxy layout -> Config config -> Server layout -> Application
|
||||||
serve p config server = toApplication (runRouter (route p config d))
|
serveWithConfig p config server = toApplication (runRouter (route p config d))
|
||||||
where
|
where
|
||||||
d = Delayed r r r (\ _ _ -> Route server)
|
d = Delayed r r r (\ _ _ -> Route server)
|
||||||
r = return (Route ())
|
r = return (Route ())
|
||||||
|
|
|
@ -158,7 +158,7 @@ methodCheck method request
|
||||||
acceptCheck :: (AllMime list) => Proxy list -> B.ByteString -> IO (RouteResult ())
|
acceptCheck :: (AllMime list) => Proxy list -> B.ByteString -> IO (RouteResult ())
|
||||||
acceptCheck proxy accH
|
acceptCheck proxy accH
|
||||||
| canHandleAcceptH proxy (AcceptHeader accH) = return $ Route ()
|
| canHandleAcceptH proxy (AcceptHeader accH) = return $ Route ()
|
||||||
| otherwise = return $ Fail err406
|
| otherwise = return $ FailFatal err406
|
||||||
|
|
||||||
methodRouter :: (AllCTRender ctypes a)
|
methodRouter :: (AllCTRender ctypes a)
|
||||||
=> Method -> Proxy ctypes -> Status
|
=> Method -> Proxy ctypes -> Status
|
||||||
|
|
|
@ -11,14 +11,8 @@ module Servant.Server.Internal.RoutingApplication where
|
||||||
import Control.Applicative ((<$>))
|
import Control.Applicative ((<$>))
|
||||||
#endif
|
#endif
|
||||||
import Control.Monad.Trans.Except (ExceptT, runExceptT)
|
import Control.Monad.Trans.Except (ExceptT, runExceptT)
|
||||||
import qualified Data.ByteString as B
|
|
||||||
import qualified Data.ByteString.Lazy as BL
|
|
||||||
import Data.IORef (newIORef, readIORef,
|
|
||||||
writeIORef)
|
|
||||||
import Network.Wai (Application, Request,
|
import Network.Wai (Application, Request,
|
||||||
Response, ResponseReceived,
|
Response, ResponseReceived)
|
||||||
requestBody,
|
|
||||||
strictRequestBody)
|
|
||||||
import Servant.Server.Internal.ServantErr
|
import Servant.Server.Internal.ServantErr
|
||||||
|
|
||||||
type RoutingApplication =
|
type RoutingApplication =
|
||||||
|
@ -33,34 +27,8 @@ data RouteResult a =
|
||||||
| Route !a
|
| Route !a
|
||||||
deriving (Eq, Show, Read, Functor)
|
deriving (Eq, Show, Read, Functor)
|
||||||
|
|
||||||
data ReqBodyState = Uncalled
|
|
||||||
| Called !B.ByteString
|
|
||||||
| Done !B.ByteString
|
|
||||||
|
|
||||||
toApplication :: RoutingApplication -> Application
|
toApplication :: RoutingApplication -> Application
|
||||||
toApplication ra request respond = do
|
toApplication ra request respond = ra request routingRespond
|
||||||
reqBodyRef <- newIORef Uncalled
|
|
||||||
-- We may need to consume the requestBody more than once. In order to
|
|
||||||
-- maintain the illusion that 'requestBody' works as expected,
|
|
||||||
-- 'ReqBodyState' is introduced, and the complete body is memoized and
|
|
||||||
-- returned as many times as requested with empty "Done" marker chunks in
|
|
||||||
-- between.
|
|
||||||
-- See https://github.com/haskell-servant/servant/issues/3
|
|
||||||
let memoReqBody = do
|
|
||||||
ior <- readIORef reqBodyRef
|
|
||||||
case ior of
|
|
||||||
Uncalled -> do
|
|
||||||
r <- BL.toStrict <$> strictRequestBody request
|
|
||||||
writeIORef reqBodyRef $ Done r
|
|
||||||
return r
|
|
||||||
Called bs -> do
|
|
||||||
writeIORef reqBodyRef $ Done bs
|
|
||||||
return bs
|
|
||||||
Done bs -> do
|
|
||||||
writeIORef reqBodyRef $ Called bs
|
|
||||||
return B.empty
|
|
||||||
|
|
||||||
ra request{ requestBody = memoReqBody } routingRespond
|
|
||||||
where
|
where
|
||||||
routingRespond :: RouteResult Response -> IO ResponseReceived
|
routingRespond :: RouteResult Response -> IO ResponseReceived
|
||||||
routingRespond (Fail err) = respond $ responseServantErr err
|
routingRespond (Fail err) = respond $ responseServantErr err
|
||||||
|
@ -98,10 +66,10 @@ toApplication ra request respond = do
|
||||||
--
|
--
|
||||||
-- There are two reasons:
|
-- There are two reasons:
|
||||||
--
|
--
|
||||||
-- 1. Currently, the order in which we perform checks coincides
|
-- 1. In a straight-forward implementation, the order in which we
|
||||||
-- with the error we will generate. This is because during checks,
|
-- perform checks will determine the error we generate. This is
|
||||||
-- once an error occurs, we do not perform any subsequent checks,
|
-- because once an error occurs, we would abort and not perform
|
||||||
-- but rather return this error.
|
-- any subsequent checks, but rather return the current error.
|
||||||
--
|
--
|
||||||
-- This is not a necessity: we could continue doing other checks,
|
-- This is not a necessity: we could continue doing other checks,
|
||||||
-- and choose the preferred error. However, that would in general
|
-- and choose the preferred error. However, that would in general
|
||||||
|
@ -159,7 +127,7 @@ data Delayed :: * -> * where
|
||||||
-> Delayed c
|
-> Delayed c
|
||||||
|
|
||||||
instance Functor Delayed where
|
instance Functor Delayed where
|
||||||
fmap f (Delayed a b c g) = Delayed a b c ((fmap.fmap.fmap) f g)
|
fmap f (Delayed a b c g) = Delayed a b c ((fmap . fmap . fmap) f g)
|
||||||
|
|
||||||
-- | Add a capture to the end of the capture block.
|
-- | Add a capture to the end of the capture block.
|
||||||
addCapture :: Delayed (a -> b)
|
addCapture :: Delayed (a -> b)
|
||||||
|
@ -240,9 +208,9 @@ runAction :: Delayed (ExceptT ServantErr IO a)
|
||||||
-> IO r
|
-> IO r
|
||||||
runAction action respond k = runDelayed action >>= go >>= respond
|
runAction action respond k = runDelayed action >>= go >>= respond
|
||||||
where
|
where
|
||||||
go (Fail e) = return $ Fail e
|
go (Fail e) = return $ Fail e
|
||||||
go (FailFatal e) = return $ FailFatal e
|
go (FailFatal e) = return $ FailFatal e
|
||||||
go (Route a) = do
|
go (Route a) = do
|
||||||
e <- runExceptT a
|
e <- runExceptT a
|
||||||
case e of
|
case e of
|
||||||
Left err -> return . Route $ responseServantErr err
|
Left err -> return . Route $ responseServantErr err
|
||||||
|
|
|
@ -42,7 +42,7 @@ errorOrderServer = \_ _ -> throwE err402
|
||||||
|
|
||||||
errorOrderSpec :: Spec
|
errorOrderSpec :: Spec
|
||||||
errorOrderSpec = describe "HTTP error order"
|
errorOrderSpec = describe "HTTP error order"
|
||||||
$ with (return $ serve errorOrderApi EmptyConfig errorOrderServer) $ do
|
$ with (return $ serve errorOrderApi errorOrderServer) $ do
|
||||||
let badContentType = (hContentType, "text/plain")
|
let badContentType = (hContentType, "text/plain")
|
||||||
badAccept = (hAccept, "text/plain")
|
badAccept = (hAccept, "text/plain")
|
||||||
badMethod = methodGet
|
badMethod = methodGet
|
||||||
|
@ -89,7 +89,7 @@ prioErrorsApi = Proxy
|
||||||
prioErrorsSpec :: Spec
|
prioErrorsSpec :: Spec
|
||||||
prioErrorsSpec = describe "PrioErrors" $ do
|
prioErrorsSpec = describe "PrioErrors" $ do
|
||||||
let server = return
|
let server = return
|
||||||
with (return $ serve prioErrorsApi EmptyConfig server) $ do
|
with (return $ serve prioErrorsApi server) $ do
|
||||||
let check (mdescr, method) path (cdescr, ctype, body) resp =
|
let check (mdescr, method) path (cdescr, ctype, body) resp =
|
||||||
it fulldescr $
|
it fulldescr $
|
||||||
Test.Hspec.Wai.request method path [(hContentType, ctype)] body
|
Test.Hspec.Wai.request method path [(hContentType, ctype)] body
|
||||||
|
@ -154,7 +154,7 @@ errorRetryServer
|
||||||
|
|
||||||
errorRetrySpec :: Spec
|
errorRetrySpec :: Spec
|
||||||
errorRetrySpec = describe "Handler search"
|
errorRetrySpec = describe "Handler search"
|
||||||
$ with (return $ serve errorRetryApi EmptyConfig errorRetryServer) $ do
|
$ with (return $ serve errorRetryApi errorRetryServer) $ do
|
||||||
|
|
||||||
let jsonCT = (hContentType, "application/json")
|
let jsonCT = (hContentType, "application/json")
|
||||||
jsonAccept = (hAccept, "application/json")
|
jsonAccept = (hAccept, "application/json")
|
||||||
|
@ -168,6 +168,10 @@ errorRetrySpec = describe "Handler search"
|
||||||
request methodGet "a" [jsonCT, jsonAccept] jsonBody
|
request methodGet "a" [jsonCT, jsonAccept] jsonBody
|
||||||
`shouldRespondWith` 200 { matchBody = Just $ encode (4 :: Int) }
|
`shouldRespondWith` 200 { matchBody = Just $ encode (4 :: Int) }
|
||||||
|
|
||||||
|
it "should not continue when body cannot be decoded" $ do
|
||||||
|
request methodPost "a" [jsonCT, jsonAccept] "a string"
|
||||||
|
`shouldRespondWith` 400
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
-- * Error Choice {{{
|
-- * Error Choice {{{
|
||||||
|
@ -194,7 +198,7 @@ errorChoiceServer = return 0
|
||||||
|
|
||||||
errorChoiceSpec :: Spec
|
errorChoiceSpec :: Spec
|
||||||
errorChoiceSpec = describe "Multiple handlers return errors"
|
errorChoiceSpec = describe "Multiple handlers return errors"
|
||||||
$ with (return $ serve errorChoiceApi EmptyConfig errorChoiceServer) $ do
|
$ with (return $ serve errorChoiceApi errorChoiceServer) $ do
|
||||||
|
|
||||||
it "should respond with 404 if no path matches" $ do
|
it "should respond with 404 if no path matches" $ do
|
||||||
request methodGet "" [] "" `shouldRespondWith` 404
|
request methodGet "" [] "" `shouldRespondWith` 404
|
||||||
|
|
|
@ -48,12 +48,12 @@ combinedReaderServer = enter fReader combinedReaderServer'
|
||||||
|
|
||||||
enterSpec :: Spec
|
enterSpec :: Spec
|
||||||
enterSpec = describe "Enter" $ do
|
enterSpec = describe "Enter" $ do
|
||||||
with (return (serve readerAPI EmptyConfig readerServer)) $ do
|
with (return (serve readerAPI readerServer)) $ do
|
||||||
|
|
||||||
it "allows running arbitrary monads" $ do
|
it "allows running arbitrary monads" $ do
|
||||||
get "int" `shouldRespondWith` "1797"
|
get "int" `shouldRespondWith` "1797"
|
||||||
post "string" "3" `shouldRespondWith` "\"hi\""{ matchStatus = 200 }
|
post "string" "3" `shouldRespondWith` "\"hi\""{ matchStatus = 200 }
|
||||||
|
|
||||||
with (return (serve combinedAPI EmptyConfig combinedReaderServer)) $ do
|
with (return (serve combinedAPI combinedReaderServer)) $ do
|
||||||
it "allows combnation of enters" $ do
|
it "allows combnation of enters" $ do
|
||||||
get "bool" `shouldRespondWith` "true"
|
get "bool" `shouldRespondWith` "true"
|
||||||
|
|
|
@ -30,7 +30,7 @@ testServer s = return s
|
||||||
|
|
||||||
oneEntryApp :: Application
|
oneEntryApp :: Application
|
||||||
oneEntryApp =
|
oneEntryApp =
|
||||||
serve (Proxy :: Proxy OneEntryAPI) config testServer
|
serveWithConfig (Proxy :: Proxy OneEntryAPI) config testServer
|
||||||
where
|
where
|
||||||
config :: Config '[String]
|
config :: Config '[String]
|
||||||
config = "configEntry" :. EmptyConfig
|
config = "configEntry" :. EmptyConfig
|
||||||
|
@ -40,7 +40,7 @@ type OneEntryTwiceAPI =
|
||||||
"bar" :> ExtractFromConfig :> Get '[JSON] String
|
"bar" :> ExtractFromConfig :> Get '[JSON] String
|
||||||
|
|
||||||
oneEntryTwiceApp :: Application
|
oneEntryTwiceApp :: Application
|
||||||
oneEntryTwiceApp = serve (Proxy :: Proxy OneEntryTwiceAPI) config $
|
oneEntryTwiceApp = serveWithConfig (Proxy :: Proxy OneEntryTwiceAPI) config $
|
||||||
testServer :<|>
|
testServer :<|>
|
||||||
testServer
|
testServer
|
||||||
where
|
where
|
||||||
|
@ -68,7 +68,7 @@ type InjectAPI =
|
||||||
Get '[JSON] String
|
Get '[JSON] String
|
||||||
|
|
||||||
injectApp :: Application
|
injectApp :: Application
|
||||||
injectApp = serve (Proxy :: Proxy InjectAPI) config $
|
injectApp = serveWithConfig (Proxy :: Proxy InjectAPI) config $
|
||||||
(\ s -> return s) :<|>
|
(\ s -> return s) :<|>
|
||||||
(\ s -> return ("tagged: " ++ s))
|
(\ s -> return ("tagged: " ++ s))
|
||||||
where
|
where
|
||||||
|
@ -90,7 +90,7 @@ type WithBirdfaceAPI =
|
||||||
"bar" :> ExtractFromConfig :> Get '[JSON] String
|
"bar" :> ExtractFromConfig :> Get '[JSON] String
|
||||||
|
|
||||||
withBirdfaceApp :: Application
|
withBirdfaceApp :: Application
|
||||||
withBirdfaceApp = serve (Proxy :: Proxy WithBirdfaceAPI) config $
|
withBirdfaceApp = serveWithConfig (Proxy :: Proxy WithBirdfaceAPI) config $
|
||||||
testServer :<|>
|
testServer :<|>
|
||||||
testServer
|
testServer
|
||||||
where
|
where
|
||||||
|
@ -112,7 +112,7 @@ type NamedConfigAPI =
|
||||||
ExtractFromConfig :> Get '[JSON] String)
|
ExtractFromConfig :> Get '[JSON] String)
|
||||||
|
|
||||||
namedConfigApp :: Application
|
namedConfigApp :: Application
|
||||||
namedConfigApp = serve (Proxy :: Proxy NamedConfigAPI) config return
|
namedConfigApp = serveWithConfig (Proxy :: Proxy NamedConfigAPI) config return
|
||||||
where
|
where
|
||||||
config :: Config '[NamedConfig "sub" '[String]]
|
config :: Config '[NamedConfig "sub" '[String]]
|
||||||
config = NamedConfig ("descend" :. EmptyConfig) :. EmptyConfig
|
config = NamedConfig ("descend" :. EmptyConfig) :. EmptyConfig
|
||||||
|
|
|
@ -49,7 +49,7 @@ import Servant.API ((:<|>) (..), (:>), Capture, Delete,
|
||||||
StdMethod (..), Verb, addHeader)
|
StdMethod (..), Verb, addHeader)
|
||||||
import Servant.API.Internal.Test.ComprehensiveAPI
|
import Servant.API.Internal.Test.ComprehensiveAPI
|
||||||
import Servant.Server (ServantErr (..), Server, err404,
|
import Servant.Server (ServantErr (..), Server, err404,
|
||||||
serve, Config(EmptyConfig))
|
serve, serveWithConfig, Config(EmptyConfig))
|
||||||
import Test.Hspec (Spec, context, describe, it,
|
import Test.Hspec (Spec, context, describe, it,
|
||||||
shouldBe, shouldContain)
|
shouldBe, shouldContain)
|
||||||
import Test.Hspec.Wai (get, liftIO, matchHeaders,
|
import Test.Hspec.Wai (get, liftIO, matchHeaders,
|
||||||
|
@ -67,7 +67,7 @@ import Servant.Server.Internal.Config
|
||||||
-- * comprehensive api test
|
-- * comprehensive api test
|
||||||
|
|
||||||
-- This declaration simply checks that all instances are in place.
|
-- This declaration simply checks that all instances are in place.
|
||||||
_ = serve comprehensiveAPI comprehensiveApiConfig
|
_ = serveWithConfig comprehensiveAPI comprehensiveApiConfig
|
||||||
|
|
||||||
comprehensiveApiConfig :: Config '[NamedConfig "foo" '[]]
|
comprehensiveApiConfig :: Config '[NamedConfig "foo" '[]]
|
||||||
comprehensiveApiConfig = NamedConfig EmptyConfig :. EmptyConfig
|
comprehensiveApiConfig = NamedConfig EmptyConfig :. EmptyConfig
|
||||||
|
@ -112,7 +112,7 @@ verbSpec = describe "Servant.API.Verb" $ do
|
||||||
wrongMethod m = if m == methodPatch then methodPost else methodPatch
|
wrongMethod m = if m == methodPatch then methodPost else methodPatch
|
||||||
test desc api method (status :: Int) = context desc $
|
test desc api method (status :: Int) = context desc $
|
||||||
|
|
||||||
with (return $ serve api EmptyConfig server) $ do
|
with (return $ serve api server) $ do
|
||||||
|
|
||||||
-- HEAD and 214/215 need not return bodies
|
-- HEAD and 214/215 need not return bodies
|
||||||
unless (status `elem` [214, 215] || method == methodHead) $
|
unless (status `elem` [214, 215] || method == methodHead) $
|
||||||
|
@ -187,7 +187,7 @@ captureServer legs = case legs of
|
||||||
captureSpec :: Spec
|
captureSpec :: Spec
|
||||||
captureSpec = do
|
captureSpec = do
|
||||||
describe "Servant.API.Capture" $ do
|
describe "Servant.API.Capture" $ do
|
||||||
with (return (serve captureApi EmptyConfig captureServer)) $ do
|
with (return (serve captureApi captureServer)) $ do
|
||||||
|
|
||||||
it "can capture parts of the 'pathInfo'" $ do
|
it "can capture parts of the 'pathInfo'" $ do
|
||||||
response <- get "/2"
|
response <- get "/2"
|
||||||
|
@ -198,7 +198,6 @@ captureSpec = do
|
||||||
|
|
||||||
with (return (serve
|
with (return (serve
|
||||||
(Proxy :: Proxy (Capture "captured" String :> Raw))
|
(Proxy :: Proxy (Capture "captured" String :> Raw))
|
||||||
EmptyConfig
|
|
||||||
(\ "captured" request_ respond ->
|
(\ "captured" request_ respond ->
|
||||||
respond $ responseLBS ok200 [] (cs $ show $ pathInfo request_)))) $ do
|
respond $ responseLBS ok200 [] (cs $ show $ pathInfo request_)))) $ do
|
||||||
it "strips the captured path snippet from pathInfo" $ do
|
it "strips the captured path snippet from pathInfo" $ do
|
||||||
|
@ -232,7 +231,7 @@ queryParamSpec :: Spec
|
||||||
queryParamSpec = do
|
queryParamSpec = do
|
||||||
describe "Servant.API.QueryParam" $ do
|
describe "Servant.API.QueryParam" $ do
|
||||||
it "allows retrieving simple GET parameters" $
|
it "allows retrieving simple GET parameters" $
|
||||||
(flip runSession) (serve queryParamApi EmptyConfig qpServer) $ do
|
(flip runSession) (serve queryParamApi qpServer) $ do
|
||||||
let params1 = "?name=bob"
|
let params1 = "?name=bob"
|
||||||
response1 <- Network.Wai.Test.request defaultRequest{
|
response1 <- Network.Wai.Test.request defaultRequest{
|
||||||
rawQueryString = params1,
|
rawQueryString = params1,
|
||||||
|
@ -244,7 +243,7 @@ queryParamSpec = do
|
||||||
}
|
}
|
||||||
|
|
||||||
it "allows retrieving lists in GET parameters" $
|
it "allows retrieving lists in GET parameters" $
|
||||||
(flip runSession) (serve queryParamApi EmptyConfig qpServer) $ do
|
(flip runSession) (serve queryParamApi qpServer) $ do
|
||||||
let params2 = "?names[]=bob&names[]=john"
|
let params2 = "?names[]=bob&names[]=john"
|
||||||
response2 <- Network.Wai.Test.request defaultRequest{
|
response2 <- Network.Wai.Test.request defaultRequest{
|
||||||
rawQueryString = params2,
|
rawQueryString = params2,
|
||||||
|
@ -258,7 +257,7 @@ queryParamSpec = do
|
||||||
|
|
||||||
|
|
||||||
it "allows retrieving value-less GET parameters" $
|
it "allows retrieving value-less GET parameters" $
|
||||||
(flip runSession) (serve queryParamApi EmptyConfig qpServer) $ do
|
(flip runSession) (serve queryParamApi qpServer) $ do
|
||||||
let params3 = "?capitalize"
|
let params3 = "?capitalize"
|
||||||
response3 <- Network.Wai.Test.request defaultRequest{
|
response3 <- Network.Wai.Test.request defaultRequest{
|
||||||
rawQueryString = params3,
|
rawQueryString = params3,
|
||||||
|
@ -310,7 +309,7 @@ reqBodySpec = describe "Servant.API.ReqBody" $ do
|
||||||
mkReq method x = Test.Hspec.Wai.request method x
|
mkReq method x = Test.Hspec.Wai.request method x
|
||||||
[(hContentType, "application/json;charset=utf-8")]
|
[(hContentType, "application/json;charset=utf-8")]
|
||||||
|
|
||||||
with (return $ serve reqBodyApi EmptyConfig server) $ do
|
with (return $ serve reqBodyApi server) $ do
|
||||||
|
|
||||||
it "passes the argument to the handler" $ do
|
it "passes the argument to the handler" $ do
|
||||||
response <- mkReq methodPost "" (encode alice)
|
response <- mkReq methodPost "" (encode alice)
|
||||||
|
@ -343,13 +342,13 @@ headerSpec = describe "Servant.API.Header" $ do
|
||||||
expectsString (Just x) = when (x /= "more from you") $ error "Expected more from you"
|
expectsString (Just x) = when (x /= "more from you") $ error "Expected more from you"
|
||||||
expectsString Nothing = error "Expected a string"
|
expectsString Nothing = error "Expected a string"
|
||||||
|
|
||||||
with (return (serve headerApi EmptyConfig expectsInt)) $ do
|
with (return (serve headerApi expectsInt)) $ do
|
||||||
let delete' x = Test.Hspec.Wai.request methodDelete x [("MyHeader", "5")]
|
let delete' x = Test.Hspec.Wai.request methodDelete x [("MyHeader", "5")]
|
||||||
|
|
||||||
it "passes the header to the handler (Int)" $
|
it "passes the header to the handler (Int)" $
|
||||||
delete' "/" "" `shouldRespondWith` 200
|
delete' "/" "" `shouldRespondWith` 200
|
||||||
|
|
||||||
with (return (serve headerApi EmptyConfig expectsString)) $ do
|
with (return (serve headerApi expectsString)) $ do
|
||||||
let delete' x = Test.Hspec.Wai.request methodDelete x [("MyHeader", "more from you")]
|
let delete' x = Test.Hspec.Wai.request methodDelete x [("MyHeader", "more from you")]
|
||||||
|
|
||||||
it "passes the header to the handler (String)" $
|
it "passes the header to the handler (String)" $
|
||||||
|
@ -373,7 +372,7 @@ rawSpec :: Spec
|
||||||
rawSpec = do
|
rawSpec = do
|
||||||
describe "Servant.API.Raw" $ do
|
describe "Servant.API.Raw" $ do
|
||||||
it "runs applications" $ do
|
it "runs applications" $ do
|
||||||
(flip runSession) (serve rawApi EmptyConfig (rawApplication (const (42 :: Integer)))) $ do
|
(flip runSession) (serve rawApi (rawApplication (const (42 :: Integer)))) $ do
|
||||||
response <- Network.Wai.Test.request defaultRequest{
|
response <- Network.Wai.Test.request defaultRequest{
|
||||||
pathInfo = ["foo"]
|
pathInfo = ["foo"]
|
||||||
}
|
}
|
||||||
|
@ -381,7 +380,7 @@ rawSpec = do
|
||||||
simpleBody response `shouldBe` "42"
|
simpleBody response `shouldBe` "42"
|
||||||
|
|
||||||
it "gets the pathInfo modified" $ do
|
it "gets the pathInfo modified" $ do
|
||||||
(flip runSession) (serve rawApi EmptyConfig (rawApplication pathInfo)) $ do
|
(flip runSession) (serve rawApi (rawApplication pathInfo)) $ do
|
||||||
response <- Network.Wai.Test.request defaultRequest{
|
response <- Network.Wai.Test.request defaultRequest{
|
||||||
pathInfo = ["foo", "bar"]
|
pathInfo = ["foo", "bar"]
|
||||||
}
|
}
|
||||||
|
@ -415,7 +414,7 @@ alternativeServer =
|
||||||
alternativeSpec :: Spec
|
alternativeSpec :: Spec
|
||||||
alternativeSpec = do
|
alternativeSpec = do
|
||||||
describe "Servant.API.Alternative" $ do
|
describe "Servant.API.Alternative" $ do
|
||||||
with (return $ serve alternativeApi EmptyConfig alternativeServer) $ do
|
with (return $ serve alternativeApi alternativeServer) $ do
|
||||||
|
|
||||||
it "unions endpoints" $ do
|
it "unions endpoints" $ do
|
||||||
response <- get "/foo"
|
response <- get "/foo"
|
||||||
|
@ -450,7 +449,7 @@ responseHeadersServer = let h = return $ addHeader 5 $ addHeader "kilroy" "hi"
|
||||||
|
|
||||||
responseHeadersSpec :: Spec
|
responseHeadersSpec :: Spec
|
||||||
responseHeadersSpec = describe "ResponseHeaders" $ do
|
responseHeadersSpec = describe "ResponseHeaders" $ do
|
||||||
with (return $ serve (Proxy :: Proxy ResponseHeadersApi) EmptyConfig responseHeadersServer) $ do
|
with (return $ serve (Proxy :: Proxy ResponseHeadersApi) responseHeadersServer) $ do
|
||||||
|
|
||||||
let methods = [methodGet, methodPost, methodPut, methodPatch]
|
let methods = [methodGet, methodPost, methodPut, methodPatch]
|
||||||
|
|
||||||
|
@ -516,7 +515,7 @@ miscServ = versionHandler
|
||||||
hostHandler = return . show
|
hostHandler = return . show
|
||||||
|
|
||||||
miscCombinatorSpec :: Spec
|
miscCombinatorSpec :: Spec
|
||||||
miscCombinatorSpec = with (return $ serve miscApi EmptyConfig miscServ) $
|
miscCombinatorSpec = with (return $ serve miscApi miscServ) $
|
||||||
describe "Misc. combinators for request inspection" $ do
|
describe "Misc. combinators for request inspection" $ do
|
||||||
it "Successfully gets the HTTP version specified in the request" $
|
it "Successfully gets the HTTP version specified in the request" $
|
||||||
go "/version" "\"HTTP/1.0\""
|
go "/version" "\"HTTP/1.0\""
|
||||||
|
|
|
@ -16,7 +16,7 @@ import Test.Hspec (Spec, around_, describe, it)
|
||||||
import Test.Hspec.Wai (get, shouldRespondWith, with)
|
import Test.Hspec.Wai (get, shouldRespondWith, with)
|
||||||
|
|
||||||
import Servant.API ((:<|>) ((:<|>)), Capture, Get, Raw, (:>), JSON)
|
import Servant.API ((:<|>) ((:<|>)), Capture, Get, Raw, (:>), JSON)
|
||||||
import Servant.Server (Server, serve, Config(EmptyConfig))
|
import Servant.Server (Server, serve)
|
||||||
import Servant.ServerSpec (Person (Person))
|
import Servant.ServerSpec (Person (Person))
|
||||||
import Servant.Utils.StaticFiles (serveDirectory)
|
import Servant.Utils.StaticFiles (serveDirectory)
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ api :: Proxy Api
|
||||||
api = Proxy
|
api = Proxy
|
||||||
|
|
||||||
app :: Application
|
app :: Application
|
||||||
app = serve api EmptyConfig server
|
app = serve api server
|
||||||
|
|
||||||
server :: Server Api
|
server :: Server Api
|
||||||
server =
|
server =
|
||||||
|
|
|
@ -34,15 +34,15 @@ data Verb (method :: k1) (statusCode :: Nat) (contentTypes :: [*]) a
|
||||||
-- the relevant information is summarily presented here.
|
-- the relevant information is summarily presented here.
|
||||||
|
|
||||||
-- | 'GET' with 200 status code.
|
-- | 'GET' with 200 status code.
|
||||||
type Get contentTypes a = Verb 'GET 200 contentTypes a
|
type Get = Verb 'GET 200
|
||||||
-- | 'POST' with 200 status code.
|
-- | 'POST' with 200 status code.
|
||||||
type Post contentTypes a = Verb 'POST 200 contentTypes a
|
type Post = Verb 'POST 200
|
||||||
-- | 'PUT' with 200 status code.
|
-- | 'PUT' with 200 status code.
|
||||||
type Put contentTypes a = Verb 'PUT 200 contentTypes a
|
type Put = Verb 'PUT 200
|
||||||
-- | 'DELETE' with 200 status code.
|
-- | 'DELETE' with 200 status code.
|
||||||
type Delete contentTypes a = Verb 'DELETE 200 contentTypes a
|
type Delete = Verb 'DELETE 200
|
||||||
-- | 'PATCH' with 200 status code.
|
-- | 'PATCH' with 200 status code.
|
||||||
type Patch contentTypes a = Verb 'PATCH 200 contentTypes a
|
type Patch = Verb 'PATCH 200
|
||||||
|
|
||||||
-- * Other responses
|
-- * Other responses
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ type Patch contentTypes a = Verb 'PATCH 200 contentTypes a
|
||||||
|
|
||||||
-- | 'POST' with 201 status code.
|
-- | 'POST' with 201 status code.
|
||||||
--
|
--
|
||||||
type PostCreated contentTypes a = Verb 'POST 201 contentTypes a
|
type PostCreated = Verb 'POST 201
|
||||||
|
|
||||||
|
|
||||||
-- ** 202 Accepted
|
-- ** 202 Accepted
|
||||||
|
@ -69,15 +69,15 @@ type PostCreated contentTypes a = Verb 'POST 201 contentTypes a
|
||||||
-- estimate of when the processing will be finished.
|
-- estimate of when the processing will be finished.
|
||||||
|
|
||||||
-- | 'GET' with 202 status code.
|
-- | 'GET' with 202 status code.
|
||||||
type GetAccepted contentTypes a = Verb 'GET 202 contentTypes a
|
type GetAccepted = Verb 'GET 202
|
||||||
-- | 'POST' with 202 status code.
|
-- | 'POST' with 202 status code.
|
||||||
type PostAccepted contentTypes a = Verb 'POST 202 contentTypes a
|
type PostAccepted = Verb 'POST 202
|
||||||
-- | 'DELETE' with 202 status code.
|
-- | 'DELETE' with 202 status code.
|
||||||
type DeleteAccepted contentTypes a = Verb 'DELETE 202 contentTypes a
|
type DeleteAccepted = Verb 'DELETE 202
|
||||||
-- | 'PATCH' with 202 status code.
|
-- | 'PATCH' with 202 status code.
|
||||||
type PatchAccepted contentTypes a = Verb 'PATCH 202 contentTypes a
|
type PatchAccepted = Verb 'PATCH 202
|
||||||
-- | 'PUT' with 202 status code.
|
-- | 'PUT' with 202 status code.
|
||||||
type PutAccepted contentTypes a = Verb 'PUT 202 contentTypes a
|
type PutAccepted = Verb 'PUT 202
|
||||||
|
|
||||||
|
|
||||||
-- ** 203 Non-Authoritative Information
|
-- ** 203 Non-Authoritative Information
|
||||||
|
@ -86,15 +86,15 @@ type PutAccepted contentTypes a = Verb 'PUT 202 contentTypes a
|
||||||
-- information may come from a third-party.
|
-- information may come from a third-party.
|
||||||
|
|
||||||
-- | 'GET' with 203 status code.
|
-- | 'GET' with 203 status code.
|
||||||
type GetNonAuthoritative contentTypes a = Verb 'GET 203 contentTypes a
|
type GetNonAuthoritative = Verb 'GET 203
|
||||||
-- | 'POST' with 203 status code.
|
-- | 'POST' with 203 status code.
|
||||||
type PostNonAuthoritative contentTypes a = Verb 'POST 203 contentTypes a
|
type PostNonAuthoritative = Verb 'POST 203
|
||||||
-- | 'DELETE' with 203 status code.
|
-- | 'DELETE' with 203 status code.
|
||||||
type DeleteNonAuthoritative contentTypes a = Verb 'DELETE 203 contentTypes a
|
type DeleteNonAuthoritative = Verb 'DELETE 203
|
||||||
-- | 'PATCH' with 203 status code.
|
-- | 'PATCH' with 203 status code.
|
||||||
type PatchNonAuthoritative contentTypes a = Verb 'PATCH 203 contentTypes a
|
type PatchNonAuthoritative = Verb 'PATCH 203
|
||||||
-- | 'PUT' with 203 status code.
|
-- | 'PUT' with 203 status code.
|
||||||
type PutNonAuthoritative contentTypes a = Verb 'PUT 203 contentTypes a
|
type PutNonAuthoritative = Verb 'PUT 203
|
||||||
|
|
||||||
|
|
||||||
-- ** 204 No Content
|
-- ** 204 No Content
|
||||||
|
@ -105,15 +105,15 @@ type PutNonAuthoritative contentTypes a = Verb 'PUT 203 contentTypes a
|
||||||
-- If the document view should be reset, use @205 Reset Content@.
|
-- If the document view should be reset, use @205 Reset Content@.
|
||||||
|
|
||||||
-- | 'GET' with 204 status code.
|
-- | 'GET' with 204 status code.
|
||||||
type GetNoContent contentTypes noContent = Verb 'GET 204 contentTypes noContent
|
type GetNoContent = Verb 'GET 204
|
||||||
-- | 'POST' with 204 status code.
|
-- | 'POST' with 204 status code.
|
||||||
type PostNoContent contentTypes noContent = Verb 'POST 204 contentTypes noContent
|
type PostNoContent = Verb 'POST 204
|
||||||
-- | 'DELETE' with 204 status code.
|
-- | 'DELETE' with 204 status code.
|
||||||
type DeleteNoContent contentTypes noContent = Verb 'DELETE 204 contentTypes noContent
|
type DeleteNoContent = Verb 'DELETE 204
|
||||||
-- | 'PATCH' with 204 status code.
|
-- | 'PATCH' with 204 status code.
|
||||||
type PatchNoContent contentTypes noContent = Verb 'PATCH 204 contentTypes noContent
|
type PatchNoContent = Verb 'PATCH 204
|
||||||
-- | 'PUT' with 204 status code.
|
-- | 'PUT' with 204 status code.
|
||||||
type PutNoContent contentTypes noContent = Verb 'PUT 204 contentTypes noContent
|
type PutNoContent = Verb 'PUT 204
|
||||||
|
|
||||||
|
|
||||||
-- ** 205 Reset Content
|
-- ** 205 Reset Content
|
||||||
|
@ -124,15 +124,15 @@ type PutNoContent contentTypes noContent = Verb 'PUT 204 contentTypes noContent
|
||||||
-- If the document view should not be reset, use @204 No Content@.
|
-- If the document view should not be reset, use @204 No Content@.
|
||||||
|
|
||||||
-- | 'GET' with 205 status code.
|
-- | 'GET' with 205 status code.
|
||||||
type GetResetContent contentTypes noContent = Verb 'GET 205 contentTypes noContent
|
type GetResetContent = Verb 'GET 205
|
||||||
-- | 'POST' with 205 status code.
|
-- | 'POST' with 205 status code.
|
||||||
type PostResetContent contentTypes noContent = Verb 'POST 205 contentTypes noContent
|
type PostResetContent = Verb 'POST 205
|
||||||
-- | 'DELETE' with 205 status code.
|
-- | 'DELETE' with 205 status code.
|
||||||
type DeleteResetContent contentTypes noContent = Verb 'DELETE 205 contentTypes noContent
|
type DeleteResetContent = Verb 'DELETE 205
|
||||||
-- | 'PATCH' with 205 status code.
|
-- | 'PATCH' with 205 status code.
|
||||||
type PatchResetContent contentTypes noContent = Verb 'PATCH 205 contentTypes noContent
|
type PatchResetContent = Verb 'PATCH 205
|
||||||
-- | 'PUT' with 205 status code.
|
-- | 'PUT' with 205 status code.
|
||||||
type PutResetContent contentTypes noContent = Verb 'PUT 205 contentTypes noContent
|
type PutResetContent = Verb 'PUT 205
|
||||||
|
|
||||||
|
|
||||||
-- ** 206 Partial Content
|
-- ** 206 Partial Content
|
||||||
|
@ -144,7 +144,7 @@ type PutResetContent contentTypes noContent = Verb 'PUT 205 contentTypes noConte
|
||||||
-- RFC7233 Section 4.1>
|
-- RFC7233 Section 4.1>
|
||||||
|
|
||||||
-- | 'GET' with 206 status code.
|
-- | 'GET' with 206 status code.
|
||||||
type GetPartialContent contentTypes noContent = Verb 'GET 206 contentTypes noContent
|
type GetPartialContent = Verb 'GET 206
|
||||||
|
|
||||||
|
|
||||||
class ReflectMethod a where
|
class ReflectMethod a where
|
||||||
|
|
Loading…
Reference in a new issue