@@ -20,24 +20,31 @@
20#include "overlapping-compat.h"
20#include "overlapping-compat.h"
21module Servant.Docs.Internal where
21module Servant.Docs.Internal where
23−import Prelude ()
23+import Prelude ()
24import Prelude.Compat
24import Prelude.Compat
25import Control.Applicative
26import Control.Applicative
26import Control.Arrow (second)
27import Control.Arrow (second)
27−import Control.Lens (makeLenses, mapped, over, traversed, view, (%~),
28+import Control.Lens (makeLenses, mapped, over,
29+ traversed, view, (%~), (&), (.~),
28− (&), (.~), (<>~), (^.), (|>))
30+ (<>~), (^.), (|>))
29import qualified Control.Monad.Omega as Omega
31import qualified Control.Monad.Omega as Omega
30−import Data.ByteString.Lazy.Char8 (ByteString)
31import qualified Data.ByteString.Char8 as BSC
32import qualified Data.ByteString.Char8 as BSC
33+import Data.ByteString.Lazy.Char8 (ByteString)
32import qualified Data.CaseInsensitive as CI
34import qualified Data.CaseInsensitive as CI
35+import Data.Foldable (fold)
33import Data.Hashable (Hashable)
36import Data.Hashable (Hashable)
34import Data.HashMap.Strict (HashMap)
37import Data.HashMap.Strict (HashMap)
35import Data.List.Compat (intercalate, intersperse, sort)
38import Data.List.Compat (intercalate, intersperse, sort)
39+import Data.List.NonEmpty (NonEmpty ((:|)), groupWith)
40+import qualified Data.List.NonEmpty as NE
36import Data.Maybe
41import Data.Maybe
37−import Data.Monoid (All (..), Any (..), Sum (..), Product (..), First (..), Last (..), Dual (..))
42+import Data.Monoid (All (..), Any (..), Dual (..),
38−import Data.Semigroup (Semigroup (..))
43+ First (..), Last (..),
44+ Product (..), Sum (..))
39import Data.Ord (comparing)
45import Data.Ord (comparing)
40−import Data.Proxy (Proxy(Proxy))
46+import Data.Proxy (Proxy (Proxy))
47+import Data.Semigroup (Semigroup (..))
41import Data.String.Conversions (cs)
48import Data.String.Conversions (cs)
42import Data.Text (Text, unpack)
49import Data.Text (Text, unpack)
43import GHC.Generics
50import GHC.Generics
@@ -291,6 +298,35 @@ defAction =
291single :: Endpoint -> Action -> API
298single :: Endpoint -> Action -> API
292single e a = API mempty (HM.singleton e a)
299single e a = API mempty (HM.singleton e a)
301+-- | How many content-types for each example should be shown?
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)
308+-- | Customise how an 'API' is converted into documentation.
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?
318+-- | Default API generation options.
320+-- All content types are shown for both 'requestExamples' and
321+-- 'responseExamples'.
324+defRenderingOptions :: RenderingOptions
325+defRenderingOptions = RenderingOptions
326+ { _requestExamples = AllContentTypes
327+ , _responseExamples = AllContentTypes
294-- gimme some lenses
330-- gimme some lenses
295makeLenses ''DocAuthentication
331makeLenses ''DocAuthentication
296makeLenses ''DocOptions
332makeLenses ''DocOptions
@@ -302,6 +338,7 @@ makeLenses ''DocIntro
302makeLenses ''DocNote
338makeLenses ''DocNote
303makeLenses ''Response
339makeLenses ''Response
304makeLenses ''Action
340makeLenses ''Action
341+makeLenses ''RenderingOptions
306-- | Generate the docs for a given API that implements 'HasDocs'. This is the
343-- | Generate the docs for a given API that implements 'HasDocs'. This is the
307-- default way to create documentation.
344-- default way to create documentation.
@@ -517,8 +554,34 @@ class ToAuthInfo a where
518-- | Generate documentation in Markdown format for
555-- | Generate documentation in Markdown format for
519-- the given 'API'.
556-- the given 'API'.
558+-- This is equivalent to @'markdownWith' 'defRenderingOptions'@.
520markdown :: API -> String
559markdown :: API -> String
521−markdown api = unlines $
560+markdown = markdownWith defRenderingOptions
562+-- | Generate documentation in Markdown format for
563+-- the given 'API' using the specified options.
565+-- These options allow you to customise aspects such as:
567+-- * Choose how many content-types for each request body example are
568+-- shown with 'requestExamples'.
570+-- * Choose how many content-types for each response body example
571+-- are shown with 'responseExamples'.
573+-- For example, to only show the first content-type of each example:
576+-- markdownWith ('defRenderingOptions'
577+-- & 'requestExamples' '.~' 'FirstContentType'
578+-- & 'responseExamples' '.~' 'FirstContentType' )
583+markdownWith :: RenderingOptions -> API -> String
584+markdownWith RenderingOptions{..} api = unlines $
522 introsStr (api ^. apiIntros)
585 introsStr (api ^. apiIntros)
523 ++ (concatMap (uncurry printEndpoint) . sort . HM.toList $ api ^. apiEndpoints)
586 ++ (concatMap (uncurry printEndpoint) . sort . HM.toList $ api ^. apiEndpoints)
@@ -632,31 +695,55 @@ markdown api = unlines $
632 rqbodyStr types s =
695 rqbodyStr types s =
633 ["#### Request:", ""]
696 ["#### Request:", ""]
634 <> formatTypes types
697 <> formatTypes types
635− <> concatMap formatBody s
698+ <> formatBodies _requestExamples s
637 formatTypes [] = []
700 formatTypes [] = []
638 formatTypes ts = ["- Supported content types are:", ""]
701 formatTypes ts = ["- Supported content types are:", ""]
639 <> map (\t -> " - `" <> show t <> "`") ts
702 <> map (\t -> " - `" <> show t <> "`") ts
642− formatBody (t, 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)
710+ bodyGroups :: [(Text, NonEmpty M.MediaType, ByteString)]
712+ map (\grps -> let (t,_,b) = NE.head grps in (t, fmap (\(_,m,_) -> m) grps, b))
713+ . groupWith (\(t,_,b) -> (t,b))
717+ AllContentTypes -> id
718+ FirstContentType -> map (\(t,ms,b) -> (t, NE.head ms :| [], b))
720+ formatBody :: (Text, NonEmpty M.MediaType, ByteString) -> [String]
721+ formatBody (t, ms, b) =
722+ "- " <> title <> " (" <> mediaList ms <> "):" :
723+ contentStr (NE.head ms) b
725+ mediaList = fold . NE.intersperse ", " . fmap (\m -> "`" ++ show m ++ "`")
643− "- Example (" <> cs t <> "): `" <> cs (show m) <> "`" :
728+ | T.null t = "Example"
644− contentStr m b
729+ | otherwise = cs t
646 markdownForType mime_type =
731 markdownForType mime_type =
647 case (M.mainType mime_type, M.subType mime_type) of
732 case (M.mainType mime_type, M.subType mime_type) of
648 ("text", "html") -> "html"
733 ("text", "html") -> "html"
649 ("application", "xml") -> "xml"
734 ("application", "xml") -> "xml"
735+ ("text", "xml") -> "xml"
650 ("application", "json") -> "javascript"
736 ("application", "json") -> "javascript"
651 ("application", "javascript") -> "javascript"
737 ("application", "javascript") -> "javascript"
652 ("text", "css") -> "css"
738 ("text", "css") -> "css"
653 (_, _) -> ""
739 (_, _) -> ""
655 contentStr mime_type body =
742 contentStr mime_type body =
657− "```" <> markdownForType mime_type :
744+ " ```" <> markdownForType mime_type :
658 cs body :
745 cs body :
659− "```" :
746+ " ```" :
@@ -674,7 +761,7 @@ markdown api = unlines $
674 [] -> ["- No response body\n"]
761 [] -> ["- No response body\n"]
675 [("", t, r)] -> "- Response body as below." : contentStr t r
762 [("", t, r)] -> "- Response body as below." : contentStr t r
677− concatMap (\(ctx, t, r) -> ("- " <> T.unpack ctx <> " (`" <> cs (show t) <> "`)") : contentStr t r) xs
764+ formatBodies _responseExamples xs
679-- * Instances
766-- * Instances