on 7af73d6 help sign in
servant/doc/tutorial/docs.lhs
227 lines 7.8 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> {-# LANGUAGE DataKinds #-}
10> {-# LANGUAGE DeriveGeneric #-}
11> {-# LANGUAGE FlexibleInstances #-}
12> {-# LANGUAGE MultiParamTypeClasses #-}
13> {-# LANGUAGE OverloadedStrings #-}
14> {-# LANGUAGE TypeOperators #-}
15> {-# OPTIONS_GHC -fno-warn-orphans #-}
17> module Docs where
19> import Data.ByteString.Lazy (ByteString)
20> import Data.Proxy
21> import Data.Text.Lazy.Encoding (encodeUtf8)
22> import Data.Text.Lazy (pack)
23> import Network.HTTP.Types
24> import Network.Wai
25> import Servant.API
26> import Servant.Docs
27> import Servant.Server
29And we'll import some things from one of our earlier modules
30([Serving an API](/tutorial/server.html)):
32> import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..),
33> server3, emailForClient)
35Like 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.
37This 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:
39> type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
40> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
41> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
43> exampleAPI :: Proxy ExampleAPI
44> exampleAPI = Proxy
46While *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.
48For 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.
50> instance ToCapture (Capture "x" Int) where
51> toCapture _ =
52> DocCapture "x" -- name
53> "(integer) position on the x axis" -- description
55> instance ToCapture (Capture "y" Int) where
56> toCapture _ =
57> DocCapture "y" -- name
58> "(integer) position on the y axis" -- description
60> instance ToSample Position Position where
61> toSample _ = Just (Position 3 14) -- example of output
63> instance ToParam (QueryParam "name" String) where
64> toParam _ =
65> DocQueryParam "name" -- name
66> ["Alp", "John Doe", "..."] -- example of values (not necessarily exhaustive)
67> "Name of the person to say hello to." -- description
68> Normal -- Normal, List or Flag
70> instance ToSample HelloMessage HelloMessage where
71> toSamples _ =
72> [ ("When a value is provided for 'name'", HelloMessage "Hello, Alp")
73> , ("When 'name' is not specified", HelloMessage "Hello, anonymous coward")
74> ]
75> -- mutliple examples to display this time
77> ci :: ClientInfo
78> ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]
80> instance ToSample ClientInfo ClientInfo where
81> toSample _ = Just ci
83> instance ToSample Email Email where
84> toSample _ = Just (emailForClient ci)
86Types 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.
88With all of this, we can derive docs for our API.
90> apiDocs :: API
91> apiDocs = docs exampleAPI
93`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.
95*servant*'s markdown pretty printer is a function named `markdown`.
97``` haskell ignore
98markdown :: API -> String
99```
101That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`.
103``` text
104 ## Welcome
106 This is our super webservice's API.
108 Enjoy!
110 ## GET /hello
112 #### GET Parameters:
114 - name
115 - **Values**: *Alp, John Doe, ...*
116 - **Description**: Name of the person to say hello to.
119 #### Response:
121 - Status code 200
122 - Headers: []
124 - Supported content types are:
126 - `application/json`
128 - When a value is provided for 'name'
130 ```javascript
131 {"msg":"Hello, Alp"}
132 ```
134 - When 'name' is not specified
136 ```javascript
137 {"msg":"Hello, anonymous coward"}
138 ```
140 ## POST /marketing
142 #### Request:
144 - Supported content types are:
146 - `application/json`
148 - Example: `application/json`
150 ```javascript
151 {"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"}
152 ```
154 #### Response:
156 - Status code 201
157 - Headers: []
159 - Supported content types are:
161 - `application/json`
163 - Response body as below.
165 ```javascript
166 {"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"}
167 ```
169 ## GET /position/:x/:y
171 #### Captures:
173 - *x*: (integer) position on the x axis
174 - *y*: (integer) position on the y axis
176 #### Response:
178 - Status code 200
179 - Headers: []
181 - Supported content types are:
183 - `application/json`
185 - Response body as below.
187 ```javascript
188 {"x":3,"y":14}
189 ```
191```
193However, 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.
195> docsBS :: ByteString
196> docsBS = encodeUtf8
197> . pack
198> . markdown
199> $ docsWithIntros [intro] exampleAPI
201> where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"]
203`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
205We can now serve the API *and* the API docs with a simple server.
207> type DocsAPI = ExampleAPI :<|> Raw
209> api :: Proxy DocsAPI
210> api = Proxy
212> server :: Server DocsAPI
213> server = Server.server3 :<|> serveDocs
215> where serveDocs _ respond =
216> respond $ responseLBS ok200 [plain] docsBS
218> plain = ("Content-Type", "text/plain")
220> app :: Application
221> app = serve api server
223And 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.
225<div style="text-align: center;">
226 <a href="/tutorial/javascript.html">Previous page: Generating javascript functions to query an API</a>
227</div>