on master help sign in
Add DocIntro and DocNote types to allow extra docs
aa64d7aChristian Marie
example/greet.hs+18−4
@@ -6,7 +6,7 @@
6{-# OPTIONS_GHC -fno-warn-orphans #-}
6{-# OPTIONS_GHC -fno-warn-orphans #-}
7import Data.Aeson
7import Data.Aeson
8import Data.Proxy
8import Data.Proxy
9−import Data.Text
9+import Data.Text(Text)
10import GHC.Generics
10import GHC.Generics
11import Servant.API
11import Servant.API
12import Servant.Docs
12import Servant.Docs
@@ -44,6 +44,17 @@ instance ToSample Greet where
44 , ("If you use ?capital=false", Greet "Hello, haskeller")
44 , ("If you use ?capital=false", Greet "Hello, haskeller")
45 ]
45 ]
46
46
47+instance ToIntro "on proper introductions" where
48+ toIntro _ = DocIntro "On proper introductions." -- The title
49+ [ "Hello there."
50+ , "As documentation is usually written for humans, it's often useful \
51+ \to introduce concepts with a few words." ] -- Elements are paragraphs
52+
53+instance ToIntro "on zebras" where
54+ toIntro _ = DocIntro "This title is below the last"
55+ [ "You'll also note that multiple intros are possible." ]
56+
57+
47-- API specification
58-- API specification
48type TestApi =
59type TestApi =
49 -- GET /hello/:name?capital={true, false} returns a Greet as JSON
60 -- GET /hello/:name?capital={true, false} returns a Greet as JSON
@@ -56,14 +67,17 @@ type TestApi =
56 -- DELETE /greet/:greetid
67 -- DELETE /greet/:greetid
57 :<|> "greet" :> Capture "greetid" Text :> Delete
68 :<|> "greet" :> Capture "greetid" Text :> Delete
58
69
59−testApi :: Proxy TestApi
70+type IntroducedApi =
60−testApi = Proxy
71+ Intro "on proper introductions" :> Intro "on zebras" :> TestApi
72+
73+introducedApi :: Proxy IntroducedApi
74+introducedApi = Proxy
61
75
62-- Generate the data that lets us have API docs. This
76-- Generate the data that lets us have API docs. This
63-- is derived from the type as well as from
77-- is derived from the type as well as from
64-- the 'ToCapture', 'ToParam' and 'ToSample' instances from above.
78-- the 'ToCapture', 'ToParam' and 'ToSample' instances from above.
65docsGreet :: API
79docsGreet :: API
66−docsGreet = docs testApi
80+docsGreet = docs introducedApi
67
81
68main :: IO ()
82main :: IO ()
69main = putStrLn $ markdown docsGreet
83main = putStrLn $ markdown docsGreet
example/greet.md+20−13
@@ -1,7 +1,16 @@
1−POST /greet
1+#### On proper introductions.
2−-----------
3
2
3+Hello there.
4+
5+As documentation is usually written for humans, it's often useful to introduce concepts with a few words.
6+
7+#### This title is below the last
8+
9+You'll also note that multiple intros are possible.
10+
11+## POST /greet
12+
4−**Request Body**:
13+#### Request Body:
5
14
6``` javascript
15``` javascript
7{
16{
@@ -9,7 +18,7 @@ POST /greet
9}
18}
10```
19```
11
20
12−**Response**:
21+#### Response:
13
22
14 - Status code 201
23 - Status code 201
15 - If you use ?capital=true
24 - If you use ?capital=true
@@ -28,21 +37,20 @@ POST /greet
28}
37}
29```
38```
30
39
31−GET /hello/:name
40+## GET /hello/:name
32−----------------
33
41
34−**Captures**:
42+#### Captures:
35
43
36- *name*: name of the person to greet
44- *name*: name of the person to greet
37
45
38−**GET Parameters**:
46+#### GET Parameters:
39
47
40 - capital
48 - capital
41 - **Values**: *true, false*
49 - **Values**: *true, false*
42 - **Description**: Get the greeting message in uppercase (true) or not (false). Default is false.
50 - **Description**: Get the greeting message in uppercase (true) or not (false). Default is false.
43
51
44
52
45−**Response**:
53+#### Response:
46
54
47 - Status code 200
55 - Status code 200
48 - If you use ?capital=true
56 - If you use ?capital=true
@@ -61,14 +69,13 @@ GET /hello/:name
61}
69}
62```
70```
63
71
64−DELETE /greet/:greetid
72+## DELETE /greet/:greetid
65−----------------------
66
73
67−**Captures**:
74+#### Captures:
68
75
69- *greetid*: identifier of the greet msg to remove
76- *greetid*: identifier of the greet msg to remove
70
77
71−**Response**:
78+#### Response:
72
79
73 - Status code 204
80 - Status code 204
74 - No response body
81 - No response body
src/Servant/Docs.hs+108−13
@@ -1,4 +1,5 @@
1{-# LANGUAGE TypeFamilies #-}
1{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE TupleSections #-}
4{-# LANGUAGE TupleSections #-}
4{-# LANGUAGE TypeOperators #-}
5{-# LANGUAGE TypeOperators #-}
@@ -91,6 +92,7 @@ module Servant.Docs
91 , sampleByteStrings
92 , sampleByteStrings
92 , ToParam(..)
93 , ToParam(..)
93 , ToCapture(..)
94 , ToCapture(..)
95+ , ToIntro(..)
94
96
95 , -- * ADTs to represent an 'API'
97 , -- * ADTs to represent an 'API'
96 Method(..)
98 Method(..)
@@ -98,8 +100,10 @@ module Servant.Docs
98 , API, emptyAPI
100 , API, emptyAPI
99 , DocCapture(..), capSymbol, capDesc
101 , DocCapture(..), capSymbol, capDesc
100 , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind
102 , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind
103+ , DocNote(..), noteTitle, noteBody
104+ , DocIntro(..), Intro
101 , Response, respStatus, respBody, defResponse
105 , Response, respStatus, respBody, defResponse
102− , Action, captures, headers, params, rqbody, response, defAction
106+ , Action, captures, headers, notes, params, rqbody, response, defAction
103 , single
107 , single
104
108
105 , -- * Useful modules when defining your doc printers
109 , -- * Useful modules when defining your doc printers
@@ -110,6 +114,7 @@ module Servant.Docs
110import Control.Lens hiding (Action)
114import Control.Lens hiding (Action)
111import Data.Aeson
115import Data.Aeson
112import Data.Aeson.Encode.Pretty (encodePretty)
116import Data.Aeson.Encode.Pretty (encodePretty)
117+import Data.Ord(comparing)
113import Data.ByteString.Lazy.Char8 (ByteString)
118import Data.ByteString.Lazy.Char8 (ByteString)
114import Data.Hashable
119import Data.Hashable
115import Data.HashMap.Strict (HashMap)
120import Data.HashMap.Strict (HashMap)
@@ -188,12 +193,36 @@ defEndpoint = Endpoint [] DocGET
188
193
189instance Hashable Endpoint
194instance Hashable Endpoint
190
195
196+-- | Our API documentation type, a product of top-level information and a good
191−-- | Our API type, a good old hashmap from 'Endpoint' to 'Action'
197+-- old hashmap from 'Endpoint' to 'Action'
198+data API = API
199+ { _apiIntros :: [DocIntro]
192−type API = HashMap Endpoint Action
200+ , _apiEndpoints :: HashMap Endpoint Action
201+ } deriving (Eq, Show)
202+
203+instance Monoid API where
204+ API a1 b1 `mappend` API a2 b2 = API (a1 <> a2) (b1 <> b2)
205+ mempty = API mempty mempty
206+
207+-- | A way for a developer to insert an introductory paragraph manually. This
208+-- is not to be used in server or client facing API types.
209+--
210+-- Example:
211+--
212+-- > type OurAPI = "users" :> Get [User]
213+-- > type IntroducedAPI = Intro "of human bondage" :> OurAPI
214+-- >
215+-- > instance ToIntro "of human bondage" where
216+-- > toIntro = DocIntro "A title for the intro section"
217+-- > [ "A blob of text that will be at the top."
218+-- > , "List elements are paragraphs."
219+-- > ]
220+--
221+data Intro (name :: Symbol)
193
222
194-- | An empty 'API'
223-- | An empty 'API'
195emptyAPI :: API
224emptyAPI :: API
196−emptyAPI = HM.empty
225+emptyAPI = mempty
197
226
198-- | A type to represent captures. Holds the name of the capture
227-- | A type to represent captures. Holds the name of the capture
199-- and a description.
228-- and a description.
@@ -216,6 +245,26 @@ data DocQueryParam = DocQueryParam
216 , _paramKind :: ParamKind
245 , _paramKind :: ParamKind
217 } deriving (Eq, Show)
246 } deriving (Eq, Show)
218
247
248+-- | An introductory paragraph for your documentation. You can attach these
249+-- with the 'Intro' type.
250+--
251+data DocIntro = DocIntro
252+ { _introTitle :: String -- ^ Appears above the intro blob
253+ , _introBody :: [String] -- ^ Each String is a paragraph.
254+ } deriving (Eq, Show)
255+
256+instance Ord DocIntro where
257+ compare = comparing _introTitle
258+
259+-- | A type to represent extra notes that may be attached to an 'Action'.
260+--
261+-- This is intended to be used when writing your own HasDocs instances to
262+-- add extra sections to your endpoint's documentation.
263+data DocNote = DocNote
264+ { _noteTitle :: String
265+ , _noteBody :: [String]
266+ } deriving (Eq, Show)
267+
219-- | Type of GET parameter:
268-- | Type of GET parameter:
220--
269--
221-- - Normal corresponds to @QueryParam@, i.e your usual GET parameter
270-- - Normal corresponds to @QueryParam@, i.e your usual GET parameter
@@ -268,6 +317,7 @@ data Action = Action
268 { _captures :: [DocCapture] -- type collected + user supplied info
317 { _captures :: [DocCapture] -- type collected + user supplied info
269 , _headers :: [Text] -- type collected
318 , _headers :: [Text] -- type collected
270 , _params :: [DocQueryParam] -- type collected + user supplied info
319 , _params :: [DocQueryParam] -- type collected + user supplied info
320+ , _notes :: [DocNote] -- user supplied
271 , _rqbody :: Maybe ByteString -- user supplied
321 , _rqbody :: Maybe ByteString -- user supplied
272 , _response :: Response -- user supplied
322 , _response :: Response -- user supplied
273 } deriving (Eq, Show)
323 } deriving (Eq, Show)
@@ -284,6 +334,7 @@ data Action = Action
284defAction :: Action
334defAction :: Action
285defAction =
335defAction =
286 Action []
336 Action []
337+ []
287 []
338 []
288 []
339 []
289 Nothing
340 Nothing
@@ -293,12 +344,15 @@ defAction =
293-- 'API' is a 'Monoid', so combine multiple endpoints with
344-- 'API' is a 'Monoid', so combine multiple endpoints with
294-- 'mappend' or '<>'.
345-- 'mappend' or '<>'.
295single :: Endpoint -> Action -> API
346single :: Endpoint -> Action -> API
296−single = HM.singleton
347+single e a = API mempty (HM.singleton e a)
297
348
298-- gimme some lenses
349-- gimme some lenses
350+makeLenses ''API
299makeLenses ''Endpoint
351makeLenses ''Endpoint
300makeLenses ''DocCapture
352makeLenses ''DocCapture
301makeLenses ''DocQueryParam
353makeLenses ''DocQueryParam
354+makeLenses ''DocIntro
355+makeLenses ''DocNote
302makeLenses ''Response
356makeLenses ''Response
303makeLenses ''Action
357makeLenses ''Action
304
358
@@ -382,16 +436,26 @@ class ToParam t where
382class ToCapture c where
436class ToCapture c where
383 toCapture :: Proxy c -> DocCapture
437 toCapture :: Proxy c -> DocCapture
384
438
439+-- | The class to define the contents of an 'Intro'
440+-- Example of an instance:
441+--
442+-- > instance ToIntro "an intro" where
443+-- > toIntro _ = DocIntro "This is some text"
444+class ToIntro (intro :: Symbol) where
445+ toIntro :: Proxy intro -> DocIntro
446+
385-- | Generate documentation in Markdown format for
447-- | Generate documentation in Markdown format for
386-- the given 'API'.
448-- the given 'API'.
387markdown :: API -> String
449markdown :: API -> String
450+markdown api = unlines $
451+ introsStr (api ^. apiIntros)
388−markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
452+ ++ (concatMap (uncurry printEndpoint) . HM.toList $ api ^. apiEndpoints)
389
453
390 where printEndpoint :: Endpoint -> Action -> [String]
454 where printEndpoint :: Endpoint -> Action -> [String]
391 printEndpoint endpoint action =
455 printEndpoint endpoint action =
392 str :
456 str :
393− replicate len '-' :
394 "" :
457 "" :
458+ notesStr (action ^. notes) ++
395 capturesStr (action ^. captures) ++
459 capturesStr (action ^. captures) ++
396 headersStr (action ^. headers) ++
460 headersStr (action ^. headers) ++
397 paramsStr (action ^. params) ++
461 paramsStr (action ^. params) ++
@@ -399,13 +463,35 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
399 responseStr (action ^. response) ++
463 responseStr (action ^. response) ++
400 []
464 []
401
465
402− where str = show (endpoint^.method) ++ " " ++ showPath (endpoint^.path)
466+ where str = "## " ++ show (endpoint^.method)
403− len = length str
467+ ++ " " ++ showPath (endpoint^.path)
468+
469+ introsStr :: [DocIntro] -> [String]
470+ introsStr = concatMap introStr
471+
472+ introStr :: DocIntro -> [String]
473+ introStr i =
474+ ("#### " ++ i ^. introTitle) :
475+ "" :
476+ intersperse "" (i ^. introBody) ++
477+ "" :
478+ []
479+
480+ notesStr :: [DocNote] -> [String]
481+ notesStr = concatMap noteStr
482+
483+ noteStr :: DocNote -> [String]
484+ noteStr nt =
485+ ("#### " ++ nt ^. noteTitle) :
486+ "" :
487+ intersperse "" (nt ^. noteBody) ++
488+ "" :
489+ []
404
490
405 capturesStr :: [DocCapture] -> [String]
491 capturesStr :: [DocCapture] -> [String]
406 capturesStr [] = []
492 capturesStr [] = []
407 capturesStr l =
493 capturesStr l =
408− "**Captures**: " :
494+ "#### Captures:" :
409 "" :
495 "" :
410 map captureStr l ++
496 map captureStr l ++
411 "" :
497 "" :
@@ -423,7 +509,7 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
423 paramsStr :: [DocQueryParam] -> [String]
509 paramsStr :: [DocQueryParam] -> [String]
424 paramsStr [] = []
510 paramsStr [] = []
425 paramsStr l =
511 paramsStr l =
426− "**GET Parameters**: " :
512+ "#### GET Parameters:" :
427 "" :
513 "" :
428 map paramStr l ++
514 map paramStr l ++
429 "" :
515 "" :
@@ -448,7 +534,7 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
448 rqbodyStr :: Maybe ByteString -> [String]
534 rqbodyStr :: Maybe ByteString -> [String]
449 rqbodyStr Nothing = []
535 rqbodyStr Nothing = []
450 rqbodyStr (Just b) =
536 rqbodyStr (Just b) =
451− "**Request Body**: " :
537+ "#### Request Body:" :
452 jsonStr b
538 jsonStr b
453
539
454 jsonStr b =
540 jsonStr b =
@@ -461,7 +547,7 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
461
547
462 responseStr :: Response -> [String]
548 responseStr :: Response -> [String]
463 responseStr resp =
549 responseStr resp =
464− "**Response**: " :
550+ "#### Response:" :
465 "" :
551 "" :
466 (" - Status code " ++ show (resp ^. respStatus)) :
552 (" - Status code " ++ show (resp ^. respStatus)) :
467 bodies
553 bodies
@@ -607,6 +693,15 @@ instance (KnownSymbol path, HasDocs sublayout) => HasDocs (path :> sublayout) wh
607 endpoint' = endpoint & path <>~ [symbolVal pa]
693 endpoint' = endpoint & path <>~ [symbolVal pa]
608 pa = Proxy :: Proxy path
694 pa = Proxy :: Proxy path
609
695
696+instance (KnownSymbol intro, HasDocs sublayout, ToIntro intro)
697+ => HasDocs (Intro intro :> sublayout) where
698+
699+ docsFor Proxy x =
700+ docsFor sublayoutP x & apiIntros %~ (toIntro intro <|)
701+ where sublayoutP = Proxy :: Proxy sublayout
702+ intro :: Proxy intro
703+ intro = Proxy
704+
610{-
705{-
611
706
612-- | Serve your API's docs as markdown embedded in an html \<pre> tag.
707-- | Serve your API's docs as markdown embedded in an html \<pre> tag.