Merge pull request #9 from anchor/wip-notes

Add DocIntro and DocNote types to allow extra docs
This commit is contained in:
Christian Marie 2015-01-30 16:06:31 +11:00
commit 2ea8a48eff
3 changed files with 211 additions and 78 deletions

View file

@ -6,7 +6,7 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
import Data.Aeson import Data.Aeson
import Data.Proxy import Data.Proxy
import Data.Text import Data.Text(Text)
import GHC.Generics import GHC.Generics
import Servant.API import Servant.API
import Servant.Docs import Servant.Docs
@ -14,7 +14,7 @@ import Servant.Docs
-- * Example -- * Example
-- | A greet message data type -- | A greet message data type
newtype Greet = Greet { msg :: Text } newtype Greet = Greet Text
deriving (Generic, Show) deriving (Generic, Show)
instance FromJSON Greet instance FromJSON Greet
@ -33,7 +33,8 @@ instance ToParam (QueryParam "capital" Bool) where
toParam _ = toParam _ =
DocQueryParam "capital" DocQueryParam "capital"
["true", "false"] ["true", "false"]
"Get the greeting message in uppercase (true) or not (false). Default is false." "Get the greeting message in uppercase (true) or not (false).\
\Default is false."
Normal Normal
instance ToParam (MatrixParam "lang" String) where instance ToParam (MatrixParam "lang" String) where
@ -51,6 +52,17 @@ instance ToSample Greet where
, ("If you use ?capital=false", Greet "Hello, haskeller") , ("If you use ?capital=false", Greet "Hello, haskeller")
] ]
intro1 :: DocIntro
intro1 = DocIntro "On proper introductions." -- The title
[ "Hello there."
, "As documentation is usually written for humans, it's often useful \
\to introduce concepts with a few words." ] -- Elements are paragraphs
intro2 :: DocIntro
intro2 = DocIntro "This title is below the last"
[ "You'll also note that multiple intros are possible." ]
-- API specification -- API specification
type TestApi = type TestApi =
-- GET /hello/:name?capital={true, false} returns a Greet as JSON -- GET /hello/:name?capital={true, false} returns a Greet as JSON
@ -69,8 +81,12 @@ testApi = Proxy
-- Generate the data that lets us have API docs. This -- Generate the data that lets us have API docs. This
-- is derived from the type as well as from -- is derived from the type as well as from
-- the 'ToCapture', 'ToParam' and 'ToSample' instances from above. -- the 'ToCapture', 'ToParam' and 'ToSample' instances from above.
--
-- If you didn't want intros you could just call:
--
-- > docs testAPI
docsGreet :: API docsGreet :: API
docsGreet = docs testApi docsGreet = docsWithIntros [intro1, intro2] testApi
main :: IO () main :: IO ()
main = putStrLn $ markdown docsGreet main = putStrLn $ markdown docsGreet

View file

@ -1,74 +1,82 @@
POST /greet #### On proper introductions.
-----------
**Request Body**: Hello there.
As documentation is usually written for humans, it's often useful to introduce concepts with a few words.
#### This title is below the last
You'll also note that multiple intros are possible.
## POST /greet
#### Request Body:
``` javascript ``` javascript
{ "Hello, haskeller!"
"msg": "Hello, haskeller!"
}
``` ```
**Response**: #### Response:
- Status code 201 - Status code 201
- If you use ?capital=true - If you use ?capital=true
``` javascript ``` javascript
{ "HELLO, HASKELLER"
"msg": "HELLO, HASKELLER"
}
``` ```
- If you use ?capital=false - If you use ?capital=false
``` javascript ``` javascript
{ "Hello, haskeller"
"msg": "Hello, haskeller"
}
``` ```
GET /hello/:name ## GET /hello;lang=<value>/:name
----------------
**Captures**: #### Captures:
- *name*: name of the person to greet - *name*: name of the person to greet
**GET Parameters**: #### Matrix Parameters**:
**hello**:
- lang
- **Values**: *en, sv, fr*
- **Description**: Get the greeting message selected language. Default is en.
#### GET Parameters:
- capital - capital
- **Values**: *true, false* - **Values**: *true, false*
- **Description**: Get the greeting message in uppercase (true) or not (false). Default is false. - **Description**: Get the greeting message in uppercase (true) or not (false).Default is false.
**Response**: #### Response:
- Status code 200 - Status code 200
- If you use ?capital=true - If you use ?capital=true
``` javascript ``` javascript
{ "HELLO, HASKELLER"
"msg": "HELLO, HASKELLER"
}
``` ```
- If you use ?capital=false - If you use ?capital=false
``` javascript ``` javascript
{ "Hello, haskeller"
"msg": "Hello, haskeller"
}
``` ```
DELETE /greet/:greetid ## DELETE /greet/:greetid
----------------------
**Captures**: #### Captures:
- *greetid*: identifier of the greet msg to remove - *greetid*: identifier of the greet msg to remove
**Response**: #### Response:
- Status code 204 - Status code 204
- No response body - No response body

