on 061bf7a help sign in
servant/doc/tutorial/Docs.lhs
234 lines 7.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
26```
28And we'll import some things from one of our earlier modules
29([Serving an API](Server.html)):
31``` haskell
32import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..),
33 server3, emailForClient)
34```
36Like 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.
38This 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:
40``` haskell
41type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
42 :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
43 :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
45exampleAPI :: Proxy ExampleAPI
46exampleAPI = Proxy
47```
49While **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.
51For 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.
53``` haskell
54instance ToCapture (Capture "x" Int) where
55 toCapture _ =
56 DocCapture "x" -- name
57 "(integer) position on the x axis" -- description
59instance ToCapture (Capture "y" Int) where
60 toCapture _ =
61 DocCapture "y" -- name
62 "(integer) position on the y axis" -- description
64instance ToSample Position where
65 toSamples _ = singleSample (Position 3 14) -- example of output
67instance ToParam (QueryParam "name" String) where
68 toParam _ =
69 DocQueryParam "name" -- name
70 ["Alp", "John Doe", "..."] -- example of values (not necessarily exhaustive)
71 "Name of the person to say hello to." -- description
72 Normal -- Normal, List or Flag
74instance ToSample HelloMessage where
75 toSamples _ =
76 [ ("When a value is provided for 'name'", HelloMessage "Hello, Alp")
77 , ("When 'name' is not specified", HelloMessage "Hello, anonymous coward")
78 ]
79 -- mutliple examples to display this time
81ci :: ClientInfo
82ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]
84instance ToSample ClientInfo where
85 toSamples _ = singleSample ci
87instance ToSample Email where
88 toSamples _ = singleSample (emailForClient ci)
89```
91Types 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.
92The `EmptyAPI` combinator needs no special treatment as it generates no
93documentation: an empty API has no endpoints to document.
95With all of this, we can derive docs for our API.
97``` haskell
98apiDocs :: API
99apiDocs = docs exampleAPI
100```
102`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.
104**servant**'s markdown pretty printer is a function named `markdown`.
106``` haskell ignore
107markdown :: API -> String
108```
110That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`.
112````````` text
113## Welcome
115This is our super webservice's API.
117Enjoy!
119## GET /hello
121#### GET Parameters:
123- name
124 - **Values**: *Alp, John Doe, ...*
125 - **Description**: Name of the person to say hello to.
128#### Response:
130- Status code 200
131- Headers: []
133- Supported content types are:
135 - `application/json`
137- When a value is provided for 'name'
139 ```javascript
140 {"msg":"Hello, Alp"}
141 ```
143- When 'name' is not specified
145 ```javascript
146 {"msg":"Hello, anonymous coward"}
147 ```
149## POST /marketing
151#### Request:
153- Supported content types are:
155 - `application/json`
157- Example: `application/json`
159 ```javascript
160 {"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"}
161 ```
163#### Response:
165- Status code 201
166- Headers: []
168- Supported content types are:
170 - `application/json`
172- Response body as below.
174 ```javascript
175 {"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"}
176 ```
178## GET /position/:x/:y
180#### Captures:
182- *x*: (integer) position on the x axis
183- *y*: (integer) position on the y axis
185#### Response:
187- Status code 200
188- Headers: []
190- Supported content types are:
192 - `application/json`
194- Response body as below.
196 ```javascript
197 {"x":3,"y":14}
198 ```
200`````````
202However, 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.
204``` haskell
205docsBS :: ByteString
206docsBS = encodeUtf8
207 . pack
208 . markdown
209 $ docsWithIntros [intro] exampleAPI
211 where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"]
212```
214`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
216We can now serve the API *and* the API docs with a simple server.
218``` haskell
219type DocsAPI = ExampleAPI :<|> Raw
221api :: Proxy DocsAPI
222api = Proxy
224server :: Server DocsAPI
225server = Server.server3 :<|> Tagged serveDocs where
226 serveDocs _ respond =
227 respond $ responseLBS ok200 [plain] docsBS
228 plain = ("Content-Type", "text/plain")
230app :: Application
231app = serve api server
232```
234And 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.