Merge pull request #829 from ivan-m/multiple-content-types
servant-docs: configurable rendering for multiple content types
This commit is contained in:
commit
f960d97256
7 changed files with 180 additions and 43 deletions
|
@ -23,6 +23,7 @@ import Network.Wai
|
||||||
import Servant.API
|
import Servant.API
|
||||||
import Servant.Docs
|
import Servant.Docs
|
||||||
import Servant.Server
|
import Servant.Server
|
||||||
|
import Web.FormUrlEncoded(FromForm(..), ToForm(..))
|
||||||
```
|
```
|
||||||
|
|
||||||
And we'll import some things from one of our earlier modules
|
And we'll import some things from one of our earlier modules
|
||||||
|
@ -110,12 +111,6 @@ markdown :: API -> String
|
||||||
That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`.
|
That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`.
|
||||||
|
|
||||||
````````` text
|
````````` text
|
||||||
## Welcome
|
|
||||||
|
|
||||||
This is our super webservice's API.
|
|
||||||
|
|
||||||
Enjoy!
|
|
||||||
|
|
||||||
## GET /hello
|
## GET /hello
|
||||||
|
|
||||||
#### GET Parameters:
|
#### GET Parameters:
|
||||||
|
@ -132,15 +127,16 @@ Enjoy!
|
||||||
|
|
||||||
- Supported content types are:
|
- Supported content types are:
|
||||||
|
|
||||||
|
- `application/json;charset=utf-8`
|
||||||
- `application/json`
|
- `application/json`
|
||||||
|
|
||||||
- When a value is provided for 'name'
|
- When a value is provided for 'name' (`application/json;charset=utf-8`, `application/json`):
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{"msg":"Hello, Alp"}
|
{"msg":"Hello, Alp"}
|
||||||
```
|
```
|
||||||
|
|
||||||
- When 'name' is not specified
|
- When 'name' is not specified (`application/json;charset=utf-8`, `application/json`):
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{"msg":"Hello, anonymous coward"}
|
{"msg":"Hello, anonymous coward"}
|
||||||
|
@ -152,24 +148,26 @@ Enjoy!
|
||||||
|
|
||||||
- Supported content types are:
|
- Supported content types are:
|
||||||
|
|
||||||
|
- `application/json;charset=utf-8`
|
||||||
- `application/json`
|
- `application/json`
|
||||||
|
|
||||||
- Example: `application/json`
|
- Example (`application/json;charset=utf-8`, `application/json`):
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"}
|
{"clientAge":26,"clientEmail":"alp@foo.com","clientName":"Alp","clientInterestedIn":["haskell","mathematics"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Response:
|
#### Response:
|
||||||
|
|
||||||
- Status code 201
|
- Status code 200
|
||||||
- Headers: []
|
- Headers: []
|
||||||
|
|
||||||
- Supported content types are:
|
- Supported content types are:
|
||||||
|
|
||||||
|
- `application/json;charset=utf-8`
|
||||||
- `application/json`
|
- `application/json`
|
||||||
|
|
||||||
- Response body as below.
|
- Example (`application/json;charset=utf-8`, `application/json`):
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{"subject":"Hey Alp, we miss you!","body":"Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"}
|
{"subject":"Hey Alp, we miss you!","body":"Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"}
|
||||||
|
@ -189,13 +187,14 @@ Enjoy!
|
||||||
|
|
||||||
- Supported content types are:
|
- Supported content types are:
|
||||||
|
|
||||||
|
- `application/json;charset=utf-8`
|
||||||
- `application/json`
|
- `application/json`
|
||||||
|
|
||||||
- Response body as below.
|
- Example (`application/json;charset=utf-8`, `application/json`):
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{"x":3,"y":14}
|
{"yCoord":14,"xCoord":3}
|
||||||
```
|
````
|
||||||
|
|
||||||
`````````
|
`````````
|
||||||
|
|
||||||
|
@ -213,6 +212,49 @@ docsBS = encodeUtf8
|
||||||
|
|
||||||
`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
|
`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
|
||||||
|
|
||||||
|
More customisation can be done with the `markdownWith` function, which allows customising some of the parameters used when generating Markdown. The most obvious of these is how to handle when a request or response body has multiple content types. For example, if we make a slight change to the `/marketing` endpoint of our API so that the request body can also be encoded as a form:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
type ExampleAPI2 = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
|
||||||
|
:<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
|
||||||
|
:<|> "marketing" :> ReqBody '[JSON, FormUrlEncoded] ClientInfo :> Post '[JSON] Email
|
||||||
|
|
||||||
|
instance ToForm ClientInfo
|
||||||
|
instance FromForm ClientInfo
|
||||||
|
|
||||||
|
exampleAPI2 :: Proxy ExampleAPI2
|
||||||
|
exampleAPI2 = Proxy
|
||||||
|
|
||||||
|
api2Docs :: API
|
||||||
|
api2Docs = docs exampleAPI2
|
||||||
|
```
|
||||||
|
|
||||||
|
The relevant output of `markdown api2Docs` is now:
|
||||||
|
|
||||||
|
```````` text
|
||||||
|
#### Request:
|
||||||
|
|
||||||
|
- Supported content types are:
|
||||||
|
|
||||||
|
- `application/json;charset=utf-8`
|
||||||
|
- `application/json`
|
||||||
|
- `application/x-www-form-urlencoded`
|
||||||
|
|
||||||
|
- Example (`application/json;charset=utf-8`, `application/json`):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{"clientAge":26,"clientEmail":"alp@foo.com","clientName":"Alp","clientInterestedIn":["haskell","mathematics"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Example (`application/x-www-form-urlencoded`):
|
||||||
|
|
||||||
|
```
|
||||||
|
clientAge=26&clientEmail=alp%40foo.com&clientName=Alp&clientInterestedIn=haskell&clientInterestedIn=mathematics
|
||||||
|
```
|
||||||
|
````````
|
||||||
|
|
||||||
|
If, however, you don't want the extra example encoding shown, then you can use `markdownWith (defRenderingOptions & requestExamples .~ FirstContentType)` to get behaviour identical to `markdown apiDocs`.
|
||||||
|
|
||||||
We can now serve the API *and* the API docs with a simple server.
|
We can now serve the API *and* the API docs with a simple server.
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
|
|
|
@ -31,6 +31,7 @@ library
|
||||||
, servant-docs == 0.11.*
|
, servant-docs == 0.11.*
|
||||||
, servant-js >= 0.9 && <0.10
|
, servant-js >= 0.9 && <0.10
|
||||||
, warp
|
, warp
|
||||||
|
, http-api-data
|
||||||
, http-media
|
, http-media
|
||||||
, lucid
|
, lucid
|
||||||
, time
|
, time
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
* Document the HTTP Method the parameters of an endpoint belong to
|
* Document the HTTP Method the parameters of an endpoint belong to
|
||||||
(rather than assuming `GET` for all of them).
|
(rather than assuming `GET` for all of them).
|
||||||
* Content type of sample response body is also displayed.
|
* Content type of sample response body is also displayed.
|
||||||
|
* Can now control how many content-types for each example are shown
|
||||||
|
with `markdownWith` and `RenderingOptions`.
|
||||||
|
|
||||||
0.11
|
0.11
|
||||||
----
|
----
|
||||||
|
|
|
@ -49,7 +49,7 @@ library
|
||||||
, control-monad-omega == 0.3.*
|
, control-monad-omega == 0.3.*
|
||||||
if !impl(ghc >= 8.0)
|
if !impl(ghc >= 8.0)
|
||||||
build-depends:
|
build-depends:
|
||||||
semigroups >=0.16.2.2 && <0.19
|
semigroups >=0.17 && <0.19
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
|
|
|
@ -24,6 +24,9 @@
|
||||||
module Servant.Docs
|
module Servant.Docs
|
||||||
( -- * 'HasDocs' class and key functions
|
( -- * 'HasDocs' class and key functions
|
||||||
HasDocs(..), docs, pretty, markdown
|
HasDocs(..), docs, pretty, markdown
|
||||||
|
-- ** Customising generated documentation
|
||||||
|
, markdownWith, RenderingOptions(..), defRenderingOptions
|
||||||
|
, requestExamples, responseExamples, ShowContentTypes(..)
|
||||||
-- * Generating docs with extra information
|
-- * Generating docs with extra information
|
||||||
, docsWith, docsWithIntros, docsWithOptions
|
, docsWith, docsWithIntros, docsWithOptions
|
||||||
, ExtraInfo(..), extraInfo
|
, ExtraInfo(..), extraInfo
|
||||||
|
|
|
@ -22,22 +22,29 @@ module Servant.Docs.Internal where
|
||||||
|
|
||||||
import Prelude ()
|
import Prelude ()
|
||||||
import Prelude.Compat
|
import Prelude.Compat
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import Control.Arrow (second)
|
import Control.Arrow (second)
|
||||||
import Control.Lens (makeLenses, mapped, over, traversed, view, (%~),
|
import Control.Lens (makeLenses, mapped, over,
|
||||||
(&), (.~), (<>~), (^.), (|>))
|
traversed, view, (%~), (&), (.~),
|
||||||
|
(<>~), (^.), (|>))
|
||||||
import qualified Control.Monad.Omega as Omega
|
import qualified Control.Monad.Omega as Omega
|
||||||
import Data.ByteString.Lazy.Char8 (ByteString)
|
|
||||||
import qualified Data.ByteString.Char8 as BSC
|
import qualified Data.ByteString.Char8 as BSC
|
||||||
|
import Data.ByteString.Lazy.Char8 (ByteString)
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
import Data.Foldable (fold)
|
||||||
import Data.Hashable (Hashable)
|
import Data.Hashable (Hashable)
|
||||||
import Data.HashMap.Strict (HashMap)
|
import Data.HashMap.Strict (HashMap)
|
||||||
import Data.List.Compat (intercalate, intersperse, sort)
|
import Data.List.Compat (intercalate, intersperse, sort)
|
||||||
|
import Data.List.NonEmpty (NonEmpty ((:|)), groupWith)
|
||||||
|
import qualified Data.List.NonEmpty as NE
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Monoid (All (..), Any (..), Sum (..), Product (..), First (..), Last (..), Dual (..))
|
import Data.Monoid (All (..), Any (..), Dual (..),
|
||||||
import Data.Semigroup (Semigroup (..))
|
First (..), Last (..),
|
||||||
|
Product (..), Sum (..))
|
||||||
import Data.Ord (comparing)
|
import Data.Ord (comparing)
|
||||||
import Data.Proxy (Proxy (Proxy))
|
import Data.Proxy (Proxy (Proxy))
|
||||||
|
import Data.Semigroup (Semigroup (..))
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Data.Text (Text, unpack)
|
import Data.Text (Text, unpack)
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
|
@ -291,6 +298,35 @@ defAction =
|
||||||
single :: Endpoint -> Action -> API
|
single :: Endpoint -> Action -> API
|
||||||
single e a = API mempty (HM.singleton e a)
|
single e a = API mempty (HM.singleton e a)
|
||||||
|
|
||||||
|
-- | How many content-types for each example should be shown?
|
||||||
|
--
|
||||||
|
-- @since 0.11.1
|
||||||
|
data ShowContentTypes = AllContentTypes -- ^ For each example, show each content type.
|
||||||
|
| FirstContentType -- ^ For each example, show only one content type.
|
||||||
|
deriving (Eq, Ord, Show, Read, Bounded, Enum)
|
||||||
|
|
||||||
|
-- | Customise how an 'API' is converted into documentation.
|
||||||
|
--
|
||||||
|
-- @since 0.11.1
|
||||||
|
data RenderingOptions = RenderingOptions
|
||||||
|
{ _requestExamples :: !ShowContentTypes
|
||||||
|
-- ^ How many content types to display for request body examples?
|
||||||
|
, _responseExamples :: !ShowContentTypes
|
||||||
|
-- ^ How many content types to display for response body examples?
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
|
-- | Default API generation options.
|
||||||
|
--
|
||||||
|
-- All content types are shown for both 'requestExamples' and
|
||||||
|
-- 'responseExamples'.
|
||||||
|
--
|
||||||
|
-- @since 0.11.1
|
||||||
|
defRenderingOptions :: RenderingOptions
|
||||||
|
defRenderingOptions = RenderingOptions
|
||||||
|
{ _requestExamples = AllContentTypes
|
||||||
|
, _responseExamples = AllContentTypes
|
||||||
|
}
|
||||||
|
|
||||||
-- gimme some lenses
|
-- gimme some lenses
|
||||||
makeLenses ''DocAuthentication
|
makeLenses ''DocAuthentication
|
||||||
makeLenses ''DocOptions
|
makeLenses ''DocOptions
|
||||||
|
@ -302,6 +338,7 @@ makeLenses ''DocIntro
|
||||||
makeLenses ''DocNote
|
makeLenses ''DocNote
|
||||||
makeLenses ''Response
|
makeLenses ''Response
|
||||||
makeLenses ''Action
|
makeLenses ''Action
|
||||||
|
makeLenses ''RenderingOptions
|
||||||
|
|
||||||
-- | Generate the docs for a given API that implements 'HasDocs'. This is the
|
-- | Generate the docs for a given API that implements 'HasDocs'. This is the
|
||||||
-- default way to create documentation.
|
-- default way to create documentation.
|
||||||
|
@ -517,8 +554,34 @@ class ToAuthInfo a where
|
||||||
|
|
||||||
-- | Generate documentation in Markdown format for
|
-- | Generate documentation in Markdown format for
|
||||||
-- the given 'API'.
|
-- the given 'API'.
|
||||||
|
--
|
||||||
|
-- This is equivalent to @'markdownWith' 'defRenderingOptions'@.
|
||||||
markdown :: API -> String
|
markdown :: API -> String
|
||||||
markdown api = unlines $
|
markdown = markdownWith defRenderingOptions
|
||||||
|
|
||||||
|
-- | Generate documentation in Markdown format for
|
||||||
|
-- the given 'API' using the specified options.
|
||||||
|
--
|
||||||
|
-- These options allow you to customise aspects such as:
|
||||||
|
--
|
||||||
|
-- * Choose how many content-types for each request body example are
|
||||||
|
-- shown with 'requestExamples'.
|
||||||
|
--
|
||||||
|
-- * Choose how many content-types for each response body example
|
||||||
|
-- are shown with 'responseExamples'.
|
||||||
|
--
|
||||||
|
-- For example, to only show the first content-type of each example:
|
||||||
|
--
|
||||||
|
-- @
|
||||||
|
-- markdownWith ('defRenderingOptions'
|
||||||
|
-- & 'requestExamples' '.~' 'FirstContentType'
|
||||||
|
-- & 'responseExamples' '.~' 'FirstContentType' )
|
||||||
|
-- myAPI
|
||||||
|
-- @
|
||||||
|
--
|
||||||
|
-- @since 0.11.1
|
||||||
|
markdownWith :: RenderingOptions -> API -> String
|
||||||
|
markdownWith RenderingOptions{..} api = unlines $
|
||||||
introsStr (api ^. apiIntros)
|
introsStr (api ^. apiIntros)
|
||||||
++ (concatMap (uncurry printEndpoint) . sort . HM.toList $ api ^. apiEndpoints)
|
++ (concatMap (uncurry printEndpoint) . sort . HM.toList $ api ^. apiEndpoints)
|
||||||
|
|
||||||
|
@ -632,26 +695,50 @@ markdown api = unlines $
|
||||||
rqbodyStr types s =
|
rqbodyStr types s =
|
||||||
["#### Request:", ""]
|
["#### Request:", ""]
|
||||||
<> formatTypes types
|
<> formatTypes types
|
||||||
<> concatMap formatBody s
|
<> formatBodies _requestExamples s
|
||||||
|
|
||||||
formatTypes [] = []
|
formatTypes [] = []
|
||||||
formatTypes ts = ["- Supported content types are:", ""]
|
formatTypes ts = ["- Supported content types are:", ""]
|
||||||
<> map (\t -> " - `" <> show t <> "`") ts
|
<> map (\t -> " - `" <> show t <> "`") ts
|
||||||
<> [""]
|
<> [""]
|
||||||
|
|
||||||
formatBody (t, m, b) =
|
-- This assumes that when the bodies are created, identical
|
||||||
"- Example (" <> cs t <> "): `" <> cs (show m) <> "`" :
|
-- labels and representations are located next to each other.
|
||||||
contentStr m b
|
formatBodies :: ShowContentTypes -> [(Text, M.MediaType, ByteString)] -> [String]
|
||||||
|
formatBodies ex bds = concatMap formatBody (select bodyGroups)
|
||||||
|
where
|
||||||
|
bodyGroups :: [(Text, NonEmpty M.MediaType, ByteString)]
|
||||||
|
bodyGroups =
|
||||||
|
map (\grps -> let (t,_,b) = NE.head grps in (t, fmap (\(_,m,_) -> m) grps, b))
|
||||||
|
. groupWith (\(t,_,b) -> (t,b))
|
||||||
|
$ bds
|
||||||
|
|
||||||
|
select = case ex of
|
||||||
|
AllContentTypes -> id
|
||||||
|
FirstContentType -> map (\(t,ms,b) -> (t, NE.head ms :| [], b))
|
||||||
|
|
||||||
|
formatBody :: (Text, NonEmpty M.MediaType, ByteString) -> [String]
|
||||||
|
formatBody (t, ms, b) =
|
||||||
|
"- " <> title <> " (" <> mediaList ms <> "):" :
|
||||||
|
contentStr (NE.head ms) b
|
||||||
|
where
|
||||||
|
mediaList = fold . NE.intersperse ", " . fmap (\m -> "`" ++ show m ++ "`")
|
||||||
|
|
||||||
|
title
|
||||||
|
| T.null t = "Example"
|
||||||
|
| otherwise = cs t
|
||||||
|
|
||||||
markdownForType mime_type =
|
markdownForType mime_type =
|
||||||
case (M.mainType mime_type, M.subType mime_type) of
|
case (M.mainType mime_type, M.subType mime_type) of
|
||||||
("text", "html") -> "html"
|
("text", "html") -> "html"
|
||||||
("application", "xml") -> "xml"
|
("application", "xml") -> "xml"
|
||||||
|
("text", "xml") -> "xml"
|
||||||
("application", "json") -> "javascript"
|
("application", "json") -> "javascript"
|
||||||
("application", "javascript") -> "javascript"
|
("application", "javascript") -> "javascript"
|
||||||
("text", "css") -> "css"
|
("text", "css") -> "css"
|
||||||
(_, _) -> ""
|
(_, _) -> ""
|
||||||
|
|
||||||
|
|
||||||
contentStr mime_type body =
|
contentStr mime_type body =
|
||||||
"" :
|
"" :
|
||||||
" ```" <> markdownForType mime_type :
|
" ```" <> markdownForType mime_type :
|
||||||
|
@ -674,7 +761,7 @@ markdown api = unlines $
|
||||||
[] -> ["- No response body\n"]
|
[] -> ["- No response body\n"]
|
||||||
[("", t, r)] -> "- Response body as below." : contentStr t r
|
[("", t, r)] -> "- Response body as below." : contentStr t r
|
||||||
xs ->
|
xs ->
|
||||||
concatMap (\(ctx, t, r) -> ("- " <> T.unpack ctx <> " (`" <> cs (show t) <> "`)") : contentStr t r) xs
|
formatBodies _responseExamples xs
|
||||||
|
|
||||||
-- * Instances
|
-- * Instances
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,11 @@ extra-deps:
|
||||||
- http-client-0.4.30
|
- http-client-0.4.30
|
||||||
- natural-transformation-0.4
|
- natural-transformation-0.4
|
||||||
- primitive-0.6.1.0
|
- primitive-0.6.1.0
|
||||||
|
- semigroups-0.17
|
||||||
- servant-js-0.9.3
|
- servant-js-0.9.3
|
||||||
- should-not-typecheck-2.1.0
|
- should-not-typecheck-2.1.0
|
||||||
- time-locale-compat-0.1.1.1
|
- time-locale-compat-0.1.1.1
|
||||||
- uri-bytestring-0.2.2.0
|
- uri-bytestring-0.2.2.0
|
||||||
|
- void-0.7.1
|
||||||
- wai-app-static-3.1.5
|
- wai-app-static-3.1.5
|
||||||
resolver: lts-2.22
|
resolver: lts-2.22
|
||||||
|
|
Loading…
Reference in a new issue