View file

@ -1,4 +1,5 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
@ -14,7 +15,12 @@
-- --
-- @docs :: 'HasDocs' api => 'Proxy' api -> 'API'@ -- @docs :: 'HasDocs' api => 'Proxy' api -> 'API'@
-- --
-- You can then call 'markdown' on it: -- Alternately, if you wish to add one or more introductions to your
-- documentation, use 'docsWithIntros':
--
-- @docsWithIntros :: 'HasDocs' api => [DocIntro] -> 'Proxy' api -> 'API'@
--
-- You can then call 'markdown' on the 'API' value:
-- --
-- @markdown :: 'API' -> String@ -- @markdown :: 'API' -> String@
-- --
@ -29,61 +35,100 @@
-- markdown pretty printer in action: -- markdown pretty printer in action:
-- --
-- > {-# LANGUAGE DataKinds #-} -- > {-# LANGUAGE DataKinds #-}
-- > {-# LANGUAGE PolyKinds #-}
-- > {-# LANGUAGE TypeFamilies #-}
-- > {-# LANGUAGE DeriveGeneric #-} -- > {-# LANGUAGE DeriveGeneric #-}
-- > {-# LANGUAGE TypeOperators #-} -- > {-# LANGUAGE TypeOperators #-}
-- > {-# LANGUAGE FlexibleInstances #-} -- > {-# LANGUAGE FlexibleInstances #-}
-- > {-# LANGUAGE OverloadedStrings #-} -- > {-# LANGUAGE OverloadedStrings #-}
-- > -- > {-# OPTIONS_GHC -fno-warn-orphans #-}
-- > import Data.Aeson
-- > import Data.Proxy -- > import Data.Proxy
-- > import Data.Text -- > import Data.Text(Text)
-- > import Servant -- > import GHC.Generics
-- > import Servant.API
-- > import Servant.Docs
-- > -- >
-- > -- our type for a Greeting message -- > -- * Example
-- > data Greet = Greet { _msg :: Text } -- >
-- > -- | A greet message data type
-- > newtype Greet = Greet Text
-- > deriving (Generic, Show) -- > deriving (Generic, Show)
-- > -- >
-- > -- we get our JSON serialization for free
-- > instance FromJSON Greet -- > instance FromJSON Greet
-- > instance ToJSON Greet -- > instance ToJSON Greet
-- > -- >
-- > -- we provide a sample value for the 'Greet' type -- > -- We add some useful annotations to our captures,
-- > instance ToSample Greet where -- > -- query parameters and request body to make the docs
-- > toSample = Just g -- > -- really helpful.
-- >
-- > where g = Greet "Hello, haskeller!"
-- >
-- > instance ToParam (QueryParam "capital" Bool) where
-- > toParam _ =
-- > DocQueryParam "capital"
-- > ["true", "false"]
-- > "Get the greeting message in uppercase (true) or not (false). Default is false."
-- >
-- > instance ToCapture (Capture "name" Text) where -- > instance ToCapture (Capture "name" Text) where
-- > toCapture _ = DocCapture "name" "name of the person to greet" -- > toCapture _ = DocCapture "name" "name of the person to greet"
-- > -- >
-- > instance ToCapture (Capture "greetid" Text) where -- > instance ToCapture (Capture "greetid" Text) where
-- > toCapture _ = DocCapture "greetid" "identifier of the greet msg to remove" -- > toCapture _ = DocCapture "greetid" "identifier of the greet msg to remove"
-- > -- >
-- > instance ToParam (QueryParam "capital" Bool) where
-- > toParam _ =
-- > DocQueryParam "capital"
-- > ["true", "false"]
-- > "Get the greeting message in uppercase (true) or not (false).\
-- > \Default is false."
-- > Normal
-- >
-- > instance ToParam (MatrixParam "lang" String) where
-- > toParam _ =
-- > DocQueryParam "lang"
-- > ["en", "sv", "fr"]
-- > "Get the greeting message selected language. Default is en."
-- > Normal
-- >
-- > instance ToSample Greet where
-- > toSample = Just $ Greet "Hello, haskeller!"
-- >
-- > toSamples =
-- > [ ("If you use ?capital=true", Greet "HELLO, HASKELLER")
-- > , ("If you use ?capital=false", Greet "Hello, haskeller")
-- > ]
-- >
-- > intro1 :: DocIntro
-- > intro1 = DocIntro "On proper introductions." -- The title
-- > [ "Hello there."
-- > , "As documentation is usually written for humans, it's often useful \
-- > \to introduce concepts with a few words." ] -- Elements are paragraphs
-- >
-- > intro2 :: DocIntro
-- > intro2 = DocIntro "This title is below the last"
-- > [ "You'll also note that multiple intros are possible." ]
-- >
-- >
-- > -- API specification -- > -- API specification
-- > type TestApi = -- > type TestApi =
-- > "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get Greet -- > -- GET /hello/:name?capital={true, false} returns a Greet as JSON
-- > :<|> "greet" :> RQBody Greet :> Post Greet -- > "hello" :> MatrixParam "lang" String :> Capture "name" Text :> QueryParam "capital" Bool :> Get Greet
-- > :<|> "delete" :> Capture "greetid" Text :> Delete -- >
-- > -- POST /greet with a Greet as JSON in the request body,
-- > -- returns a Greet as JSON
-- > :<|> "greet" :> ReqBody Greet :> Post Greet
-- >
-- > -- DELETE /greet/:greetid
-- > :<|> "greet" :> Capture "greetid" Text :> Delete
-- > -- >
-- > testApi :: Proxy TestApi -- > testApi :: Proxy TestApi
-- > testApi = Proxy -- > testApi = Proxy
-- > -- >
-- > -- Generate the Documentation's ADT -- > -- Generate the data that lets us have API docs. This
-- > greetDocs :: API -- > -- is derived from the type as well as from
-- > greetDocs = docs testApi -- > -- the 'ToCapture', 'ToParam' and 'ToSample' instances from above.
-- > --
-- > -- If you didn't want intros you could just call:
-- > --
-- > -- > docs testAPI
-- > docsGreet :: API
-- > docsGreet = docsWithIntros [intro1, intro2] testApi
-- > -- >
-- > main :: IO () -- > main :: IO ()
-- > main = putStrLn $ markdown greetDocs -- > main = putStrLn $ markdown docsGreet
module Servant.Docs module Servant.Docs
( -- * 'HasDocs' class and key functions ( -- * 'HasDocs' class and key functions
HasDocs(..), docs, markdown HasDocs(..), docs, docsWithIntros, markdown
, -- * Classes you need to implement for your types , -- * Classes you need to implement for your types
ToSample(..) ToSample(..)
@ -98,8 +143,10 @@ module Servant.Docs
, API, emptyAPI , API, emptyAPI
, DocCapture(..), capSymbol, capDesc , DocCapture(..), capSymbol, capDesc
, DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind
, DocNote(..), noteTitle, noteBody
, DocIntro(..)
, Response, respStatus, respBody, defResponse , Response, respStatus, respBody, defResponse
, Action, captures, headers, params, rqbody, response, defAction , Action, captures, headers, notes, params, rqbody, response, defAction
, single , single
, -- * Useful modules when defining your doc printers , -- * Useful modules when defining your doc printers
@ -110,6 +157,7 @@ module Servant.Docs
import Control.Lens hiding (Action) import Control.Lens hiding (Action)
import Data.Aeson import Data.Aeson
import Data.Aeson.Encode.Pretty (encodePretty) import Data.Aeson.Encode.Pretty (encodePretty)
import Data.Ord(comparing)
import Data.ByteString.Lazy.Char8 (ByteString) import Data.ByteString.Lazy.Char8 (ByteString)
import Data.Hashable import Data.Hashable
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
@ -188,12 +236,20 @@ defEndpoint = Endpoint [] DocGET
instance Hashable Endpoint instance Hashable Endpoint
-- | Our API type, a good old hashmap from 'Endpoint' to 'Action' -- | Our API documentation type, a product of top-level information and a good
type API = HashMap Endpoint Action -- old hashmap from 'Endpoint' to 'Action'
data API = API
{ _apiIntros :: [DocIntro]
, _apiEndpoints :: HashMap Endpoint Action
} deriving (Eq, Show)
instance Monoid API where
API a1 b1 `mappend` API a2 b2 = API (a1 <> a2) (b1 <> b2)
mempty = API mempty mempty
-- | An empty 'API' -- | An empty 'API'
emptyAPI :: API emptyAPI :: API
emptyAPI = HM.empty emptyAPI = mempty
-- | A type to represent captures. Holds the name of the capture -- | A type to represent captures. Holds the name of the capture
-- and a description. -- and a description.
@ -216,6 +272,24 @@ data DocQueryParam = DocQueryParam
, _paramKind :: ParamKind , _paramKind :: ParamKind
} deriving (Eq, Show) } deriving (Eq, Show)
-- | An introductory paragraph for your documentation. You can pass these to
-- 'docsWithIntros'.
data DocIntro = DocIntro
{ _introTitle :: String -- ^ Appears above the intro blob
, _introBody :: [String] -- ^ Each String is a paragraph.
} deriving (Eq, Show)
instance Ord DocIntro where
compare = comparing _introTitle
-- | A type to represent extra notes that may be attached to an 'Action'.
--
-- This is intended to be used when writing your own HasDocs instances to
-- add extra sections to your endpoint's documentation.
data DocNote = DocNote
{ _noteTitle :: String
, _noteBody :: [String]
} deriving (Eq, Show)
-- | Type of GET parameter: -- | Type of GET parameter:
-- --
@ -269,6 +343,7 @@ data Action = Action
{ _captures :: [DocCapture] -- type collected + user supplied info { _captures :: [DocCapture] -- type collected + user supplied info
, _headers :: [Text] -- type collected , _headers :: [Text] -- type collected
, _params :: [DocQueryParam] -- type collected + user supplied info , _params :: [DocQueryParam] -- type collected + user supplied info
, _notes :: [DocNote] -- user supplied
, _mxParams :: [(String, [DocQueryParam])] -- type collected + user supplied info , _mxParams :: [(String, [DocQueryParam])] -- type collected + user supplied info
, _rqbody :: Maybe ByteString -- user supplied , _rqbody :: Maybe ByteString -- user supplied
, _response :: Response -- user supplied , _response :: Response -- user supplied
@ -289,6 +364,7 @@ defAction =
[] []
[] []
[] []
[]
Nothing Nothing
defResponse defResponse
@ -296,19 +372,28 @@ defAction =
-- 'API' is a 'Monoid', so combine multiple endpoints with -- 'API' is a 'Monoid', so combine multiple endpoints with
-- 'mappend' or '<>'. -- 'mappend' or '<>'.
single :: Endpoint -> Action -> API single :: Endpoint -> Action -> API
single = HM.singleton single e a = API mempty (HM.singleton e a)
-- gimme some lenses -- gimme some lenses
makeLenses ''API
makeLenses ''Endpoint makeLenses ''Endpoint
makeLenses ''DocCapture makeLenses ''DocCapture
makeLenses ''DocQueryParam makeLenses ''DocQueryParam
makeLenses ''DocIntro
makeLenses ''DocNote
makeLenses ''Response makeLenses ''Response
makeLenses ''Action makeLenses ''Action
-- | Generate the docs for a given API that implements 'HasDocs'. -- | Generate the docs for a given API that implements 'HasDocs'. This is the
-- default way to create documentation.
docs :: HasDocs layout => Proxy layout -> API docs :: HasDocs layout => Proxy layout -> API
docs p = docsFor p (defEndpoint, defAction) docs p = docsFor p (defEndpoint, defAction)
-- | Generate the docs for a given API that implements 'HasDocs' with with any
-- number of introduction(s)
docsWithIntros :: HasDocs layout => [DocIntro] -> Proxy layout -> API
docsWithIntros intros p = docs p & apiIntros <>~ intros
-- | The class that abstracts away the impact of API combinators -- | The class that abstracts away the impact of API combinators
-- on documentation generation. -- on documentation generation.
class HasDocs layout where class HasDocs layout where
@ -388,13 +473,15 @@ class ToCapture c where
-- | Generate documentation in Markdown format for -- | Generate documentation in Markdown format for
-- the given 'API'. -- the given 'API'.
markdown :: API -> String markdown :: API -> String
markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList markdown api = unlines $
introsStr (api ^. apiIntros)
++ (concatMap (uncurry printEndpoint) . HM.toList $ api ^. apiEndpoints)
where printEndpoint :: Endpoint -> Action -> [String] where printEndpoint :: Endpoint -> Action -> [String]
printEndpoint endpoint action = printEndpoint endpoint action =
str : str :
replicate len '-' :
"" : "" :
notesStr (action ^. notes) ++
capturesStr (action ^. captures) ++ capturesStr (action ^. captures) ++
mxParamsStr (action ^. mxParams) ++ mxParamsStr (action ^. mxParams) ++
headersStr (action ^. headers) ++ headersStr (action ^. headers) ++
@ -403,13 +490,35 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
responseStr (action ^. response) ++ responseStr (action ^. response) ++
[] []
where str = show (endpoint^.method) ++ " " ++ showPath (endpoint^.path) where str = "## " ++ show (endpoint^.method)
len = length str ++ " " ++ showPath (endpoint^.path)
introsStr :: [DocIntro] -> [String]
introsStr = concatMap introStr
introStr :: DocIntro -> [String]
introStr i =
("#### " ++ i ^. introTitle) :
"" :
intersperse "" (i ^. introBody) ++
"" :
[]
notesStr :: [DocNote] -> [String]
notesStr = concatMap noteStr
noteStr :: DocNote -> [String]
noteStr nt =
("#### " ++ nt ^. noteTitle) :
"" :
intersperse "" (nt ^. noteBody) ++
"" :
[]
capturesStr :: [DocCapture] -> [String] capturesStr :: [DocCapture] -> [String]
capturesStr [] = [] capturesStr [] = []
capturesStr l = capturesStr l =
"**Captures**: " : "#### Captures:" :
"" : "" :
map captureStr l ++ map captureStr l ++
"" : "" :
@ -420,14 +529,14 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
mxParamsStr :: [(String, [DocQueryParam])] -> [String] mxParamsStr :: [(String, [DocQueryParam])] -> [String]
mxParamsStr [] = [] mxParamsStr [] = []
mxParamsStr l = mxParamsStr l =
"**Matrix Parameters**: " : "#### Matrix Parameters:" :
"" : "" :
map segmentStr l ++ map segmentStr l ++
"" : "" :
[] []
segmentStr :: (String, [DocQueryParam]) -> String segmentStr :: (String, [DocQueryParam]) -> String
segmentStr (segment, l) = unlines $ segmentStr (segment, l) = unlines $
("**" ++ segment ++ "**: ") : ("**" ++ segment ++ "**:") :
"" : "" :
map paramStr l ++ map paramStr l ++
"" : "" :
@ -443,7 +552,7 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
paramsStr :: [DocQueryParam] -> [String] paramsStr :: [DocQueryParam] -> [String]
paramsStr [] = [] paramsStr [] = []
paramsStr l = paramsStr l =
"**GET Parameters**: " : "#### GET Parameters:" :
"" : "" :
map paramStr l ++ map paramStr l ++
"" : "" :
@ -468,7 +577,7 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
rqbodyStr :: Maybe ByteString -> [String] rqbodyStr :: Maybe ByteString -> [String]
rqbodyStr Nothing = [] rqbodyStr Nothing = []
rqbodyStr (Just b) = rqbodyStr (Just b) =
"**Request Body**: " : "#### Request Body:" :
jsonStr b jsonStr b
jsonStr b = jsonStr b =
@ -481,7 +590,7 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
responseStr :: Response -> [String] responseStr :: Response -> [String]
responseStr resp = responseStr resp =
"**Response**: " : "#### Response:" :
"" : "" :
(" - Status code " ++ show (resp ^. respStatus)) : (" - Status code " ++ show (resp ^. respStatus)) :
bodies bodies