1{-# LANGUAGE TypeFamilies #-}
2{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE TemplateHaskell #-}
5-------------------------------------------------------------------------------
7-- Module : Servant.Docs
9-- Maintainer : alpmestan@gmail.com
10-- Stability : provisional
11-- Portability : TH, TypeFamilies, DeriveGeneric
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':
16-- @docs :: 'HasDocs' api => 'Proxy' api -> 'API'@
18-- You can then call 'printMarkdown' on it:
20-- @printMarkdown :: 'API' -> IO ()@
22-- or define a custom pretty printer:
24-- @yourPrettyDocs :: 'API' -> IO () -- or blaze-html's HTML, or ...@
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.
29-- Here's a little (but complete) example that you can run to see the
30-- markdown pretty printer in action:
32-- > {-# LANGUAGE DataKinds #-}
33-- > {-# LANGUAGE PolyKinds #-}
34-- > {-# LANGUAGE TypeFamilies #-}
35-- > {-# LANGUAGE DeriveGeneric #-}
36-- > {-# LANGUAGE TypeOperators #-}
37-- > {-# LANGUAGE FlexibleInstances #-}
38-- > {-# LANGUAGE OverloadedStrings #-}
43-- > import Servant.Docs
45-- > -- our type for a Greeting message
46-- > data Greet = Greet { _msg :: Text }
47-- > deriving (Generic, Show)
49-- > -- we get our JSON serialization for free
50-- > instance FromJSON Greet
51-- > instance ToJSON Greet
53-- > -- we provide a sample value for the 'Greet' type
54-- > instance ToSample Greet where
55-- > toSample Proxy = Just (encode g)
57-- > where g = Greet "Hello, haskeller!"
59-- > instance ToParam (QueryParam "capital" Bool) where
61-- > DocQueryParam "capital"
63-- > "Get the greeting message in uppercase (true) or not (false). Default is false."
65-- > instance ToCapture (Capture "name" Text) where
66-- > toCapture _ = DocCapture "name" "name of the person to greet"
68-- > instance ToCapture (Capture "greetid" Text) where
69-- > toCapture _ = DocCapture "greetid" "identifier of the greet msg to remove"
71-- > -- API specification
73-- > "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get Greet
74-- > :<|> "greet" :> RQBody Greet :> Post Greet
75-- > :<|> "delete" :> Capture "greetid" Text :> Delete
77-- > testApi :: Proxy TestApi
80-- > -- Generate the Documentation's ADT
82-- > greetDocs = docs testApi
85-- > main = printMarkdown greetDocs
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'
95 , Endpoint, path, method, defEndpoint
97 , DocCapture(..), capSymbol, capDesc
98 , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind
99 , Response, respStatus, respBody, defResponse
100 , Action, captures, params, rqbody, response, defAction
103 , -- * Useful modules when defining your own instances
108import Control.Lens hiding (Action)
109import Control.Monad (when)
111import Data.ByteString.Lazy.Char8 (ByteString)
113import Data.HashMap.Strict (HashMap)
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
131 show DocPOST = "POST"
132 show DocDELETE = "DELETE"
135instance Hashable Method
137-- | An 'Endpoint' type that holds the 'path' and the 'method'.
139-- Gets used as the key in the 'API' hashmap. Modify 'defEndpoint'
140-- or any 'Endpoint' value you want using the 'path' and 'method'
146-- λ> 'defEndpoint' & 'path' '<>~' "foo"
148-- λ> 'defEndpoint' & 'path' '<>~' "foo" & 'method' '.~' 'DocPOST'
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) =
160-- | An 'Endpoint' whose path is `"/"` and whose method is 'DocGET'
162-- Here's how you can modify it:
167-- λ> 'defEndpoint' & 'path' '<>~' "foo"
169-- λ> 'defEndpoint' & 'path' '<>~' "foo" & 'method' '.~' 'DocPOST'
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
184-- | A type to represent captures. Holds the name of the capture
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.
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:
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
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.
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.
221-- Can be tweaked with two lenses.
224-- > Response {_respStatus = 200, _respBody = Nothing}
225-- > λ> defResponse & respStatus .~ 204 & respBody .~ Just "[]"
226-- > Response {_respStatus = 204, _respBody = Just "[]"}
227data Response = Response
229 , _respBody :: Maybe ByteString
230 } deriving (Eq, Show)
232-- | Default response: status code 200, no response body.
234-- Can be tweaked with two lenses.
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:
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')
251-- You can tweak an 'Action' (like the default 'defAction') with these lenses
252-- to transform an action and add some information to it.
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'.
263-- Tweakable with lenses.
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}}
276-- | Create an API that's comprised of a single endpoint.
277-- 'API' is a 'Monoid', so combine multiple endpoints with
279single :: Endpoint -> Action -> API
284makeLenses ''DocCapture
285makeLenses ''DocQueryParam
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:
301-- - expect a request body, or
302-- - return a non empty response body
304-- Example of an instance:
306-- > {-# LANGUAGE DeriveGeneric #-}
307-- > {-# LANGUAGE OverloadedStrings #-}
309-- > import Data.Aeson
311-- > import GHC.Generics
313-- > data Greet = Greet { _msg :: Text }
314-- > deriving (Generic, Show)
316-- > instance FromJSON Greet
317-- > instance ToJSON Greet
319-- > instance ToSample Greet where
320-- > toSample Proxy = Just (encode g)
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.
329-- Example of an instance:
331-- > instance ToParam (QueryParam "capital" Bool) where
333-- > DocQueryParam "capital"
334-- > ["true", "false"]
335-- > "Get the greeting message in uppercase (true) or not (false). Default is false."
337 toParam :: Proxy t -> DocQueryParam
339-- | The class that helps us automatically get documentation
342-- Example of an instance:
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
356 putStrLn $ replicate len '-'
358 capturesStr (action ^. captures)
359 paramsStr (action ^. params)
360 rqbodyStr (action ^. rqbody)
361 responseStr (action ^. response)
363 where str = show (endpoint^.method) ++ " " ++ endpoint^.path
366 capturesStr :: [DocCapture] -> IO ()
367 capturesStr [] = return ()
369 putStrLn "**Captures**: "
374 putStrLn $ "- *" ++ (cap ^. capSymbol) ++ "*: " ++ (cap ^. capDesc)
376 paramsStr :: [DocQueryParam] -> IO ()
377 paramsStr [] = return ()
379 putStrLn "**GET Parameters**: "
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**: "
404 putStrLn "``` javascript"
409 responseStr :: Response -> IO ()
410 responseStr resp = do
411 putStrLn $ "**Response**: "
413 putStrLn $ " - Status code " ++ show (resp ^. respStatus)
415 maybe (putStrLn " - No response body\n")
416 (\b -> putStrLn " - Response body as below." >> jsonStr b)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list