Remove bird-tracks
This commit is contained in:
parent
ef2a44d11f
commit
8997a669c8
5 changed files with 886 additions and 740 deletions
|
@ -6,13 +6,15 @@ toc: true
|
||||||
The source for this tutorial section is a literate haskell file, so first we
|
The source for this tutorial section is a literate haskell file, so first we
|
||||||
need to have some language extensions and imports:
|
need to have some language extensions and imports:
|
||||||
|
|
||||||
> {-# LANGUAGE DataKinds #-}
|
``` haskell
|
||||||
> {-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
>
|
{-# LANGUAGE TypeOperators #-}
|
||||||
> module ApiType where
|
|
||||||
>
|
module ApiType where
|
||||||
> import Data.Text
|
|
||||||
> import Servant.API
|
import Data.Text
|
||||||
|
import Servant.API
|
||||||
|
```
|
||||||
|
|
||||||
Consider the following informal specification of an API:
|
Consider the following informal specification of an API:
|
||||||
|
|
||||||
|
@ -29,14 +31,16 @@ getting some client libraries, and documentation (and in the future, who knows
|
||||||
How would we describe it with servant? As mentioned earlier, an endpoint
|
How would we describe it with servant? As mentioned earlier, an endpoint
|
||||||
description is a good old Haskell **type**:
|
description is a good old Haskell **type**:
|
||||||
|
|
||||||
> type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User]
|
``` haskell
|
||||||
>
|
type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User]
|
||||||
> data SortBy = Age | Name
|
|
||||||
>
|
data SortBy = Age | Name
|
||||||
> data User = User {
|
|
||||||
> name :: String,
|
data User = User {
|
||||||
> age :: Int
|
name :: String,
|
||||||
> }
|
age :: Int
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Let's break that down:
|
Let's break that down:
|
||||||
|
|
||||||
|
@ -61,8 +65,10 @@ equivalent to `/`, but sometimes it just lets you chain another combinator.
|
||||||
We can also describe APIs with multiple endpoints by using the `:<|>`
|
We can also describe APIs with multiple endpoints by using the `:<|>`
|
||||||
combinators. Here's an example:
|
combinators. Here's an example:
|
||||||
|
|
||||||
> type UserAPI2 = "users" :> "list-all" :> Get '[JSON] [User]
|
``` haskell
|
||||||
> :<|> "list-all" :> "users" :> Get '[JSON] [User]
|
type UserAPI2 = "users" :> "list-all" :> Get '[JSON] [User]
|
||||||
|
:<|> "list-all" :> "users" :> Get '[JSON] [User]
|
||||||
|
```
|
||||||
|
|
||||||
*servant* provides a fair amount of combinators out-of-the-box, but you can
|
*servant* provides a fair amount of combinators out-of-the-box, but you can
|
||||||
always write your own when you need it. Here's a quick overview of all the
|
always write your own when you need it. Here's a quick overview of all the
|
||||||
|
@ -78,9 +84,11 @@ As you've already seen, you can use type-level strings (enabled with the
|
||||||
`DataKinds` language extension) for static path fragments. Chaining
|
`DataKinds` language extension) for static path fragments. Chaining
|
||||||
them amounts to `/`-separating them in a URL.
|
them amounts to `/`-separating them in a URL.
|
||||||
|
|
||||||
> type UserAPI3 = "users" :> "list-all" :> "now" :> Get '[JSON] [User]
|
``` haskell
|
||||||
> -- describes an endpoint reachable at:
|
type UserAPI3 = "users" :> "list-all" :> "now" :> Get '[JSON] [User]
|
||||||
> -- /users/list-all/now
|
-- describes an endpoint reachable at:
|
||||||
|
-- /users/list-all/now
|
||||||
|
```
|
||||||
|
|
||||||
`Delete`, `Get`, `Patch`, `Post` and `Put`
|
`Delete`, `Get`, `Patch`, `Post` and `Put`
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
@ -99,8 +107,10 @@ data Put (contentTypes :: [*]) a
|
||||||
An endpoint ends with one of the 5 combinators above (unless you write your
|
An endpoint ends with one of the 5 combinators above (unless you write your
|
||||||
own). Examples:
|
own). Examples:
|
||||||
|
|
||||||
> type UserAPI4 = "users" :> Get '[JSON] [User]
|
``` haskell
|
||||||
> :<|> "admins" :> Get '[JSON] [User]
|
type UserAPI4 = "users" :> Get '[JSON] [User]
|
||||||
|
:<|> "admins" :> Get '[JSON] [User]
|
||||||
|
```
|
||||||
|
|
||||||
`Capture`
|
`Capture`
|
||||||
---------
|
---------
|
||||||
|
@ -127,13 +137,15 @@ class, which the captured value must be an instance of.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
> type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User
|
``` haskell
|
||||||
> -- equivalent to 'GET /user/:userid'
|
type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User
|
||||||
> -- except that we explicitly say that "userid"
|
-- equivalent to 'GET /user/:userid'
|
||||||
> -- must be an integer
|
-- except that we explicitly say that "userid"
|
||||||
>
|
-- must be an integer
|
||||||
> :<|> "user" :> Capture "userid" Integer :> Delete '[] ()
|
|
||||||
> -- equivalent to 'DELETE /user/:userid'
|
:<|> "user" :> Capture "userid" Integer :> Delete '[] ()
|
||||||
|
-- equivalent to 'DELETE /user/:userid'
|
||||||
|
```
|
||||||
|
|
||||||
`QueryParam`, `QueryParams`, `QueryFlag`, `MatrixParam`, `MatrixParams` and `MatrixFlag`
|
`QueryParam`, `QueryParams`, `QueryFlag`, `MatrixParam`, `MatrixParams` and `MatrixFlag`
|
||||||
----------------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------------
|
||||||
|
@ -179,11 +191,13 @@ data MatrixFlag (sym :: Symbol)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
> type UserAPI6 = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User]
|
``` haskell
|
||||||
> -- equivalent to 'GET /users?sortby={age, name}'
|
type UserAPI6 = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User]
|
||||||
>
|
-- equivalent to 'GET /users?sortby={age, name}'
|
||||||
> :<|> "users" :> MatrixParam "sortby" SortBy :> Get '[JSON] [User]
|
|
||||||
> -- equivalent to 'GET /users;sortby={age, name}'
|
:<|> "users" :> MatrixParam "sortby" SortBy :> Get '[JSON] [User]
|
||||||
|
-- equivalent to 'GET /users;sortby={age, name}'
|
||||||
|
```
|
||||||
|
|
||||||
Again, your handlers don't have to deserialize these things (into, for example,
|
Again, your handlers don't have to deserialize these things (into, for example,
|
||||||
a `SortBy`). *servant* takes care of it.
|
a `SortBy`). *servant* takes care of it.
|
||||||
|
@ -212,17 +226,19 @@ data ReqBody (contentTypes :: [*]) a
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
> type UserAPI7 = "users" :> ReqBody '[JSON] User :> Post '[JSON] User
|
``` haskell
|
||||||
> -- - equivalent to 'POST /users' with a JSON object
|
type UserAPI7 = "users" :> ReqBody '[JSON] User :> Post '[JSON] User
|
||||||
> -- describing a User in the request body
|
-- - equivalent to 'POST /users' with a JSON object
|
||||||
> -- - returns a User encoded in JSON
|
-- describing a User in the request body
|
||||||
>
|
-- - returns a User encoded in JSON
|
||||||
> :<|> "users" :> Capture "userid" Integer
|
|
||||||
> :> ReqBody '[JSON] User
|
:<|> "users" :> Capture "userid" Integer
|
||||||
> :> Put '[JSON] User
|
:> ReqBody '[JSON] User
|
||||||
> -- - equivalent to 'PUT /users/:userid' with a JSON
|
:> Put '[JSON] User
|
||||||
> -- object describing a User in the request body
|
-- - equivalent to 'PUT /users/:userid' with a JSON
|
||||||
> -- - returns a User encoded in JSON
|
-- object describing a User in the request body
|
||||||
|
-- - returns a User encoded in JSON
|
||||||
|
```
|
||||||
|
|
||||||
Request `Header`s
|
Request `Header`s
|
||||||
-----------------
|
-----------------
|
||||||
|
@ -243,7 +259,9 @@ Here's an example where we declare that an endpoint makes use of the
|
||||||
`User-Agent` header which specifies the name of the software/library used by
|
`User-Agent` header which specifies the name of the software/library used by
|
||||||
the client to send the request.
|
the client to send the request.
|
||||||
|
|
||||||
> type UserAPI8 = "users" :> Header "User-Agent" Text :> Get '[JSON] [User]
|
``` haskell
|
||||||
|
type UserAPI8 = "users" :> Header "User-Agent" Text :> Get '[JSON] [User]
|
||||||
|
```
|
||||||
|
|
||||||
Content types
|
Content types
|
||||||
-------------
|
-------------
|
||||||
|
@ -257,7 +275,9 @@ Four content-types are provided out-of-the-box by the core *servant* package:
|
||||||
reason you wanted one of your endpoints to make your user data available under
|
reason you wanted one of your endpoints to make your user data available under
|
||||||
those 4 formats, you would write the API type as below:
|
those 4 formats, you would write the API type as below:
|
||||||
|
|
||||||
> type UserAPI9 = "users" :> Get '[JSON, PlainText, FormUrlEncoded, OctetStream] [User]
|
``` haskell
|
||||||
|
type UserAPI9 = "users" :> Get '[JSON, PlainText, FormUrlEncoded, OctetStream] [User]
|
||||||
|
```
|
||||||
|
|
||||||
We also provide an HTML content-type, but since there's no single library
|
We also provide an HTML content-type, but since there's no single library
|
||||||
that everyone uses, we decided to release 2 packages, *servant-lucid* and
|
that everyone uses, we decided to release 2 packages, *servant-lucid* and
|
||||||
|
@ -281,7 +301,9 @@ data Headers (ls :: [*]) a
|
||||||
If you want to describe an endpoint that returns a "User-Count" header in each
|
If you want to describe an endpoint that returns a "User-Count" header in each
|
||||||
response, you could write it as below:
|
response, you could write it as below:
|
||||||
|
|
||||||
> type UserAPI10 = "users" :> Get '[JSON] (Headers '[Header "User-Count" Integer] [User])
|
``` haskell
|
||||||
|
type UserAPI10 = "users" :> Get '[JSON] (Headers '[Header "User-Count" Integer] [User])
|
||||||
|
```
|
||||||
|
|
||||||
Interoperability with other WAI `Application`s: `Raw`
|
Interoperability with other WAI `Application`s: `Raw`
|
||||||
-----------------------------------------------------
|
-----------------------------------------------------
|
||||||
|
@ -290,14 +312,16 @@ Finally, we also include a combinator named `Raw` that can be used for two reaso
|
||||||
|
|
||||||
- You want to serve static files from a given directory. In that case you can just say:
|
- You want to serve static files from a given directory. In that case you can just say:
|
||||||
|
|
||||||
> type UserAPI11 = "users" :> Get '[JSON] [User]
|
``` haskell
|
||||||
> -- a /users endpoint
|
type UserAPI11 = "users" :> Get '[JSON] [User]
|
||||||
>
|
-- a /users endpoint
|
||||||
> :<|> Raw
|
|
||||||
> -- requests to anything else than /users
|
:<|> Raw
|
||||||
> -- go here, where the server will try to
|
-- requests to anything else than /users
|
||||||
> -- find a file with the right name
|
-- go here, where the server will try to
|
||||||
> -- at the right path
|
-- find a file with the right name
|
||||||
|
-- at the right path
|
||||||
|
```
|
||||||
|
|
||||||
- You more generally want to plug a [WAI `Application`](http://hackage.haskell.org/package/wai)
|
- You more generally want to plug a [WAI `Application`](http://hackage.haskell.org/package/wai)
|
||||||
into your webservice. Static file serving is a specific example of that. The API type would look the
|
into your webservice. Static file serving is a specific example of that. The API type would look the
|
||||||
|
|
|
@ -11,75 +11,85 @@ and friends. By *derive*, we mean that there's no code generation involved, the
|
||||||
The source for this tutorial section is a literate haskell file, so first we
|
The source for this tutorial section is a literate haskell file, so first we
|
||||||
need to have some language extensions and imports:
|
need to have some language extensions and imports:
|
||||||
|
|
||||||
> {-# LANGUAGE DataKinds #-}
|
``` haskell
|
||||||
> {-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
> {-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
>
|
{-# LANGUAGE TypeOperators #-}
|
||||||
> module Client where
|
|
||||||
>
|
module Client where
|
||||||
> import Control.Monad.Trans.Either
|
|
||||||
> import Data.Aeson
|
import Control.Monad.Trans.Either
|
||||||
> import Data.Proxy
|
import Data.Aeson
|
||||||
> import GHC.Generics
|
import Data.Proxy
|
||||||
> import Servant.API
|
import GHC.Generics
|
||||||
> import Servant.Client
|
import Servant.API
|
||||||
|
import Servant.Client
|
||||||
|
```
|
||||||
|
|
||||||
Also, we need examples for some domain specific data types:
|
Also, we need examples for some domain specific data types:
|
||||||
|
|
||||||
> data Position = Position
|
``` haskell
|
||||||
> { x :: Int
|
data Position = Position
|
||||||
> , y :: Int
|
{ x :: Int
|
||||||
> } deriving (Show, Generic)
|
, y :: Int
|
||||||
>
|
} deriving (Show, Generic)
|
||||||
> instance FromJSON Position
|
|
||||||
>
|
instance FromJSON Position
|
||||||
> newtype HelloMessage = HelloMessage { msg :: String }
|
|
||||||
> deriving (Show, Generic)
|
newtype HelloMessage = HelloMessage { msg :: String }
|
||||||
>
|
deriving (Show, Generic)
|
||||||
> instance FromJSON HelloMessage
|
|
||||||
>
|
instance FromJSON HelloMessage
|
||||||
> data ClientInfo = ClientInfo
|
|
||||||
> { clientName :: String
|
data ClientInfo = ClientInfo
|
||||||
> , clientEmail :: String
|
{ clientName :: String
|
||||||
> , clientAge :: Int
|
, clientEmail :: String
|
||||||
> , clientInterestedIn :: [String]
|
, clientAge :: Int
|
||||||
> } deriving Generic
|
, clientInterestedIn :: [String]
|
||||||
>
|
} deriving Generic
|
||||||
> instance ToJSON ClientInfo
|
|
||||||
>
|
instance ToJSON ClientInfo
|
||||||
> data Email = Email
|
|
||||||
> { from :: String
|
data Email = Email
|
||||||
> , to :: String
|
{ from :: String
|
||||||
> , subject :: String
|
, to :: String
|
||||||
> , body :: String
|
, subject :: String
|
||||||
> } deriving (Show, Generic)
|
, body :: String
|
||||||
>
|
} deriving (Show, Generic)
|
||||||
> instance FromJSON Email
|
|
||||||
|
instance FromJSON Email
|
||||||
|
```
|
||||||
|
|
||||||
Enough chitchat, let's see an example. Consider the following API type from the previous section:
|
Enough chitchat, let's see an example. Consider the following API type from the previous section:
|
||||||
|
|
||||||
> type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
|
``` haskell
|
||||||
> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
|
type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
|
||||||
> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
|
:<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
|
||||||
|
:<|> "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:
|
||||||
|
|
||||||
> position :: Int -- ^ value for "x"
|
``` haskell
|
||||||
> -> Int -- ^ value for "y"
|
position :: Int -- ^ value for "x"
|
||||||
> -> EitherT ServantError IO Position
|
-> Int -- ^ value for "y"
|
||||||
>
|
-> EitherT ServantError IO Position
|
||||||
> hello :: Maybe String -- ^ an optional value for "name"
|
|
||||||
> -> EitherT ServantError IO HelloMessage
|
hello :: Maybe String -- ^ an optional value for "name"
|
||||||
>
|
-> EitherT ServantError IO HelloMessage
|
||||||
> marketing :: ClientInfo -- ^ value for the request body
|
|
||||||
> -> EitherT ServantError IO Email
|
marketing :: ClientInfo -- ^ value for the request body
|
||||||
|
-> EitherT 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? Just give a `Proxy` to your API and a host to make the requests to:
|
||||||
|
|
||||||
> api :: Proxy API
|
``` haskell
|
||||||
> api = Proxy
|
api :: Proxy API
|
||||||
>
|
api = Proxy
|
||||||
> position :<|> hello :<|> marketing = client api (BaseUrl Http "localhost" 8081)
|
|
||||||
|
position :<|> hello :<|> marketing = client api (BaseUrl Http "localhost" 8081)
|
||||||
|
```
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
|
@ -101,22 +111,24 @@ data BaseUrl = BaseUrl
|
||||||
|
|
||||||
That's it. Let's now write some code that uses our client functions.
|
That's it. Let's now write some code that uses our client functions.
|
||||||
|
|
||||||
> queries :: EitherT ServantError IO (Position, HelloMessage, Email)
|
``` haskell
|
||||||
> queries = do
|
queries :: EitherT ServantError IO (Position, HelloMessage, Email)
|
||||||
> pos <- position 10 10
|
queries = do
|
||||||
> msg <- hello (Just "servant")
|
pos <- position 10 10
|
||||||
> em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"])
|
msg <- hello (Just "servant")
|
||||||
> return (pos, msg, em)
|
em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"])
|
||||||
>
|
return (pos, msg, em)
|
||||||
> run :: IO ()
|
|
||||||
> run = do
|
run :: IO ()
|
||||||
> res <- runEitherT queries
|
run = do
|
||||||
> case res of
|
res <- runEitherT queries
|
||||||
> Left err -> putStrLn $ "Error: " ++ show err
|
case res of
|
||||||
> Right (pos, msg, em) -> do
|
Left err -> putStrLn $ "Error: " ++ show err
|
||||||
> print pos
|
Right (pos, msg, em) -> do
|
||||||
> print msg
|
print pos
|
||||||
> print em
|
print msg
|
||||||
|
print em
|
||||||
|
```
|
||||||
|
|
||||||
You can now run `dist/build/tutorial/tutorial 8` (the server) and
|
You can now run `dist/build/tutorial/tutorial 8` (the server) and
|
||||||
`dist/build/t8-main/t8-main` (the client) to see them both in action.
|
`dist/build/t8-main/t8-main` (the client) to see them both in action.
|
||||||
|
|
|
@ -6,89 +6,99 @@ toc: true
|
||||||
The source for this tutorial section is a literate haskell file, so first we
|
The source for this tutorial section is a literate haskell file, so first we
|
||||||
need to have some language extensions and imports:
|
need to have some language extensions and imports:
|
||||||
|
|
||||||
> {-# LANGUAGE DataKinds #-}
|
``` haskell
|
||||||
> {-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
> {-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
> {-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
> {-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
> {-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
> {-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# LANGUAGE TypeOperators #-}
|
||||||
>
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
> module Docs where
|
|
||||||
>
|
module Docs where
|
||||||
> import Data.ByteString.Lazy (ByteString)
|
|
||||||
> import Data.Proxy
|
import Data.ByteString.Lazy (ByteString)
|
||||||
> import Data.Text.Lazy.Encoding (encodeUtf8)
|
import Data.Proxy
|
||||||
> import Data.Text.Lazy (pack)
|
import Data.Text.Lazy.Encoding (encodeUtf8)
|
||||||
> import Network.HTTP.Types
|
import Data.Text.Lazy (pack)
|
||||||
> import Network.Wai
|
import Network.HTTP.Types
|
||||||
> import Servant.API
|
import Network.Wai
|
||||||
> import Servant.Docs
|
import Servant.API
|
||||||
> import Servant.Server
|
import Servant.Docs
|
||||||
|
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](/tutorial/server.html)):
|
||||||
|
|
||||||
> import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..),
|
``` haskell
|
||||||
> server3, emailForClient)
|
import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..),
|
||||||
|
server3, emailForClient)
|
||||||
|
```
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
> type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
|
``` haskell
|
||||||
> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
|
type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
|
||||||
> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
|
:<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
|
||||||
>
|
:<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
|
||||||
> exampleAPI :: Proxy ExampleAPI
|
|
||||||
> exampleAPI = Proxy
|
exampleAPI :: Proxy ExampleAPI
|
||||||
|
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.
|
||||||
|
|
||||||
> instance ToCapture (Capture "x" Int) where
|
``` haskell
|
||||||
> toCapture _ =
|
instance ToCapture (Capture "x" Int) where
|
||||||
> DocCapture "x" -- name
|
toCapture _ =
|
||||||
> "(integer) position on the x axis" -- description
|
DocCapture "x" -- name
|
||||||
>
|
"(integer) position on the x axis" -- description
|
||||||
> instance ToCapture (Capture "y" Int) where
|
|
||||||
> toCapture _ =
|
instance ToCapture (Capture "y" Int) where
|
||||||
> DocCapture "y" -- name
|
toCapture _ =
|
||||||
> "(integer) position on the y axis" -- description
|
DocCapture "y" -- name
|
||||||
>
|
"(integer) position on the y axis" -- description
|
||||||
> instance ToSample Position Position where
|
|
||||||
> toSample _ = Just (Position 3 14) -- example of output
|
instance ToSample Position Position where
|
||||||
>
|
toSample _ = Just (Position 3 14) -- example of output
|
||||||
> instance ToParam (QueryParam "name" String) where
|
|
||||||
> toParam _ =
|
instance ToParam (QueryParam "name" String) where
|
||||||
> DocQueryParam "name" -- name
|
toParam _ =
|
||||||
> ["Alp", "John Doe", "..."] -- example of values (not necessarily exhaustive)
|
DocQueryParam "name" -- name
|
||||||
> "Name of the person to say hello to." -- description
|
["Alp", "John Doe", "..."] -- example of values (not necessarily exhaustive)
|
||||||
> Normal -- Normal, List or Flag
|
"Name of the person to say hello to." -- description
|
||||||
>
|
Normal -- Normal, List or Flag
|
||||||
> instance ToSample HelloMessage HelloMessage where
|
|
||||||
> toSamples _ =
|
instance ToSample HelloMessage HelloMessage where
|
||||||
> [ ("When a value is provided for 'name'", HelloMessage "Hello, Alp")
|
toSamples _ =
|
||||||
> , ("When 'name' is not specified", HelloMessage "Hello, anonymous coward")
|
[ ("When a value is provided for 'name'", HelloMessage "Hello, Alp")
|
||||||
> ]
|
, ("When 'name' is not specified", HelloMessage "Hello, anonymous coward")
|
||||||
> -- mutliple examples to display this time
|
]
|
||||||
>
|
-- mutliple examples to display this time
|
||||||
> ci :: ClientInfo
|
|
||||||
> ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]
|
ci :: ClientInfo
|
||||||
>
|
ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]
|
||||||
> instance ToSample ClientInfo ClientInfo where
|
|
||||||
> toSample _ = Just ci
|
instance ToSample ClientInfo ClientInfo where
|
||||||
>
|
toSample _ = Just ci
|
||||||
> instance ToSample Email Email where
|
|
||||||
> toSample _ = Just (emailForClient ci)
|
instance ToSample Email Email where
|
||||||
|
toSample _ = Just (emailForClient ci)
|
||||||
|
```
|
||||||
|
|
||||||
Types that are used as request or response bodies have to instantiate the `ToSample` typeclass which lets you specify one or more examples of values. `Capture`s and `QueryParam`s have to instantiate their respective `ToCapture` and `ToParam` classes and provide a name and some information about the concrete meaning of that argument, as illustrated in the code above.
|
Types that are used as request or response bodies have to instantiate the `ToSample` typeclass which lets you specify one or more examples of values. `Capture`s and `QueryParam`s have to instantiate their respective `ToCapture` and `ToParam` classes and provide a name and some information about the concrete meaning of that argument, as illustrated in the code above.
|
||||||
|
|
||||||
With all of this, we can derive docs for our API.
|
With all of this, we can derive docs for our API.
|
||||||
|
|
||||||
> apiDocs :: API
|
``` haskell
|
||||||
> apiDocs = docs exampleAPI
|
apiDocs :: API
|
||||||
|
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.
|
||||||
|
|
||||||
|
@ -192,33 +202,37 @@ That lets us see what our API docs look down in markdown, by looking at `markdow
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
> docsBS :: ByteString
|
``` haskell
|
||||||
> docsBS = encodeUtf8
|
docsBS :: ByteString
|
||||||
> . pack
|
docsBS = encodeUtf8
|
||||||
> . markdown
|
. pack
|
||||||
> $ docsWithIntros [intro] exampleAPI
|
. markdown
|
||||||
>
|
$ docsWithIntros [intro] exampleAPI
|
||||||
> where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"]
|
|
||||||
|
where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"]
|
||||||
|
```
|
||||||
|
|
||||||
`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
|
`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
|
||||||
|
|
||||||
We can now serve the API *and* the API docs with a simple server.
|
We can now serve the API *and* the API docs with a simple server.
|
||||||
|
|
||||||
> type DocsAPI = ExampleAPI :<|> Raw
|
``` haskell
|
||||||
>
|
type DocsAPI = ExampleAPI :<|> Raw
|
||||||
> api :: Proxy DocsAPI
|
|
||||||
> api = Proxy
|
api :: Proxy DocsAPI
|
||||||
>
|
api = Proxy
|
||||||
> server :: Server DocsAPI
|
|
||||||
> server = Server.server3 :<|> serveDocs
|
server :: Server DocsAPI
|
||||||
>
|
server = Server.server3 :<|> serveDocs
|
||||||
> where serveDocs _ respond =
|
|
||||||
> respond $ responseLBS ok200 [plain] docsBS
|
where serveDocs _ respond =
|
||||||
>
|
respond $ responseLBS ok200 [plain] docsBS
|
||||||
> plain = ("Content-Type", "text/plain")
|
|
||||||
>
|
plain = ("Content-Type", "text/plain")
|
||||||
> app :: Application
|
|
||||||
> app = serve api server
|
app :: Application
|
||||||
|
app = serve api server
|
||||||
|
```
|
||||||
|
|
||||||
And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type.
|
And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type.
|
||||||
|
|
||||||
|
|
|
@ -22,117 +22,131 @@ query your API.
|
||||||
The source for this tutorial section is a literate haskell file, so first we
|
The source for this tutorial section is a literate haskell file, so first we
|
||||||
need to have some language extensions and imports:
|
need to have some language extensions and imports:
|
||||||
|
|
||||||
> {-# LANGUAGE DataKinds #-}
|
``` haskell
|
||||||
> {-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
> {-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
> {-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
>
|
{-# LANGUAGE TypeOperators #-}
|
||||||
> module Javascript where
|
|
||||||
>
|
module Javascript where
|
||||||
> import Control.Monad.IO.Class
|
|
||||||
> import Data.Aeson
|
import Control.Monad.IO.Class
|
||||||
> import Data.Proxy
|
import Data.Aeson
|
||||||
> import Data.Text (Text)
|
import Data.Proxy
|
||||||
> import qualified Data.Text as T
|
import Data.Text (Text)
|
||||||
> import GHC.Generics
|
import qualified Data.Text as T
|
||||||
> import Language.Javascript.JQuery
|
import GHC.Generics
|
||||||
> import Network.Wai
|
import Language.Javascript.JQuery
|
||||||
> import Servant
|
import Network.Wai
|
||||||
> import Servant.JQuery
|
import Servant
|
||||||
> import System.Random
|
import Servant.JQuery
|
||||||
|
import System.Random
|
||||||
|
```
|
||||||
|
|
||||||
Now let's have the API type(s) and the accompanying datatypes.
|
Now let's have the API type(s) and the accompanying datatypes.
|
||||||
|
|
||||||
> type API = "point" :> Get '[JSON] Point
|
``` haskell
|
||||||
> :<|> "books" :> QueryParam "q" Text :> Get '[JSON] (Search Book)
|
type API = "point" :> Get '[JSON] Point
|
||||||
>
|
:<|> "books" :> QueryParam "q" Text :> Get '[JSON] (Search Book)
|
||||||
> type API' = API :<|> Raw
|
|
||||||
>
|
type API' = API :<|> Raw
|
||||||
> data Point = Point
|
|
||||||
> { x :: Double
|
data Point = Point
|
||||||
> , y :: Double
|
{ x :: Double
|
||||||
> } deriving Generic
|
, y :: Double
|
||||||
>
|
} deriving Generic
|
||||||
> instance ToJSON Point
|
|
||||||
>
|
instance ToJSON Point
|
||||||
> data Search a = Search
|
|
||||||
> { query :: Text
|
data Search a = Search
|
||||||
> , results :: [a]
|
{ query :: Text
|
||||||
> } deriving Generic
|
, results :: [a]
|
||||||
>
|
} deriving Generic
|
||||||
> mkSearch :: Text -> [a] -> Search a
|
|
||||||
> mkSearch = Search
|
mkSearch :: Text -> [a] -> Search a
|
||||||
>
|
mkSearch = Search
|
||||||
> instance ToJSON a => ToJSON (Search a)
|
|
||||||
>
|
instance ToJSON a => ToJSON (Search a)
|
||||||
> data Book = Book
|
|
||||||
> { author :: Text
|
data Book = Book
|
||||||
> , title :: Text
|
{ author :: Text
|
||||||
> , year :: Int
|
, title :: Text
|
||||||
> } deriving Generic
|
, year :: Int
|
||||||
>
|
} deriving Generic
|
||||||
> instance ToJSON Book
|
|
||||||
>
|
instance ToJSON Book
|
||||||
> book :: Text -> Text -> Int -> Book
|
|
||||||
> book = Book
|
book :: Text -> Text -> Int -> Book
|
||||||
|
book = Book
|
||||||
|
```
|
||||||
|
|
||||||
We need a "book database". For the purpose of this guide, let's restrict ourselves to the following books.
|
We need a "book database". For the purpose of this guide, let's restrict ourselves to the following books.
|
||||||
|
|
||||||
> books :: [Book]
|
``` haskell
|
||||||
> books =
|
books :: [Book]
|
||||||
> [ book "Paul Hudak" "The Haskell School of Expression: Learning Functional Programming through Multimedia" 2000
|
books =
|
||||||
> , book "Bryan O'Sullivan, Don Stewart, and John Goerzen" "Real World Haskell" 2008
|
[ book "Paul Hudak" "The Haskell School of Expression: Learning Functional Programming through Multimedia" 2000
|
||||||
> , book "Miran Lipovača" "Learn You a Haskell for Great Good!" 2011
|
, book "Bryan O'Sullivan, Don Stewart, and John Goerzen" "Real World Haskell" 2008
|
||||||
> , book "Graham Hutton" "Programming in Haskell" 2007
|
, book "Miran Lipovača" "Learn You a Haskell for Great Good!" 2011
|
||||||
> , book "Simon Marlow" "Parallel and Concurrent Programming in Haskell" 2013
|
, book "Graham Hutton" "Programming in Haskell" 2007
|
||||||
> , book "Richard Bird" "Introduction to Functional Programming using Haskell" 1998
|
, book "Simon Marlow" "Parallel and Concurrent Programming in Haskell" 2013
|
||||||
> ]
|
, book "Richard Bird" "Introduction to Functional Programming using Haskell" 1998
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
Now, given an optional search string `q`, we want to perform a case insensitive search in that list of books. We're obviously not going to try and implement the best possible algorithm, this is out of scope for this tutorial. The following simple linear scan will do, given how small our list is.
|
Now, given an optional search string `q`, we want to perform a case insensitive search in that list of books. We're obviously not going to try and implement the best possible algorithm, this is out of scope for this tutorial. The following simple linear scan will do, given how small our list is.
|
||||||
|
|
||||||
> searchBook :: Monad m => Maybe Text -> m (Search Book)
|
``` haskell
|
||||||
> searchBook Nothing = return (mkSearch "" books)
|
searchBook :: Monad m => Maybe Text -> m (Search Book)
|
||||||
> searchBook (Just q) = return (mkSearch q books')
|
searchBook Nothing = return (mkSearch "" books)
|
||||||
>
|
searchBook (Just q) = return (mkSearch q books')
|
||||||
> where books' = filter (\b -> q' `T.isInfixOf` T.toLower (author b)
|
|
||||||
> || q' `T.isInfixOf` T.toLower (title b)
|
where books' = filter (\b -> q' `T.isInfixOf` T.toLower (author b)
|
||||||
> )
|
|| q' `T.isInfixOf` T.toLower (title b)
|
||||||
> books
|
)
|
||||||
> q' = T.toLower q
|
books
|
||||||
|
q' = T.toLower q
|
||||||
|
```
|
||||||
|
|
||||||
We also need an endpoint that generates random points `(x, y)` with `-1 <= x,y <= 1`. The code below uses [random](http://hackage.haskell.org/package/random)'s `System.Random`.
|
We also need an endpoint that generates random points `(x, y)` with `-1 <= x,y <= 1`. The code below uses [random](http://hackage.haskell.org/package/random)'s `System.Random`.
|
||||||
|
|
||||||
> randomPoint :: MonadIO m => m Point
|
``` haskell
|
||||||
> randomPoint = liftIO . getStdRandom $ \g ->
|
randomPoint :: MonadIO m => m Point
|
||||||
> let (rx, g') = randomR (-1, 1) g
|
randomPoint = liftIO . getStdRandom $ \g ->
|
||||||
> (ry, g'') = randomR (-1, 1) g'
|
let (rx, g') = randomR (-1, 1) g
|
||||||
> in (Point rx ry, g'')
|
(ry, g'') = randomR (-1, 1) g'
|
||||||
|
in (Point rx ry, g'')
|
||||||
|
```
|
||||||
|
|
||||||
If we add static file serving, our server is now complete.
|
If we add static file serving, our server is now complete.
|
||||||
|
|
||||||
> api :: Proxy API
|
``` haskell
|
||||||
> api = Proxy
|
api :: Proxy API
|
||||||
>
|
api = Proxy
|
||||||
> api' :: Proxy API'
|
|
||||||
> api' = Proxy
|
api' :: Proxy API'
|
||||||
>
|
api' = Proxy
|
||||||
> server :: Server API
|
|
||||||
> server = randomPoint
|
server :: Server API
|
||||||
> :<|> searchBook
|
server = randomPoint
|
||||||
>
|
:<|> searchBook
|
||||||
> server' :: Server API'
|
|
||||||
> server' = server
|
server' :: Server API'
|
||||||
> :<|> serveDirectory "tutorial/t9"
|
server' = server
|
||||||
>
|
:<|> serveDirectory "tutorial/t9"
|
||||||
> app :: Application
|
|
||||||
> app = serve api' server'
|
app :: Application
|
||||||
|
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.
|
||||||
|
|
||||||
Very similarly to how one can derive haskell functions, we can derive the javascript with just a simple function call to `jsForAPI` from `Servant.JQuery`.
|
Very similarly to how one can derive haskell functions, we can derive the javascript with just a simple function call to `jsForAPI` from `Servant.JQuery`.
|
||||||
|
|
||||||
> apiJS :: String
|
``` haskell
|
||||||
> apiJS = jsForAPI api
|
apiJS :: String
|
||||||
|
apiJS = jsForAPI api
|
||||||
|
```
|
||||||
|
|
||||||
This `String` contains 2 Javascript functions:
|
This `String` contains 2 Javascript functions:
|
||||||
|
|
||||||
|
@ -161,11 +175,13 @@ function getbooks(q, onSuccess, onError)
|
||||||
|
|
||||||
Right before starting up our server, we will need to write this `String` to a file, say `api.js`, along with a copy of the *jQuery* library, as provided by the [js-jquery](http://hackage.haskell.org/package/js-jquery) package.
|
Right before starting up our server, we will need to write this `String` to a file, say `api.js`, along with a copy of the *jQuery* library, as provided by the [js-jquery](http://hackage.haskell.org/package/js-jquery) package.
|
||||||
|
|
||||||
> writeJSFiles :: IO ()
|
``` haskell
|
||||||
> writeJSFiles = do
|
writeJSFiles :: IO ()
|
||||||
> writeFile "getting-started/gs9/api.js" apiJS
|
writeJSFiles = do
|
||||||
> jq <- readFile =<< Language.Javascript.JQuery.file
|
writeFile "getting-started/gs9/api.js" apiJS
|
||||||
> writeFile "getting-started/gs9/jq.js" jq
|
jq <- readFile =<< Language.Javascript.JQuery.file
|
||||||
|
writeFile "getting-started/gs9/jq.js" jq
|
||||||
|
```
|
||||||
|
|
||||||
And we're good to go. Start the server with `dist/build/tutorial/tutorial 9` and go to `http://localhost:8081/`. Start typing in the name of one of the authors in our database or part of a book title, and check out how long it takes to approximate π using the method mentioned above.
|
And we're good to go. Start the server with `dist/build/tutorial/tutorial 9` and go to `http://localhost:8081/`. Start typing in the name of one of the authors in our database or part of a book title, and check out how long it takes to approximate π using the method mentioned above.
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue