on type-errors help sign in
servant/doc/tutorial/Docs.lhs
276 lines 9.6 kB history raw
1# Documenting an API
3The source for this tutorial section is a literate haskell file, so first we
4need to have some language extensions and imports:
6``` haskell
7{-# LANGUAGE DataKinds #-}
8{-# LANGUAGE DeriveGeneric #-}
9{-# LANGUAGE FlexibleInstances #-}
10{-# LANGUAGE MultiParamTypeClasses #-}
11{-# LANGUAGE OverloadedStrings #-}
12{-# LANGUAGE TypeOperators #-}
13{-# OPTIONS_GHC -fno-warn-orphans #-}
15module Docs where
17import Data.ByteString.Lazy (ByteString)
18import Data.Proxy
19import Data.Text.Lazy.Encoding (encodeUtf8)
20import Data.Text.Lazy (pack)
21import Network.HTTP.Types
22import Network.Wai
23import Servant.API
24import Servant.Docs
25import Servant.Server
26import Web.FormUrlEncoded(FromForm(..), ToForm(..))
27```
29And we'll import some things from one of our earlier modules
30([Serving an API](Server.html)):
32``` haskell
33import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..),
34 server3, emailForClient)
35```
37Like 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.
39This 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:
41``` haskell
42type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
43 :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
44 :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
46exampleAPI :: Proxy ExampleAPI
47exampleAPI = Proxy
48```
50While **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.
52For 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.
54``` haskell
55instance ToCapture (Capture "x" Int) where
56 toCapture _ =
57 DocCapture "x" -- name
58 "(integer) position on the x axis" -- description
60instance ToCapture (Capture "y" Int) where
61 toCapture _ =
62 DocCapture "y" -- name
63 "(integer) position on the y axis" -- description
65instance ToSample Position where
66 toSamples _ = singleSample (Position 3 14) -- example of output
68instance ToParam (QueryParam "name" String) where
69 toParam _ =
70 DocQueryParam "name" -- name
71 ["Alp", "John Doe", "..."] -- example of values (not necessarily exhaustive)
72 "Name of the person to say hello to." -- description
73 Normal -- Normal, List or Flag
75instance ToSample HelloMessage where
76 toSamples _ =
77 [ ("When a value is provided for 'name'", HelloMessage "Hello, Alp")
78 , ("When 'name' is not specified", HelloMessage "Hello, anonymous coward")
79 ]
80 -- multiple examples to display this time
82ci :: ClientInfo
83ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]
85instance ToSample ClientInfo where
86 toSamples _ = singleSample ci
88instance ToSample Email where
89 toSamples _ = singleSample (emailForClient ci)
90```
92Types 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.
93The `EmptyAPI` combinator needs no special treatment as it generates no
94documentation: an empty API has no endpoints to document.
96With all of this, we can derive docs for our API.
98``` haskell
99apiDocs :: API
100apiDocs = docs exampleAPI
101```
103`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.
105**servant**'s markdown pretty printer is a function named `markdown`.
107``` haskell ignore
108markdown :: API -> String
109```
111That lets us see what our API docs look like in markdown, by looking at `markdown apiDocs`.
113````````` text
114## GET /hello
116#### GET Parameters:
118- name
119 - **Values**: *Alp, John Doe, ...*
120 - **Description**: Name of the person to say hello to.
123#### Response:
125- Status code 200
126- Headers: []
128- Supported content types are:
130 - `application/json;charset=utf-8`
131 - `application/json`
133- When a value is provided for 'name' (`application/json;charset=utf-8`, `application/json`):
135 ```javascript
136{"msg":"Hello, Alp"}
137 ```
139- When 'name' is not specified (`application/json;charset=utf-8`, `application/json`):
141 ```javascript
142{"msg":"Hello, anonymous coward"}
143 ```
145## POST /marketing
147#### Request:
149- Supported content types are:
151 - `application/json;charset=utf-8`
152 - `application/json`
154- Example (`application/json;charset=utf-8`, `application/json`):
156 ```javascript
157{"clientAge":26,"clientEmail":"alp@foo.com","clientName":"Alp","clientInterestedIn":["haskell","mathematics"]}
158 ```
160#### Response:
162- Status code 200
163- Headers: []
165- Supported content types are:
167 - `application/json;charset=utf-8`
168 - `application/json`
170- Example (`application/json;charset=utf-8`, `application/json`):
172 ```javascript
173{"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"}
174 ```
176## GET /position/:x/:y
178#### Captures:
180- *x*: (integer) position on the x axis
181- *y*: (integer) position on the y axis
183#### Response:
185- Status code 200
186- Headers: []
188- Supported content types are:
190 - `application/json;charset=utf-8`
191 - `application/json`
193- Example (`application/json;charset=utf-8`, `application/json`):
195 ```javascript
196{"yCoord":14,"xCoord":3}
197 ````
199`````````
201However, 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.
203``` haskell
204docsBS :: ByteString
205docsBS = encodeUtf8
206 . pack
207 . markdown
208 $ docsWithIntros [intro] exampleAPI
210 where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"]
211```
213`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
215More customisation can be done with the `markdownWith` function, which allows customising some of the parameters used when generating Markdown. The most obvious of these is how to handle when a request or response body has multiple content types. For example, if we make a slight change to the `/marketing` endpoint of our API so that the request body can also be encoded as a form:
217``` haskell
218type ExampleAPI2 = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
219 :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
220 :<|> "marketing" :> ReqBody '[JSON, FormUrlEncoded] ClientInfo :> Post '[JSON] Email
222instance ToForm ClientInfo
223instance FromForm ClientInfo
225exampleAPI2 :: Proxy ExampleAPI2
226exampleAPI2 = Proxy
228api2Docs :: API
229api2Docs = docs exampleAPI2
230```
232The relevant output of `markdown api2Docs` is now:
234```````` text
235#### Request:
237- Supported content types are:
239 - `application/json;charset=utf-8`
240 - `application/json`
241 - `application/x-www-form-urlencoded`
243- Example (`application/json;charset=utf-8`, `application/json`):
245 ```javascript
246{"clientAge":26,"clientEmail":"alp@foo.com","clientName":"Alp","clientInterestedIn":["haskell","mathematics"]}
247 ```
249- Example (`application/x-www-form-urlencoded`):
251 ```
252clientAge=26&clientEmail=alp%40foo.com&clientName=Alp&clientInterestedIn=haskell&clientInterestedIn=mathematics
253 ```
254````````
256If, however, you don't want the extra example encoding shown, then you can use `markdownWith (defRenderingOptions & requestExamples .~ FirstContentType)` to get behaviour identical to `markdown apiDocs`.
258We can now serve the API *and* the API docs with a simple server.
260``` haskell
261type DocsAPI = ExampleAPI :<|> Raw
263api :: Proxy DocsAPI
264api = Proxy
266server :: Server DocsAPI
267server = Server.server3 :<|> Tagged serveDocs where
268 serveDocs _ respond =
269 respond $ responseLBS ok200 [plain] docsBS
270 plain = ("Content-Type", "text/plain")
272app :: Application
273app = serve api server
274```
276And 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.