on a7c3880 help sign in
servant/doc/tutorial/docs.lhs
241 lines 7.7 kB history raw
1---
2title: Generating documentation from API types
3toc: true
4---
6The source for this tutorial section is a literate haskell file, so first we
7need to have some language extensions and imports:
9``` haskell
10{-# LANGUAGE DataKinds #-}
11{-# LANGUAGE DeriveGeneric #-}
12{-# LANGUAGE FlexibleInstances #-}
13{-# LANGUAGE MultiParamTypeClasses #-}
14{-# LANGUAGE OverloadedStrings #-}
15{-# LANGUAGE TypeOperators #-}
16{-# OPTIONS_GHC -fno-warn-orphans #-}
18module Docs where
20import Data.ByteString.Lazy (ByteString)
21import Data.Proxy
22import Data.Text.Lazy.Encoding (encodeUtf8)
23import Data.Text.Lazy (pack)
24import Network.HTTP.Types
25import Network.Wai
26import Servant.API
27import Servant.Docs
28import Servant.Server
29```
31And we'll import some things from one of our earlier modules
32([Serving an API](/tutorial/server.html)):
34``` haskell
35import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..),
36 server3, emailForClient)
37```
39Like 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.
41This 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:
43``` haskell
44type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
45 :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
46 :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
48exampleAPI :: Proxy ExampleAPI
49exampleAPI = Proxy
50```
52While *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.
54For 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.
56``` haskell
57instance ToCapture (Capture "x" Int) where
58 toCapture _ =
59 DocCapture "x" -- name
60 "(integer) position on the x axis" -- description
62instance ToCapture (Capture "y" Int) where
63 toCapture _ =
64 DocCapture "y" -- name
65 "(integer) position on the y axis" -- description
67instance ToSample Position Position where
68 toSample _ = Just (Position 3 14) -- example of output
70instance ToParam (QueryParam "name" String) where
71 toParam _ =
72 DocQueryParam "name" -- name
73 ["Alp", "John Doe", "..."] -- example of values (not necessarily exhaustive)
74 "Name of the person to say hello to." -- description
75 Normal -- Normal, List or Flag
77instance ToSample HelloMessage HelloMessage where
78 toSamples _ =
79 [ ("When a value is provided for 'name'", HelloMessage "Hello, Alp")
80 , ("When 'name' is not specified", HelloMessage "Hello, anonymous coward")
81 ]
82 -- mutliple examples to display this time
84ci :: ClientInfo
85ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]
87instance ToSample ClientInfo ClientInfo where
88 toSample _ = Just ci
90instance ToSample Email Email where
91 toSample _ = Just (emailForClient ci)
92```
94Types 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.
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 down in markdown, by looking at `markdown apiDocs`.
113``` text
114 ## Welcome
116 This is our super webservice's API.
118 Enjoy!
120 ## GET /hello
122 #### GET Parameters:
124 - name
125 - **Values**: *Alp, John Doe, ...*
126 - **Description**: Name of the person to say hello to.
129 #### Response:
131 - Status code 200
132 - Headers: []
134 - Supported content types are:
136 - `application/json`
138 - When a value is provided for 'name'
140 ```javascript
141 {"msg":"Hello, Alp"}
142 ```
144 - When 'name' is not specified
146 ```javascript
147 {"msg":"Hello, anonymous coward"}
148 ```
150 ## POST /marketing
152 #### Request:
154 - Supported content types are:
156 - `application/json`
158 - Example: `application/json`
160 ```javascript
161 {"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"}
162 ```
164 #### Response:
166 - Status code 201
167 - Headers: []
169 - Supported content types are:
171 - `application/json`
173 - Response body as below.
175 ```javascript
176 {"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"}
177 ```
179 ## GET /position/:x/:y
181 #### Captures:
183 - *x*: (integer) position on the x axis
184 - *y*: (integer) position on the y axis
186 #### Response:
188 - Status code 200
189 - Headers: []
191 - Supported content types are:
193 - `application/json`
195 - Response body as below.
197 ```javascript
198 {"x":3,"y":14}
199 ```
201```
203However, 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.
205``` haskell
206docsBS :: ByteString
207docsBS = encodeUtf8
208 . pack
209 . markdown
210 $ docsWithIntros [intro] exampleAPI
212 where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"]
213```
215`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
217We can now serve the API *and* the API docs with a simple server.
219``` haskell
220type DocsAPI = ExampleAPI :<|> Raw
222api :: Proxy DocsAPI
223api = Proxy
225server :: Server DocsAPI
226server = Server.server3 :<|> serveDocs
228 where serveDocs _ respond =
229 respond $ responseLBS ok200 [plain] docsBS
231 plain = ("Content-Type", "text/plain")
233app :: Application
234app = serve api server
235```
237And 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.
239<div style="text-align: center;">
240 <a href="/tutorial/javascript.html">Previous page: Generating javascript functions to query an API</a>
241</div>