on 0802221 help sign in
417 lines 14 kB history raw
1{-# LANGUAGE TypeFamilies #-}
2{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE TemplateHaskell #-}
5-------------------------------------------------------------------------------
6-- |
7-- Module : Servant.Docs
8-- License : BSD-style
9-- Maintainer : alpmestan@gmail.com
10-- Stability : provisional
11-- Portability : TH, TypeFamilies, DeriveGeneric
12--
13-- This module lets you get API docs for free. It lets generate
14-- an 'API' from the type that represents your API using 'docs':
15--
16-- @docs :: 'HasDocs' api => 'Proxy' api -> 'API'@
17--
18-- You can then call 'printMarkdown' on it:
19--
20-- @printMarkdown :: 'API' -> IO ()@
21--
22-- or define a custom pretty printer:
23--
24-- @yourPrettyDocs :: 'API' -> IO () -- or blaze-html's HTML, or ...@
25--
26-- The only thing you'll need to do will be to implement some classes
27-- for your captures, get parameters and request or response bodies.
28--
29-- Here's a little (but complete) example that you can run to see the
30-- markdown pretty printer in action:
31--
32-- > {-# LANGUAGE DataKinds #-}
33-- > {-# LANGUAGE PolyKinds #-}
34-- > {-# LANGUAGE TypeFamilies #-}
35-- > {-# LANGUAGE DeriveGeneric #-}
36-- > {-# LANGUAGE TypeOperators #-}
37-- > {-# LANGUAGE FlexibleInstances #-}
38-- > {-# LANGUAGE OverloadedStrings #-}
39-- >
40-- > import Data.Proxy
41-- > import Data.Text
42-- > import Servant
43-- > import Servant.Docs
44-- >
45-- > -- our type for a Greeting message
46-- > data Greet = Greet { _msg :: Text }
47-- > deriving (Generic, Show)
48-- >
49-- > -- we get our JSON serialization for free
50-- > instance FromJSON Greet
51-- > instance ToJSON Greet
52-- >
53-- > -- we provide a sample value for the 'Greet' type
54-- > instance ToSample Greet where
55-- > toSample Proxy = Just (encode g)
56-- >
57-- > where g = Greet "Hello, haskeller!"
58-- >
59-- > instance ToParam (QueryParam "capital" Bool) where
60-- > toParam _ =
61-- > DocQueryParam "capital"
62-- > ["true", "false"]
63-- > "Get the greeting message in uppercase (true) or not (false). Default is false."
64-- >
65-- > instance ToCapture (Capture "name" Text) where
66-- > toCapture _ = DocCapture "name" "name of the person to greet"
67-- >
68-- > instance ToCapture (Capture "greetid" Text) where
69-- > toCapture _ = DocCapture "greetid" "identifier of the greet msg to remove"
70-- >
71-- > -- API specification
72-- > type TestApi =
73-- > "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get Greet
74-- > :<|> "greet" :> RQBody Greet :> Post Greet
75-- > :<|> "delete" :> Capture "greetid" Text :> Delete
76-- >
77-- > testApi :: Proxy TestApi
78-- > testApi = Proxy
79-- >
80-- > -- Generate the Documentation's ADT
81-- > greetDocs :: API
82-- > greetDocs = docs testApi
83-- >
84-- > main :: IO ()
85-- > main = printMarkdown greetDocs
86module Servant.Docs
87 ( -- * 'HasDocs' class and key functions
88 HasDocs(..), docs, printMarkdown
90 , -- * Classes you need to implement for your types
91 ToSample(..), ToParam(..), ToCapture(..)
93 , -- * ADTs to represent an 'API'
94 Method(..)
95 , Endpoint, path, method, defEndpoint
96 , API, emptyAPI
97 , DocCapture(..), capSymbol, capDesc
98 , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind
99 , Response, respStatus, respBody, defResponse
100 , Action, captures, params, rqbody, response, defAction
101 , single
103 , -- * Useful modules when defining your own instances
104 module Control.Lens
105 , module Data.Monoid
106 ) where
108import Control.Lens hiding (Action)
109import Control.Monad (when)
110import Data.Aeson
111import Data.ByteString.Lazy.Char8 (ByteString)
112import Data.Hashable
113import Data.HashMap.Strict (HashMap)
114import Data.List
115import Data.Monoid
116import Data.Proxy
117import GHC.Generics
119import qualified Data.ByteString.Lazy.Char8 as LB
120import qualified Data.HashMap.Strict as HM
122-- | Supported HTTP request methods
123data Method = DocDELETE -- ^ the DELETE method
124 | DocGET -- ^ the GET method
125 | DocPOST -- ^ the POST method
126 | DocPUT -- ^ the PUT method
127 deriving (Eq, Generic)
129instance Show Method where
130 show DocGET = "GET"
131 show DocPOST = "POST"
132 show DocDELETE = "DELETE"
133 show DocPUT = "PUT"
135instance Hashable Method
137-- | An 'Endpoint' type that holds the 'path' and the 'method'.
138--
139-- Gets used as the key in the 'API' hashmap. Modify 'defEndpoint'
140-- or any 'Endpoint' value you want using the 'path' and 'method'
141-- lenses to tweak.
142--
143-- @
144-- λ> 'defEndpoint'
145-- GET /
146-- λ> 'defEndpoint' & 'path' '<>~' "foo"
147-- GET /foo
148-- λ> 'defEndpoint' & 'path' '<>~' "foo" & 'method' '.~' 'DocPOST'
149-- POST /foo
150-- @
151data Endpoint = Endpoint
152 { _path :: String -- type collected
153 , _method :: Method -- type collected
154 } deriving (Eq, Generic)
156instance Show Endpoint where
157 show (Endpoint p m) =
158 show m ++ " " ++ p
160-- | An 'Endpoint' whose path is `"/"` and whose method is 'DocGET'
161--
162-- Here's how you can modify it:
163--
164-- @
165-- λ> 'defEndpoint'
166-- GET /
167-- λ> 'defEndpoint' & 'path' '<>~' "foo"
168-- GET /foo
169-- λ> 'defEndpoint' & 'path' '<>~' "foo" & 'method' '.~' 'DocPOST'
170-- POST /foo
171-- @
172defEndpoint :: Endpoint
173defEndpoint = Endpoint "/" DocGET
175instance Hashable Endpoint
177-- | Our API type, a good old hashmap from 'Endpoint' to 'Action'
178type API = HashMap Endpoint Action
180-- | An empty 'API'
181emptyAPI :: API
182emptyAPI = HM.empty
184-- | A type to represent captures. Holds the name of the capture
185-- and a description.
186--
187-- Write a 'ToCapture' instance for your captured types.
188data DocCapture = DocCapture
189 { _capSymbol :: String -- type supplied
190 , _capDesc :: String -- user supplied
191 } deriving (Eq, Show)
193-- | A type to represent a /GET/ parameter from the Query String. Holds its name,
194-- the possible values (leave empty if there isn't a finite number of them),
195-- and a description of how it influences the output or behavior.
196--
197-- Write a 'ToParam' instance for your GET parameter types
198data DocQueryParam = DocQueryParam
199 { _paramName :: String -- type supplied
200 , _paramValues :: [String] -- user supplied
201 , _paramDesc :: String -- user supplied
202 , _paramKind :: ParamKind
203 } deriving (Eq, Show)
205-- | Type of GET parameter:
206--
207-- - Normal corresponds to @QueryParam@, i.e your usual GET parameter
208-- - List corresponds to @QueryParams@, i.e GET parameters with multiple values
209-- - Flag corresponds to @QueryFlag@, i.e a value-less GET parameter
210data ParamKind = Normal | List | Flag
211 deriving (Eq, Show)
213-- | A type to represent an HTTP response. Has an 'Int' status and
214-- a 'Maybe ByteString' response body. Tweak 'defResponse' using
215-- the 'respStatus' and 'respBody' lenses if you want.
216--
217-- If you want to respond with a non-empty response body, you'll most likely
218-- want to write a 'ToSample' instance for the type that'll be represented
219-- as some JSON in the response.
220--
221-- Can be tweaked with two lenses.
222--
223-- > λ> defResponse
224-- > Response {_respStatus = 200, _respBody = Nothing}
225-- > λ> defResponse & respStatus .~ 204 & respBody .~ Just "[]"
226-- > Response {_respStatus = 204, _respBody = Just "[]"}
227data Response = Response
228 { _respStatus :: Int
229 , _respBody :: Maybe ByteString
230 } deriving (Eq, Show)
232-- | Default response: status code 200, no response body.
233--
234-- Can be tweaked with two lenses.
235--
236-- > λ> defResponse
237-- > Response {_respStatus = 200, _respBody = Nothing}
238-- > λ> defResponse & respStatus .~ 204 & respBody .~ Just "[]"
239-- > Response {_respStatus = 204, _respBody = Just "[]"}
240defResponse :: Response
241defResponse = Response 200 Nothing
243-- | A datatype that represents everything that can happen
244-- at an endpoint, with its lenses:
245--
246-- - List of captures ('captures')
247-- - List of GET parameters ('params')
248-- - What the request body should look like, if any is requested ('rqbody')
249-- - What the response should be if everything goes well ('response')
250--
251-- You can tweak an 'Action' (like the default 'defAction') with these lenses
252-- to transform an action and add some information to it.
253data Action = Action
254 { _captures :: [DocCapture] -- type collected + user supplied info
255 , _params :: [DocQueryParam] -- type collected + user supplied info
256 , _rqbody :: Maybe ByteString -- user supplied
257 , _response :: Response -- user supplied
258 } deriving (Eq, Show)
260-- Default 'Action'. Has no 'captures', no GET 'params', expects
261-- no request body ('rqbody') and the typical response is 'defResponse'.
262--
263-- Tweakable with lenses.
264--
265-- > λ> defAction
266-- > Action {_captures = [], _params = [], _rqbody = Nothing, _response = Response {_respStatus = 200, _respBody = Nothing}}
267-- > λ> defAction & response.respStatus .~ 201
268-- > Action {_captures = [], _params = [], _rqbody = Nothing, _response = Response {_respStatus = 201, _respBody = Nothing}}
269defAction :: Action
270defAction =
271 Action []
272 []
273 Nothing
274 defResponse
276-- | Create an API that's comprised of a single endpoint.
277-- 'API' is a 'Monoid', so combine multiple endpoints with
278-- 'mappend' or '<>'.
279single :: Endpoint -> Action -> API
280single = HM.singleton
282-- gimme some lenses
283makeLenses ''Endpoint
284makeLenses ''DocCapture
285makeLenses ''DocQueryParam
286makeLenses ''Response
287makeLenses ''Action
289-- | Generate the docs for a given API that implements 'HasDocs'.
290docs :: HasDocs layout => Proxy layout -> API
291docs p = docsFor p (defEndpoint, defAction)
293-- | The class that abstracts away the impact of API combinators
294-- on documentation generation.
295class HasDocs layout where
296 docsFor :: Proxy layout -> (Endpoint, Action) -> API
298-- | The class that lets us display a sample JSON input or output
299-- when generating documentation for endpoints that either:
300--
301-- - expect a request body, or
302-- - return a non empty response body
303--
304-- Example of an instance:
305--
306-- > {-# LANGUAGE DeriveGeneric #-}
307-- > {-# LANGUAGE OverloadedStrings #-}
308-- >
309-- > import Data.Aeson
310-- > import Data.Text
311-- > import GHC.Generics
312-- >
313-- > data Greet = Greet { _msg :: Text }
314-- > deriving (Generic, Show)
315-- >
316-- > instance FromJSON Greet
317-- > instance ToJSON Greet
318-- >
319-- > instance ToSample Greet where
320-- > toSample Proxy = Just (encode g)
321-- >
322-- > where g = Greet "Hello, haskeller!"
323class ToJSON a => ToSample a where
324 toSample :: Proxy a -> Maybe ByteString
326-- | The class that helps us automatically get documentation
327-- for GET parameters.
328--
329-- Example of an instance:
330--
331-- > instance ToParam (QueryParam "capital" Bool) where
332-- > toParam _ =
333-- > DocQueryParam "capital"
334-- > ["true", "false"]
335-- > "Get the greeting message in uppercase (true) or not (false). Default is false."
336class ToParam t where
337 toParam :: Proxy t -> DocQueryParam
339-- | The class that helps us automatically get documentation
340-- for URL captures.
341--
342-- Example of an instance:
343--
344-- > instance ToCapture (Capture "name" Text) where
345-- > toCapture _ = DocCapture "name" "name of the person to greet"
346class ToCapture c where
347 toCapture :: Proxy c -> DocCapture
349-- | Print documentation in Markdown format for
350-- the given 'API', on standard output.
351printMarkdown :: API -> IO ()
352printMarkdown = imapM_ printEndpoint
354 where printEndpoint endpoint action = do
355 putStrLn $ str
356 putStrLn $ replicate len '-'
357 putStrLn ""
358 capturesStr (action ^. captures)
359 paramsStr (action ^. params)
360 rqbodyStr (action ^. rqbody)
361 responseStr (action ^. response)
363 where str = show (endpoint^.method) ++ " " ++ endpoint^.path
364 len = length str
366 capturesStr :: [DocCapture] -> IO ()
367 capturesStr [] = return ()
368 capturesStr l = do
369 putStrLn "**Captures**: "
370 putStrLn ""
371 mapM_ captureStr l
372 putStrLn ""
373 captureStr cap =
374 putStrLn $ "- *" ++ (cap ^. capSymbol) ++ "*: " ++ (cap ^. capDesc)
376 paramsStr :: [DocQueryParam] -> IO ()
377 paramsStr [] = return ()
378 paramsStr l = do
379 putStrLn "**GET Parameters**: "
380 putStrLn ""
381 mapM_ paramStr l
382 putStrLn ""
383 paramStr param = do
384 putStrLn $ " - " ++ param ^. paramName
385 when (not (null values) || param ^. paramKind /= Flag) $
386 putStrLn $ " - **Values**: *" ++ intercalate ", " values ++ "*"
387 putStrLn $ " - **Description**: " ++ param ^. paramDesc
388 when (param ^. paramKind == List) $
389 putStrLn $ " - This parameter is a **list**. All GET parameters with the name "
390 ++ param ^. paramName ++ "[] will forward their values in a list to the handler."
391 when (param ^. paramKind == Flag) $
392 putStrLn $ " - This parameter is a **flag**. This means no value is expected to be associated to this parameter."
394 where values = param ^. paramValues
396 rqbodyStr :: Maybe ByteString -> IO ()
397 rqbodyStr Nothing = return ()
398 rqbodyStr (Just b) = do
399 putStrLn "**Request Body**: "
400 jsonStr b
402 jsonStr b = do
403 putStrLn ""
404 putStrLn "``` javascript"
405 LB.putStrLn b
406 putStrLn "```"
407 putStrLn ""
409 responseStr :: Response -> IO ()
410 responseStr resp = do
411 putStrLn $ "**Response**: "
412 putStrLn $ ""
413 putStrLn $ " - Status code " ++ show (resp ^. respStatus)
414 resp ^. respBody &
415 maybe (putStrLn " - No response body\n")
416 (\b -> putStrLn " - Response body as below." >> jsonStr b)