on master help sign in
Merge pull request #829 from ivan-m/multiple-content-types

servant-docs: configurable rendering for multiple content types

commitf960d97256bcc4159dd003d48461f12d8aa339d2 authorAlp Mestanogullari <alpmestan@gmail.com> dateMon 9 Oct 2017, 08:13 (9 years ago) parent63d8f6cb3e35fb
7 files changed+180−43
doc/tutorial/Docs.lhs+69−27 doc/tutorial/tutorial.cabal+1 servant-docs/CHANGELOG.md+2 servant-docs/servant-docs.cabal+1−1 servant-docs/src/Servant/Docs.hs+3 servant-docs/src/Servant/Docs/Internal.hs+102−15 stack-ghc-7.8.4.yaml+2
doc/tutorial/Docs.lhs+69−27
@@ -23,6 +23,7 @@ import Network.Wai
2323import Servant.API
2424import Servant.Docs
2525import Servant.Server
26+import Web.FormUrlEncoded(FromForm(..), ToForm(..))
2627```
2728
2829And we'll import some things from one of our earlier modules
@@ -110,12 +111,6 @@ markdown :: API -> String
110111That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`.
111112
112113````````` text
113−## Welcome
114−
115−This is our super webservice's API.
116−
117−Enjoy!
118−
119114## GET /hello
120115
121116#### GET Parameters:
@@ -132,19 +127,20 @@ Enjoy!
132127
133128- Supported content types are:
134129
130+ - `application/json;charset=utf-8`
135131 - `application/json`
136132
137−- When a value is provided for 'name'
133+- When a value is provided for 'name' (`application/json;charset=utf-8`, `application/json`):
138134
139− ```javascript
140− {"msg":"Hello, Alp"}
141− ```
135+ ```javascript
136+{"msg":"Hello, Alp"}
137+ ```
142138
143−- When 'name' is not specified
139+- When 'name' is not specified (`application/json;charset=utf-8`, `application/json`):
144140
145− ```javascript
146− {"msg":"Hello, anonymous coward"}
147− ```
141+ ```javascript
142+{"msg":"Hello, anonymous coward"}
143+ ```
148144
149145## POST /marketing
150146
@@ -152,28 +148,30 @@ Enjoy!
152148
153149- Supported content types are:
154150
151+ - `application/json;charset=utf-8`
155152 - `application/json`
156153
157−- Example: `application/json`
154+- Example (`application/json;charset=utf-8`, `application/json`):
158155
159− ```javascript
160− {"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"}
161− ```
156+ ```javascript
157+{"clientAge":26,"clientEmail":"alp@foo.com","clientName":"Alp","clientInterestedIn":["haskell","mathematics"]}
158+ ```
162159
163160#### Response:
164161
165−- Status code 201
162+- Status code 200
166163- Headers: []
167164
168165- Supported content types are:
169166
167+ - `application/json;charset=utf-8`
170168 - `application/json`
171169
172−- Response body as below.
170+- Example (`application/json;charset=utf-8`, `application/json`):
173171
174− ```javascript
175− {"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"}
176− ```
172+ ```javascript
173+{"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"}
174+ ```
177175
178176## GET /position/:x/:y
179177
@@ -189,13 +187,14 @@ Enjoy!
189187
190188- Supported content types are:
191189
190+ - `application/json;charset=utf-8`
192191 - `application/json`
193192
194−- Response body as below.
193+- Example (`application/json;charset=utf-8`, `application/json`):
195194
196− ```javascript
197− {"x":3,"y":14}
198− ```
195+ ```javascript
196+{"yCoord":14,"xCoord":3}
197+ ````
199198
200199`````````
201200
@@ -213,6 +212,49 @@ docsBS = encodeUtf8
213212
214213`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
215214
215+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:
216+
217+``` haskell
218+type ExampleAPI2 = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
219+ :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
220+ :<|> "marketing" :> ReqBody '[JSON, FormUrlEncoded] ClientInfo :> Post '[JSON] Email
221+
222+instance ToForm ClientInfo
223+instance FromForm ClientInfo
224+
225+exampleAPI2 :: Proxy ExampleAPI2
226+exampleAPI2 = Proxy
227+
228+api2Docs :: API
229+api2Docs = docs exampleAPI2
230+```
231+
232+The relevant output of `markdown api2Docs` is now:
233+
234+```````` text
235+#### Request:
236+
237+- Supported content types are:
238+
239+ - `application/json;charset=utf-8`
240+ - `application/json`
241+ - `application/x-www-form-urlencoded`
242+
243+- Example (`application/json;charset=utf-8`, `application/json`):
244+
245+ ```javascript
246+{"clientAge":26,"clientEmail":"alp@foo.com","clientName":"Alp","clientInterestedIn":["haskell","mathematics"]}
247+ ```
248+
249+- Example (`application/x-www-form-urlencoded`):
250+
251+ ```
252+clientAge=26&clientEmail=alp%40foo.com&clientName=Alp&clientInterestedIn=haskell&clientInterestedIn=mathematics
253+ ```
254+````````
255+
256+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`.
257+
216258We can now serve the API *and* the API docs with a simple server.
217259
218260``` haskell
doc/tutorial/tutorial.cabal+1
@@ -31,6 +31,7 @@ library
3131 , servant-docs == 0.11.*
3232 , servant-js >= 0.9 && <0.10
3333 , warp
34+ , http-api-data
3435 , http-media
3536 , lucid
3637 , time
servant-docs/CHANGELOG.md+2
@@ -7,6 +7,8 @@
77* Document the HTTP Method the parameters of an endpoint belong to
88 (rather than assuming `GET` for all of them).
99* Content type of sample response body is also displayed.
10+* Can now control how many content-types for each example are shown
11+ with `markdownWith` and `RenderingOptions`.
1012
11130.11
1214----
servant-docs/servant-docs.cabal+1−1
@@ -49,7 +49,7 @@ library
4949 , control-monad-omega == 0.3.*
5050 if !impl(ghc >= 8.0)
5151 build-depends:
52− semigroups >=0.16.2.2 && <0.19
52+ semigroups >=0.17 && <0.19
5353 hs-source-dirs: src
5454 default-language: Haskell2010
5555 ghc-options: -Wall
servant-docs/src/Servant/Docs.hs+3
@@ -24,6 +24,9 @@
2424module Servant.Docs
2525 ( -- * 'HasDocs' class and key functions
2626 HasDocs(..), docs, pretty, markdown
27+ -- ** Customising generated documentation
28+ , markdownWith, RenderingOptions(..), defRenderingOptions
29+ , requestExamples, responseExamples, ShowContentTypes(..)
2730 -- * Generating docs with extra information
2831 , docsWith, docsWithIntros, docsWithOptions
2932 , ExtraInfo(..), extraInfo
servant-docs/src/Servant/Docs/Internal.hs+102−15
@@ -20,24 +20,31 @@
2020#include "overlapping-compat.h"
2121module Servant.Docs.Internal where
2222
23−import Prelude ()
23+import Prelude ()
2424import Prelude.Compat
25+
2526import Control.Applicative
2627import Control.Arrow (second)
27−import Control.Lens (makeLenses, mapped, over, traversed, view, (%~),
28− (&), (.~), (<>~), (^.), (|>))
28+import Control.Lens (makeLenses, mapped, over,
29+ traversed, view, (%~), (&), (.~),
30+ (<>~), (^.), (|>))
2931import qualified Control.Monad.Omega as Omega
30−import Data.ByteString.Lazy.Char8 (ByteString)
3132import qualified Data.ByteString.Char8 as BSC
33+import Data.ByteString.Lazy.Char8 (ByteString)
3234import qualified Data.CaseInsensitive as CI
35+import Data.Foldable (fold)
3336import Data.Hashable (Hashable)
3437import Data.HashMap.Strict (HashMap)
3538import Data.List.Compat (intercalate, intersperse, sort)
39+import Data.List.NonEmpty (NonEmpty ((:|)), groupWith)
40+import qualified Data.List.NonEmpty as NE
3641import Data.Maybe
37−import Data.Monoid (All (..), Any (..), Sum (..), Product (..), First (..), Last (..), Dual (..))
38−import Data.Semigroup (Semigroup (..))
42+import Data.Monoid (All (..), Any (..), Dual (..),
43+ First (..), Last (..),
44+ Product (..), Sum (..))
3945import Data.Ord (comparing)
40−import Data.Proxy (Proxy(Proxy))
46+import Data.Proxy (Proxy (Proxy))
47+import Data.Semigroup (Semigroup (..))
4148import Data.String.Conversions (cs)
4249import Data.Text (Text, unpack)
4350import GHC.Generics
@@ -291,6 +298,35 @@ defAction =
291298single :: Endpoint -> Action -> API
292299single e a = API mempty (HM.singleton e a)
293300
301+-- | How many content-types for each example should be shown?
302+--
303+-- @since 0.11.1
304+data ShowContentTypes = AllContentTypes -- ^ For each example, show each content type.
305+ | FirstContentType -- ^ For each example, show only one content type.
306+ deriving (Eq, Ord, Show, Read, Bounded, Enum)
307+
308+-- | Customise how an 'API' is converted into documentation.
309+--
310+-- @since 0.11.1
311+data RenderingOptions = RenderingOptions
312+ { _requestExamples :: !ShowContentTypes
313+ -- ^ How many content types to display for request body examples?
314+ , _responseExamples :: !ShowContentTypes
315+ -- ^ How many content types to display for response body examples?
316+ } deriving (Show)
317+
318+-- | Default API generation options.
319+--
320+-- All content types are shown for both 'requestExamples' and
321+-- 'responseExamples'.
322+--
323+-- @since 0.11.1
324+defRenderingOptions :: RenderingOptions
325+defRenderingOptions = RenderingOptions
326+ { _requestExamples = AllContentTypes
327+ , _responseExamples = AllContentTypes
328+ }
329+
294330-- gimme some lenses
295331makeLenses ''DocAuthentication
296332makeLenses ''DocOptions
@@ -302,6 +338,7 @@ makeLenses ''DocIntro
302338makeLenses ''DocNote
303339makeLenses ''Response
304340makeLenses ''Action
341+makeLenses ''RenderingOptions
305342
306343-- | Generate the docs for a given API that implements 'HasDocs'. This is the
307344-- default way to create documentation.
@@ -517,8 +554,34 @@ class ToAuthInfo a where
517554
518555-- | Generate documentation in Markdown format for
519556-- the given 'API'.
557+--
558+-- This is equivalent to @'markdownWith' 'defRenderingOptions'@.
520559markdown :: API -> String
521−markdown api = unlines $
560+markdown = markdownWith defRenderingOptions
561+
562+-- | Generate documentation in Markdown format for
563+-- the given 'API' using the specified options.
564+--
565+-- These options allow you to customise aspects such as:
566+--
567+-- * Choose how many content-types for each request body example are
568+-- shown with 'requestExamples'.
569+--
570+-- * Choose how many content-types for each response body example
571+-- are shown with 'responseExamples'.
572+--
573+-- For example, to only show the first content-type of each example:
574+--
575+-- @
576+-- markdownWith ('defRenderingOptions'
577+-- & 'requestExamples' '.~' 'FirstContentType'
578+-- & 'responseExamples' '.~' 'FirstContentType' )
579+-- myAPI
580+-- @
581+--
582+-- @since 0.11.1
583+markdownWith :: RenderingOptions -> API -> String
584+markdownWith RenderingOptions{..} api = unlines $
522585 introsStr (api ^. apiIntros)
523586 ++ (concatMap (uncurry printEndpoint) . sort . HM.toList $ api ^. apiEndpoints)
524587
@@ -632,31 +695,55 @@ markdown api = unlines $
632695 rqbodyStr types s =
633696 ["#### Request:", ""]
634697 <> formatTypes types
635− <> concatMap formatBody s
698+ <> formatBodies _requestExamples s
636699
637700 formatTypes [] = []
638701 formatTypes ts = ["- Supported content types are:", ""]
639702 <> map (\t -> " - `" <> show t <> "`") ts
640703 <> [""]
641704
642− formatBody (t, m, b) =
643− "- Example (" <> cs t <> "): `" <> cs (show m) <> "`" :
644− contentStr m b
705+ -- This assumes that when the bodies are created, identical
706+ -- labels and representations are located next to each other.
707+ formatBodies :: ShowContentTypes -> [(Text, M.MediaType, ByteString)] -> [String]
708+ formatBodies ex bds = concatMap formatBody (select bodyGroups)
709+ where
710+ bodyGroups :: [(Text, NonEmpty M.MediaType, ByteString)]
711+ bodyGroups =
712+ map (\grps -> let (t,_,b) = NE.head grps in (t, fmap (\(_,m,_) -> m) grps, b))
713+ . groupWith (\(t,_,b) -> (t,b))
714+ $ bds
715+
716+ select = case ex of
717+ AllContentTypes -> id
718+ FirstContentType -> map (\(t,ms,b) -> (t, NE.head ms :| [], b))
719+
720+ formatBody :: (Text, NonEmpty M.MediaType, ByteString) -> [String]
721+ formatBody (t, ms, b) =
722+ "- " <> title <> " (" <> mediaList ms <> "):" :
723+ contentStr (NE.head ms) b
724+ where
725+ mediaList = fold . NE.intersperse ", " . fmap (\m -> "`" ++ show m ++ "`")
726+
727+ title
728+ | T.null t = "Example"
729+ | otherwise = cs t
645730
646731 markdownForType mime_type =
647732 case (M.mainType mime_type, M.subType mime_type) of
648733 ("text", "html") -> "html"
649734 ("application", "xml") -> "xml"
735+ ("text", "xml") -> "xml"
650736 ("application", "json") -> "javascript"
651737 ("application", "javascript") -> "javascript"
652738 ("text", "css") -> "css"
653739 (_, _) -> ""
654740
741+
655742 contentStr mime_type body =
656743 "" :
657− "```" <> markdownForType mime_type :
744+ " ```" <> markdownForType mime_type :
658745 cs body :
659− "```" :
746+ " ```" :
660747 "" :
661748 []
662749
@@ -674,7 +761,7 @@ markdown api = unlines $
674761 [] -> ["- No response body\n"]
675762 [("", t, r)] -> "- Response body as below." : contentStr t r
676763 xs ->
677− concatMap (\(ctx, t, r) -> ("- " <> T.unpack ctx <> " (`" <> cs (show t) <> "`)") : contentStr t r) xs
764+ formatBodies _responseExamples xs
678765
679766-- * Instances
680767
stack-ghc-7.8.4.yaml+2
@@ -23,9 +23,11 @@ extra-deps:
2323- http-client-0.4.30
2424- natural-transformation-0.4
2525- primitive-0.6.1.0
26+- semigroups-0.17
2627- servant-js-0.9.3
2728- should-not-typecheck-2.1.0
2829- time-locale-compat-0.1.1.1
2930- uri-bytestring-0.2.2.0
31+- void-0.7.1
3032- wai-app-static-3.1.5
3133resolver: lts-2.22