@@ -1,16 +1,16 @@
1−{-# LANGUAGE ConstraintKinds #-}
1+{-# LANGUAGE TypeFamilies #-}
2−{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE DeriveGeneric #-}
4+{-# LANGUAGE TupleSections #-}
5+{-# LANGUAGE TypeOperators #-}
6+{-# LANGUAGE TemplateHaskell #-}
4−{-# LANGUAGE FlexibleContexts #-}
7+{-# LANGUAGE FlexibleContexts #-}
5−{-# LANGUAGE FlexibleInstances #-}
8+{-# LANGUAGE FlexibleInstances #-}
6−{-# LANGUAGE MultiParamTypeClasses #-}
7−{-# LANGUAGE OverloadedStrings #-}
9+{-# LANGUAGE OverloadedStrings #-}
8−{-# LANGUAGE ScopedTypeVariables #-}
10+{-# LANGUAGE ScopedTypeVariables #-}
9−{-# LANGUAGE TemplateHaskell #-}
11+{-# LANGUAGE ConstraintKinds #-}
10−{-# LANGUAGE TupleSections #-}
11−{-# LANGUAGE TypeFamilies #-}
12−{-# LANGUAGE TypeOperators #-}
13−{-# LANGUAGE UndecidableInstances #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+{-# LANGUAGE PolyKinds #-}
15-------------------------------------------------------------------------------
15-------------------------------------------------------------------------------
16-- | This module lets you get API docs for free. It lets generate
16-- | This module lets you get API docs for free. It lets generate
@@ -37,15 +37,17 @@
37-- Here's a little (but complete) example that you can run to see the
37-- Here's a little (but complete) example that you can run to see the
38-- markdown pretty printer in action:
38-- markdown pretty printer in action:
40−-- > {-# LANGUAGE DataKinds #-}
40+-- > {-# LANGUAGE DataKinds #-}
41−-- > {-# LANGUAGE DeriveGeneric #-}
41+-- > {-# LANGUAGE DeriveGeneric #-}
42−-- > {-# LANGUAGE TypeOperators #-}
43−-- > {-# LANGUAGE FlexibleInstances #-}
42+-- > {-# LANGUAGE FlexibleInstances #-}
43+-- > {-# LANGUAGE MultiParamTypeClasses #-}
44−-- > {-# LANGUAGE OverloadedStrings #-}
44+-- > {-# LANGUAGE OverloadedStrings #-}
45+-- > {-# LANGUAGE TypeOperators #-}
45-- > {-# OPTIONS_GHC -fno-warn-orphans #-}
46-- > {-# OPTIONS_GHC -fno-warn-orphans #-}
46-- > import Data.Aeson
47-- > import Data.Aeson
47-- > import Data.Proxy
48-- > import Data.Proxy
48−-- > import Data.Text(Text)
49+-- > import Data.String.Conversions
50+-- > import Data.Text (Text)
49-- > import GHC.Generics
51-- > import GHC.Generics
50-- > import Servant.API
52-- > import Servant.API
51-- > import Servant.Docs
53-- > import Servant.Docs
@@ -56,9 +58,15 @@
56-- > newtype Greet = Greet Text
58-- > newtype Greet = Greet Text
57-- > deriving (Generic, Show)
59-- > deriving (Generic, Show)
61+-- > -- | We can get JSON support automatically. This will be used to parse
62+-- > -- and encode a Greeting as 'JSON'.
59-- > instance FromJSON Greet
63-- > instance FromJSON Greet
60-- > instance ToJSON Greet
64-- > instance ToJSON Greet
66+-- > -- | We can also implement 'MimeRender' for additional formats like 'PlainText'.
67+-- > instance MimeRender PlainText Greet where
68+-- > toByteString Proxy (Greet s) = "\"" <> cs s <> "\""
62-- > -- We add some useful annotations to our captures,
70-- > -- We add some useful annotations to our captures,
63-- > -- query parameters and request body to make the docs
71-- > -- query parameters and request body to make the docs
64-- > -- really helpful.
72-- > -- really helpful.
@@ -91,6 +99,11 @@
91-- > , ("If you use ?capital=false", Greet "Hello, haskeller")
99-- > , ("If you use ?capital=false", Greet "Hello, haskeller")
102+-- > -- We define some introductory sections, these will appear at the top of the
103+-- > -- documentation.
105+-- > -- We pass them in with 'docsWith', below. If you only want to add
106+-- > -- introductions, you may use 'docsWithIntros'
94-- > intro1 :: DocIntro
107-- > intro1 :: DocIntro
95-- > intro1 = DocIntro "On proper introductions." -- The title
108-- > intro1 = DocIntro "On proper introductions." -- The title
96-- > [ "Hello there."
109-- > [ "Hello there."
@@ -104,8 +117,8 @@
105-- > -- API specification
118-- > -- API specification
106-- > type TestApi =
119-- > type TestApi =
107−-- > -- GET /hello/:name?capital={true, false} returns a Greet as JSON
120+-- > -- GET /hello/:name?capital={true, false} returns a Greet as JSON or PlainText
108−-- > "hello" :> MatrixParam "lang" String :> Capture "name" Text :> QueryParam "capital" Bool :> Get '[JSON] Greet
121+-- > "hello" :> MatrixParam "lang" String :> Capture "name" Text :> QueryParam "capital" Bool :> Get '[JSON, PlainText] Greet
110-- > -- POST /greet with a Greet as JSON in the request body,
123-- > -- POST /greet with a Greet as JSON in the request body,
111-- > -- returns a Greet as JSON
124-- > -- returns a Greet as JSON
@@ -117,21 +130,34 @@
117-- > testApi :: Proxy TestApi
130-- > testApi :: Proxy TestApi
118-- > testApi = Proxy
131-- > testApi = Proxy
133+-- > -- Build some extra information for the DELETE /greet/:greetid endpoint. We
134+-- > -- want to add documentation about a secret unicorn header and some extra
136+-- > extra :: ExtraInfo TestApi
138+-- > extraInfo (Proxy :: Proxy ("greet" :> Capture "greetid" Text :> Delete)) $
139+-- > defAction & headers <>~ ["unicorns"]
140+-- > & notes <>~ [ DocNote "Title" ["This is some text"]
141+-- > , DocNote "Second secton" ["And some more"]
120-- > -- Generate the data that lets us have API docs. This
144-- > -- Generate the data that lets us have API docs. This
121-- > -- is derived from the type as well as from
145-- > -- is derived from the type as well as from
122-- > -- the 'ToCapture', 'ToParam' and 'ToSample' instances from above.
146-- > -- the 'ToCapture', 'ToParam' and 'ToSample' instances from above.
124−-- > -- If you didn't want intros you could just call:
148+-- > -- If you didn't want intros and extra information, you could just call:
126−-- > -- > docs testAPI
150+-- > -- > docs testAPI :: API
127-- > docsGreet :: API
151-- > docsGreet :: API
128−-- > docsGreet = docsWithIntros [intro1, intro2] testApi
152+-- > docsGreet = docsWith [intro1, intro2] extra testApi
130-- > main :: IO ()
154-- > main :: IO ()
131-- > main = putStrLn $ markdown docsGreet
155-- > main = putStrLn $ markdown docsGreet
132module Servant.Docs
156module Servant.Docs
133 ( -- * 'HasDocs' class and key functions
157 ( -- * 'HasDocs' class and key functions
134− HasDocs(..), docs, docsWithIntros, markdown
158+ HasDocs(..), docs, markdown
159+ -- * Generating docs with extra information
160+ , ExtraInfo(..), docsWith, docsWithIntros, extraInfo
136 , -- * Classes you need to implement for your types
162 , -- * Classes you need to implement for your types
137 ToSample(..)
163 ToSample(..)
@@ -158,7 +184,7 @@ module Servant.Docs
160import Control.Applicative
186import Control.Applicative
161−import Control.Lens
187+import Control.Lens hiding (Action)
162import Data.Aeson
188import Data.Aeson
163import Data.ByteString.Lazy.Char8 (ByteString)
189import Data.ByteString.Lazy.Char8 (ByteString)
164import Data.Hashable
190import Data.Hashable
@@ -172,7 +198,9 @@ import Data.String.Conversions
172import Data.Text (Text, pack, unpack)
198import Data.Text (Text, pack, unpack)
173import GHC.Generics
199import GHC.Generics
174import GHC.TypeLits
200import GHC.TypeLits
201+import GHC.Exts(Constraint)
175import Servant.API
202import Servant.API
203+import Servant.Utils.Links
176import Servant.API.ContentTypes
204import Servant.API.ContentTypes
178import qualified Data.HashMap.Strict as HM
206import qualified Data.HashMap.Strict as HM
@@ -296,6 +324,17 @@ data DocNote = DocNote
296 , _noteBody :: [String]
324 , _noteBody :: [String]
297 } deriving (Eq, Show)
325 } deriving (Eq, Show)
327+-- | Type of extra information that a user may wish to "union" with their
330+-- These are intended to be built using extraInfo.
331+-- Multiple ExtraInfo may be combined with the monoid instance.
332+newtype ExtraInfo layout = ExtraInfo (HashMap Endpoint Action)
333+instance Monoid (ExtraInfo a) where
334+ mempty = ExtraInfo mempty
335+ ExtraInfo a `mappend` ExtraInfo b =
336+ ExtraInfo $ HM.unionWith combineAction a b
299-- | Type of GET parameter:
338-- | Type of GET parameter:
301-- - Normal corresponds to @QueryParam@, i.e your usual GET parameter
340-- - Normal corresponds to @QueryParam@, i.e your usual GET parameter
@@ -357,6 +396,16 @@ data Action = Action
357 , _response :: Response -- user supplied
396 , _response :: Response -- user supplied
358 } deriving (Eq, Show)
397 } deriving (Eq, Show)
399+-- | Combine two Actions, we can't make a monoid as merging Response breaks the
402+-- As such, we invent a non-commutative, left associative operation
403+-- 'combineAction' to mush two together taking the response, body and content
404+-- types from the very left.
405+combineAction :: Action -> Action -> Action
406+Action c h p n m ts body resp `combineAction` Action c' h' p' n' m' _ _ _ =
407+ Action (c <> c') (h <> h') (p <> p') (n <> n') (m <> m') ts body resp
360-- Default 'Action'. Has no 'captures', no GET 'params', expects
409-- Default 'Action'. Has no 'captures', no GET 'params', expects
361-- no request body ('rqbody') and the typical response is 'defResponse'.
410-- no request body ('rqbody') and the typical response is 'defResponse'.
@@ -398,10 +447,62 @@ makeLenses ''Action
398docs :: HasDocs layout => Proxy layout -> API
447docs :: HasDocs layout => Proxy layout -> API
399docs p = docsFor p (defEndpoint, defAction)
448docs p = docsFor p (defEndpoint, defAction)
450+-- | Closed type family, check if endpoint is exactly within API.
452+-- We aren't sure what affects how an Endpoint is built up, so we require an
454+type family IsIn (endpoint :: *) (api :: *) :: Constraint where
455+ IsIn e (sa :<|> sb) = Or (IsIn e sa) (IsIn e sb)
456+ IsIn (e :> sa) (e :> sb) = IsIn sa sb
459+-- | Create an 'ExtraInfo' that is garunteed to be within the given API layout.
461+-- The safety here is to ensure that you only add custom documentation to an
462+-- endpoint that actually exists within your API.
464+-- > extra :: ExtraInfo TestApi
466+-- > extraInfo (Proxy :: Proxy ("greet" :> Capture "greetid" Text :> Delete)) $
467+-- > defAction & headers <>~ ["unicorns"]
468+-- > & notes <>~ [ DocNote "Title" ["This is some text"]
469+-- > , DocNote "Second secton" ["And some more"]
472+extraInfo :: (IsIn endpoint layout, HasLink endpoint, HasDocs endpoint)
473+ => Proxy endpoint -> Action -> ExtraInfo layout
475+ let api = docsFor p (defEndpoint, defAction)
476+ -- Assume one endpoint, HasLink constraint means that we should only ever
477+ -- point at one endpoint.
478+ in ExtraInfo $ api ^. apiEndpoints & traversed .~ action
480+-- | Generate documentation given some extra introductions (in the form of
481+-- 'DocInfo') and some extra endpoint documentation (in the form of
484+-- The extra introductions will be prepended to the top of the documentation,
485+-- before the specific endpoint documentation. The extra endpoint documentation
486+-- will be "unioned" with the automatically generated endpoint documentation.
488+-- You are expected to build up the ExtraInfo with the Monoid instance and
491+-- If you only want to add an introduction, use 'docsWithIntros'.
492+docsWith :: HasDocs layout
497+docsWith intros (ExtraInfo endpoints) p =
498+ docs p & apiIntros <>~ intros
499+ & apiEndpoints %~ HM.unionWith combineAction endpoints
401-- | Generate the docs for a given API that implements 'HasDocs' with with any
502-- | Generate the docs for a given API that implements 'HasDocs' with with any
402-- number of introduction(s)
503-- number of introduction(s)
403docsWithIntros :: HasDocs layout => [DocIntro] -> Proxy layout -> API
504docsWithIntros :: HasDocs layout => [DocIntro] -> Proxy layout -> API
404−docsWithIntros intros p = docs p & apiIntros <>~ intros
505+docsWithIntros intros = docsWith intros mempty
406-- | The class that abstracts away the impact of API combinators
507-- | The class that abstracts away the impact of API combinators
407-- on documentation generation.
508-- on documentation generation.
@@ -567,10 +668,7 @@ markdown api = unlines $
567 mxParamsStr l =
668 mxParamsStr l =
568 "#### Matrix Parameters:" :
669 "#### Matrix Parameters:" :
570− map segmentStr l ++
671+ map segmentStr l
574 segmentStr :: (String, [DocQueryParam]) -> String
672 segmentStr :: (String, [DocQueryParam]) -> String
575 segmentStr (segment, l) = unlines $
673 segmentStr (segment, l) = unlines $
576 ("**" ++ segment ++ "**:") :
674 ("**" ++ segment ++ "**:") :
@@ -620,7 +718,7 @@ markdown api = unlines $
620 <> concatMap formatBody samples
718 <> concatMap formatBody samples
622 formatTypes [] = []
720 formatTypes [] = []
623− formatTypes ts = ["- Supported content types are: ", ""]
721+ formatTypes ts = ["- Supported content types are:", ""]
624 <> map (\t -> " - `" <> show t <> "`") ts
722 <> map (\t -> " - `" <> show t <> "`") ts
@@ -639,7 +737,7 @@ markdown api = unlines $
640 contentStr mime_type body =
738 contentStr mime_type body =
642− "``` " <> markdownForType mime_type :
740+ "```" <> markdownForType mime_type :
643 cs body :
741 cs body :