@@ -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 #-}
@@ -14,7 +15,12 @@
15-- @docs :: 'HasDocs' api => 'Proxy' api -> 'API'@
16-- @docs :: 'HasDocs' api => 'Proxy' api -> 'API'@
18+-- Alternately, if you wish to add one or more introductions to your
19+-- documentation, use 'docsWithIntros':
21+-- @docsWithIntros :: 'HasDocs' api => [DocIntro] -> 'Proxy' api -> 'API'@
17−-- You can then call 'markdown' on it:
23+-- You can then call 'markdown' on the 'API' value:
19-- @markdown :: 'API' -> String@
25-- @markdown :: 'API' -> String@
@@ -29,61 +35,100 @@
29-- markdown pretty printer in action:
35-- markdown pretty printer in action:
31-- > {-# LANGUAGE DataKinds #-}
37-- > {-# LANGUAGE DataKinds #-}
32−-- > {-# LANGUAGE PolyKinds #-}
33−-- > {-# LANGUAGE TypeFamilies #-}
34-- > {-# LANGUAGE DeriveGeneric #-}
38-- > {-# LANGUAGE DeriveGeneric #-}
35-- > {-# LANGUAGE TypeOperators #-}
39-- > {-# LANGUAGE TypeOperators #-}
36-- > {-# LANGUAGE FlexibleInstances #-}
40-- > {-# LANGUAGE FlexibleInstances #-}
37-- > {-# LANGUAGE OverloadedStrings #-}
41-- > {-# LANGUAGE OverloadedStrings #-}
38−-- >
42+-- > {-# OPTIONS_GHC -fno-warn-orphans #-}
43+-- > import Data.Aeson
39-- > import Data.Proxy
44-- > import Data.Proxy
40−-- > import Data.Text
45+-- > import Data.Text(Text)
46+-- > import GHC.Generics
41−-- > import Servant
47+-- > import Servant.API
48+-- > import Servant.Docs
43−-- > -- our type for a Greeting message
50+-- > -- * Example
52+-- > -- | A greet message data type
44−-- > data Greet = Greet { _msg :: Text }
53+-- > newtype Greet = Greet Text
45-- > deriving (Generic, Show)
54-- > deriving (Generic, Show)
47−-- > -- we get our JSON serialization for free
48-- > instance FromJSON Greet
56-- > instance FromJSON Greet
49-- > instance ToJSON Greet
57-- > instance ToJSON Greet
51−-- > -- we provide a sample value for the 'Greet' type
59+-- > -- We add some useful annotations to our captures,
60+-- > -- query parameters and request body to make the docs
61+-- > -- really helpful.
52−-- > instance ToSample Greet where
62+-- > instance ToCapture (Capture "name" Text) where
53−-- > toSample = Just g
63+-- > toCapture _ = DocCapture "name" "name of the person to greet"
55−-- > where g = Greet "Hello, haskeller!"
65+-- > instance ToCapture (Capture "greetid" Text) where
66+-- > toCapture _ = DocCapture "greetid" "identifier of the greet msg to remove"
57-- > instance ToParam (QueryParam "capital" Bool) where
68-- > instance ToParam (QueryParam "capital" Bool) where
58-- > toParam _ =
69-- > toParam _ =
59-- > DocQueryParam "capital"
70-- > DocQueryParam "capital"
60-- > ["true", "false"]
71-- > ["true", "false"]
61−-- > "Get the greeting message in uppercase (true) or not (false). Default is false."
72+-- > "Get the greeting message in uppercase (true) or not (false).\
73+-- > \Default is false."
63−-- > instance ToCapture (Capture "name" Text) where
76+-- > instance ToParam (MatrixParam "lang" String) where
64−-- > toCapture _ = DocCapture "name" "name of the person to greet"
77+-- > toParam _ =
78+-- > DocQueryParam "lang"
79+-- > ["en", "sv", "fr"]
80+-- > "Get the greeting message selected language. Default is en."
83+-- > instance ToSample Greet where
84+-- > toSample = Just $ Greet "Hello, haskeller!"
87+-- > [ ("If you use ?capital=true", Greet "HELLO, HASKELLER")
88+-- > , ("If you use ?capital=false", Greet "Hello, haskeller")
91+-- > intro1 :: DocIntro
92+-- > intro1 = DocIntro "On proper introductions." -- The title
94+-- > , "As documentation is usually written for humans, it's often useful \
95+-- > \to introduce concepts with a few words." ] -- Elements are paragraphs
97+-- > intro2 :: DocIntro
98+-- > intro2 = DocIntro "This title is below the last"
99+-- > [ "You'll also note that multiple intros are possible." ]
66−-- > instance ToCapture (Capture "greetid" Text) where
67−-- > toCapture _ = DocCapture "greetid" "identifier of the greet msg to remove"
69-- > -- API specification
102-- > -- API specification
70-- > type TestApi =
103-- > type TestApi =
104+-- > -- GET /hello/:name?capital={true, false} returns a Greet as JSON
71−-- > "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get Greet
105+-- > "hello" :> MatrixParam "lang" String :> Capture "name" Text :> QueryParam "capital" Bool :> Get Greet
107+-- > -- POST /greet with a Greet as JSON in the request body,
108+-- > -- returns a Greet as JSON
72−-- > :<|> "greet" :> RQBody Greet :> Post Greet
109+-- > :<|> "greet" :> ReqBody Greet :> Post Greet
111+-- > -- DELETE /greet/:greetid
73−-- > :<|> "delete" :> Capture "greetid" Text :> Delete
112+-- > :<|> "greet" :> Capture "greetid" Text :> Delete
75-- > testApi :: Proxy TestApi
114-- > testApi :: Proxy TestApi
76-- > testApi = Proxy
115-- > testApi = Proxy
78−-- > -- Generate the Documentation's ADT
117+-- > -- Generate the data that lets us have API docs. This
79−-- > greetDocs :: API
118+-- > -- is derived from the type as well as from
80−-- > greetDocs = docs testApi
119+-- > -- the 'ToCapture', 'ToParam' and 'ToSample' instances from above.
121+-- > -- If you didn't want intros you could just call:
123+-- > -- > docs testAPI
124+-- > docsGreet :: API
125+-- > docsGreet = docsWithIntros [intro1, intro2] testApi
82-- > main :: IO ()
127-- > main :: IO ()
83−-- > main = putStrLn $ markdown greetDocs
128+-- > main = putStrLn $ markdown docsGreet
84module Servant.Docs
129module Servant.Docs
85 ( -- * 'HasDocs' class and key functions
130 ( -- * 'HasDocs' class and key functions
86− HasDocs(..), docs, markdown
131+ HasDocs(..), docs, docsWithIntros, markdown
88 , -- * Classes you need to implement for your types
133 , -- * Classes you need to implement for your types
89 ToSample(..)
134 ToSample(..)
@@ -98,8 +143,10 @@ module Servant.Docs
98 , API, emptyAPI
143 , API, emptyAPI
99 , DocCapture(..), capSymbol, capDesc
144 , DocCapture(..), capSymbol, capDesc
100 , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind
145 , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind
146+ , DocNote(..), noteTitle, noteBody
101 , Response, respStatus, respBody, defResponse
148 , Response, respStatus, respBody, defResponse
102− , Action, captures, headers, params, rqbody, response, defAction
149+ , Action, captures, headers, notes, params, rqbody, response, defAction
105 , -- * Useful modules when defining your doc printers
152 , -- * Useful modules when defining your doc printers
@@ -110,6 +157,7 @@ module Servant.Docs
110import Control.Lens hiding (Action)
157import Control.Lens hiding (Action)
111import Data.Aeson
158import Data.Aeson
112import Data.Aeson.Encode.Pretty (encodePretty)
159import Data.Aeson.Encode.Pretty (encodePretty)
160+import Data.Ord(comparing)
113import Data.ByteString.Lazy.Char8 (ByteString)
161import Data.ByteString.Lazy.Char8 (ByteString)
114import Data.Hashable
162import Data.Hashable
115import Data.HashMap.Strict (HashMap)
163import Data.HashMap.Strict (HashMap)
@@ -188,12 +236,20 @@ defEndpoint = Endpoint [] DocGET
189instance Hashable Endpoint
237instance Hashable Endpoint
239+-- | 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'
240+-- old hashmap from 'Endpoint' to 'Action'
242+ { _apiIntros :: [DocIntro]
192−type API = HashMap Endpoint Action
243+ , _apiEndpoints :: HashMap Endpoint Action
244+ } deriving (Eq, Show)
246+instance Monoid API where
247+ API a1 b1 `mappend` API a2 b2 = API (a1 <> a2) (b1 <> b2)
248+ mempty = API mempty mempty
194-- | An empty 'API'
250-- | An empty 'API'
195emptyAPI :: API
251emptyAPI :: API
196−emptyAPI = HM.empty
252+emptyAPI = mempty
198-- | A type to represent captures. Holds the name of the capture
254-- | A type to represent captures. Holds the name of the capture
199-- and a description.
255-- and a description.
@@ -216,6 +272,24 @@ data DocQueryParam = DocQueryParam
216 , _paramKind :: ParamKind
272 , _paramKind :: ParamKind
217 } deriving (Eq, Show)
273 } deriving (Eq, Show)
275+-- | An introductory paragraph for your documentation. You can pass these to
277+data DocIntro = DocIntro
278+ { _introTitle :: String -- ^ Appears above the intro blob
279+ , _introBody :: [String] -- ^ Each String is a paragraph.
280+ } deriving (Eq, Show)
282+instance Ord DocIntro where
283+ compare = comparing _introTitle
285+-- | A type to represent extra notes that may be attached to an 'Action'.
287+-- This is intended to be used when writing your own HasDocs instances to
288+-- add extra sections to your endpoint's documentation.
289+data DocNote = DocNote
290+ { _noteTitle :: String
291+ , _noteBody :: [String]
292+ } deriving (Eq, Show)
220-- | Type of GET parameter:
294-- | Type of GET parameter:
@@ -269,6 +343,7 @@ data Action = Action
269 { _captures :: [DocCapture] -- type collected + user supplied info
343 { _captures :: [DocCapture] -- type collected + user supplied info
270 , _headers :: [Text] -- type collected
344 , _headers :: [Text] -- type collected
271 , _params :: [DocQueryParam] -- type collected + user supplied info
345 , _params :: [DocQueryParam] -- type collected + user supplied info
346+ , _notes :: [DocNote] -- user supplied
272 , _mxParams :: [(String, [DocQueryParam])] -- type collected + user supplied info
347 , _mxParams :: [(String, [DocQueryParam])] -- type collected + user supplied info
273 , _rqbody :: Maybe ByteString -- user supplied
348 , _rqbody :: Maybe ByteString -- user supplied
274 , _response :: Response -- user supplied
349 , _response :: Response -- user supplied
@@ -289,6 +364,7 @@ defAction =
293 defResponse
369 defResponse
@@ -296,19 +372,28 @@ defAction =
296-- 'API' is a 'Monoid', so combine multiple endpoints with
372-- 'API' is a 'Monoid', so combine multiple endpoints with
297-- 'mappend' or '<>'.
373-- 'mappend' or '<>'.
298single :: Endpoint -> Action -> API
374single :: Endpoint -> Action -> API
299−single = HM.singleton
375+single e a = API mempty (HM.singleton e a)
301-- gimme some lenses
377-- gimme some lenses
302makeLenses ''Endpoint
379makeLenses ''Endpoint
303makeLenses ''DocCapture
380makeLenses ''DocCapture
304makeLenses ''DocQueryParam
381makeLenses ''DocQueryParam
382+makeLenses ''DocIntro
305makeLenses ''Response
384makeLenses ''Response
306makeLenses ''Action
385makeLenses ''Action
308−-- | Generate the docs for a given API that implements 'HasDocs'.
387+-- | Generate the docs for a given API that implements 'HasDocs'. This is the
388+-- default way to create documentation.
309docs :: HasDocs layout => Proxy layout -> API
389docs :: HasDocs layout => Proxy layout -> API
310docs p = docsFor p (defEndpoint, defAction)
390docs p = docsFor p (defEndpoint, defAction)
392+-- | Generate the docs for a given API that implements 'HasDocs' with with any
393+-- number of introduction(s)
394+docsWithIntros :: HasDocs layout => [DocIntro] -> Proxy layout -> API
395+docsWithIntros intros p = docs p & apiIntros <>~ intros
312-- | The class that abstracts away the impact of API combinators
397-- | The class that abstracts away the impact of API combinators
313-- on documentation generation.
398-- on documentation generation.
314class HasDocs layout where
399class HasDocs layout where
@@ -388,13 +473,15 @@ class ToCapture c where
388-- | Generate documentation in Markdown format for
473-- | Generate documentation in Markdown format for
389-- the given 'API'.
474-- the given 'API'.
390markdown :: API -> String
475markdown :: API -> String
476+markdown api = unlines $
477+ introsStr (api ^. apiIntros)
391−markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
478+ ++ (concatMap (uncurry printEndpoint) . HM.toList $ api ^. apiEndpoints)
393 where printEndpoint :: Endpoint -> Action -> [String]
480 where printEndpoint :: Endpoint -> Action -> [String]
394 printEndpoint endpoint action =
481 printEndpoint endpoint action =
484+ notesStr (action ^. notes) ++
398 capturesStr (action ^. captures) ++
485 capturesStr (action ^. captures) ++
399 mxParamsStr (action ^. mxParams) ++
486 mxParamsStr (action ^. mxParams) ++
400 headersStr (action ^. headers) ++
487 headersStr (action ^. headers) ++
@@ -403,13 +490,35 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
403 responseStr (action ^. response) ++
490 responseStr (action ^. response) ++
406− where str = show (endpoint^.method) ++ " " ++ showPath (endpoint^.path)
493+ where str = "## " ++ show (endpoint^.method)
407− len = length str
494+ ++ " " ++ showPath (endpoint^.path)
496+ introsStr :: [DocIntro] -> [String]
497+ introsStr = concatMap introStr
499+ introStr :: DocIntro -> [String]
501+ ("#### " ++ i ^. introTitle) :
503+ intersperse "" (i ^. introBody) ++
507+ notesStr :: [DocNote] -> [String]
508+ notesStr = concatMap noteStr
510+ noteStr :: DocNote -> [String]
512+ ("#### " ++ nt ^. noteTitle) :
514+ intersperse "" (nt ^. noteBody) ++
409 capturesStr :: [DocCapture] -> [String]
518 capturesStr :: [DocCapture] -> [String]
410 capturesStr [] = []
519 capturesStr [] = []
411 capturesStr l =
520 capturesStr l =
412− "**Captures**: " :
521+ "#### Captures:" :
414 map captureStr l ++
523 map captureStr l ++
@@ -420,14 +529,14 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
420 mxParamsStr :: [(String, [DocQueryParam])] -> [String]
529 mxParamsStr :: [(String, [DocQueryParam])] -> [String]
421 mxParamsStr [] = []
530 mxParamsStr [] = []
422 mxParamsStr l =
531 mxParamsStr l =
423− "**Matrix Parameters**: " :
532+ "#### Matrix Parameters:" :
425 map segmentStr l ++
534 map segmentStr l ++
428 segmentStr :: (String, [DocQueryParam]) -> String
537 segmentStr :: (String, [DocQueryParam]) -> String
429 segmentStr (segment, l) = unlines $
538 segmentStr (segment, l) = unlines $
430− ("**" ++ segment ++ "**: ") :
539+ ("**" ++ segment ++ "**:") :
432 map paramStr l ++
541 map paramStr l ++
@@ -443,7 +552,7 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
443 paramsStr :: [DocQueryParam] -> [String]
552 paramsStr :: [DocQueryParam] -> [String]
444 paramsStr [] = []
553 paramsStr [] = []
445 paramsStr l =
554 paramsStr l =
446− "**GET Parameters**: " :
555+ "#### GET Parameters:" :
448 map paramStr l ++
557 map paramStr l ++
@@ -468,7 +577,7 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
468 rqbodyStr :: Maybe ByteString -> [String]
577 rqbodyStr :: Maybe ByteString -> [String]
469 rqbodyStr Nothing = []
578 rqbodyStr Nothing = []
470 rqbodyStr (Just b) =
579 rqbodyStr (Just b) =
471− "**Request Body**: " :
580+ "#### Request Body:" :
472 jsonStr b
581 jsonStr b
474 jsonStr b =
583 jsonStr b =
@@ -481,7 +590,7 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
482 responseStr :: Response -> [String]
591 responseStr :: Response -> [String]
483 responseStr resp =
592 responseStr resp =
484− "**Response**: " :
593+ "#### Response:" :
486 (" - Status code " ++ show (resp ^. respStatus)) :
595 (" - Status code " ++ show (resp ^. respStatus)) :