servant-docs (empty) → 0.2
raw patch · 5 files changed
+802/−0 lines, 5 filesdep +aesondep +basedep +bytestringsetup-changed
Dependencies added: aeson, base, bytestring, hashable, lens, servant, servant-docs, string-conversions, system-filepath, text, unordered-containers
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- example/greet.hs +62/−0
- servant-docs.cabal +100/−0
- src/Servant/Docs.hs +608/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2014, Zalora South East Asia Pte Ltd++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Zalora South East Asia Pte Ltd nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ example/greet.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+import Data.Aeson+import Data.Text+import GHC.Generics+import Servant+import Servant.Docs++-- * Example++-- | A greet message data type+newtype Greet = Greet { msg :: Text }+ deriving (Generic, Show)++instance FromJSON Greet+instance ToJSON Greet++-- We add some useful annotations to our captures,+-- query parameters and request body to make the docs+-- really helpful.+instance ToCapture (Capture "name" Text) where+ toCapture _ = DocCapture "name" "name of the person to greet"++instance ToCapture (Capture "greetid" Text) where+ 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 ToSample Greet where+ toSample = Just $ Greet "Hello, haskeller!"++-- API specification+type TestApi =+ -- GET /hello/:name?capital={true, false} returns a Greet as JSON+ "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get Greet++ -- 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++-- Generate the data that lets us have API docs. This+-- is derived from the type as well as from+-- the 'ToCapture', 'ToParam' and 'ToSample' instances from above.+docsGreet :: API+docsGreet = docs testApi++main :: IO ()+main = putStrLn $ markdown docsGreet
+ servant-docs.cabal view
@@ -0,0 +1,100 @@+name: servant-docs+version: 0.2+synopsis: generate API docs for your servant webservice+description:+ Library for generating API docs from a servant API definition.+ .+ Runnable example below that prints API docs in markdown.+ .+ > {-# LANGUAGE DataKinds #-}+ > {-# LANGUAGE PolyKinds #-}+ > {-# LANGUAGE TypeFamilies #-}+ > {-# LANGUAGE DeriveGeneric #-}+ > {-# LANGUAGE TypeOperators #-}+ > {-# LANGUAGE FlexibleInstances #-}+ > {-# LANGUAGE OverloadedStrings #-}+ >+ > import Data.Proxy+ > import Data.Text+ > import Servant+ >+ > -- our type for a Greeting message+ > data Greet = Greet { _msg :: Text }+ > deriving (Generic, Show)+ >+ > -- we get our JSON serialization for free+ > instance FromJSON Greet+ > instance ToJSON Greet+ >+ > -- we provide a sample value for the 'Greet' type+ > instance ToSample Greet where+ > toSample = Just g+ >+ > 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+ > toCapture _ = DocCapture "name" "name of the person to greet"+ >+ > instance ToCapture (Capture "greetid" Text) where+ > toCapture _ = DocCapture "greetid" "identifier of the greet msg to remove"+ >+ > -- API specification+ > type TestApi =+ > "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get Greet+ > :<|> "greet" :> RQBody Greet :> Post Greet+ > :<|> "delete" :> Capture "greetid" Text :> Delete+ >+ > testApi :: Proxy TestApi+ > testApi = Proxy+ >+ > -- Generate the Documentation's ADT+ > greetDocs :: API+ > greetDocs = docs testApi+ >+ > main :: IO ()+ > main = putStrLn $ markdown greetDocs+license: BSD3+license-file: LICENSE+author: Alp Mestanogullari, Sönke Hahn, Julian K. Arni+maintainer: alpmestan@gmail.com+copyright: 2014 Zalora South East Asia Pte Ltd+category: Web+build-type: Simple+cabal-version: >=1.10+tested-with: GHC >= 7.8+homepage: http://haskell-servant.github.io/+Bug-reports: http://github.com/haskell-servant/servant-docs/issues+source-repository head+ type: git+ location: http://github.com/haskell-servant/servant-docs.git++library+ exposed-modules:+ Servant.Docs+ build-depends:+ base >=4.7 && <5+ , aeson+ , bytestring+ , hashable+ , lens+ , servant >= 0.2+ , string-conversions+ , system-filepath+ , text+ , unordered-containers+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -Wall++executable greet-docs+ main-is: greet.hs+ hs-source-dirs: example+ ghc-options: -Wall+ build-depends: base, aeson, servant, servant-docs, text+ default-language: Haskell2010
+ src/Servant/Docs.hs view
@@ -0,0 +1,608 @@+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}++-------------------------------------------------------------------------------+-- | This module lets you get API docs for free. It lets generate+-- an 'API' from the type that represents your API using 'docs':+--+-- @docs :: 'HasDocs' api => 'Proxy' api -> 'API'@+--+-- You can then call 'markdown' on it:+--+-- @markdown :: 'API' -> String@+--+-- or define a custom pretty printer:+--+-- @yourPrettyDocs :: 'API' -> String -- or blaze-html's HTML, or ...@+--+-- The only thing you'll need to do will be to implement some classes+-- for your captures, get parameters and request or response bodies.+--+-- Here's a little (but complete) example that you can run to see the+-- markdown pretty printer in action:+--+-- > {-# LANGUAGE DataKinds #-}+-- > {-# LANGUAGE PolyKinds #-}+-- > {-# LANGUAGE TypeFamilies #-}+-- > {-# LANGUAGE DeriveGeneric #-}+-- > {-# LANGUAGE TypeOperators #-}+-- > {-# LANGUAGE FlexibleInstances #-}+-- > {-# LANGUAGE OverloadedStrings #-}+-- >+-- > import Data.Proxy+-- > import Data.Text+-- > import Servant+-- >+-- > -- our type for a Greeting message+-- > data Greet = Greet { _msg :: Text }+-- > deriving (Generic, Show)+-- >+-- > -- we get our JSON serialization for free+-- > instance FromJSON Greet+-- > instance ToJSON Greet+-- >+-- > -- we provide a sample value for the 'Greet' type+-- > instance ToSample Greet where+-- > toSample = Just g+-- >+-- > 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+-- > toCapture _ = DocCapture "name" "name of the person to greet"+-- >+-- > instance ToCapture (Capture "greetid" Text) where+-- > toCapture _ = DocCapture "greetid" "identifier of the greet msg to remove"+-- >+-- > -- API specification+-- > type TestApi =+-- > "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get Greet+-- > :<|> "greet" :> RQBody Greet :> Post Greet+-- > :<|> "delete" :> Capture "greetid" Text :> Delete+-- >+-- > testApi :: Proxy TestApi+-- > testApi = Proxy+-- >+-- > -- Generate the Documentation's ADT+-- > greetDocs :: API+-- > greetDocs = docs testApi+-- >+-- > main :: IO ()+-- > main = putStrLn $ markdown greetDocs+module Servant.Docs+ ( -- * 'HasDocs' class and key functions+ HasDocs(..), docs, markdown++ {- , -- * Serving the documentation+ serveDocumentation -}++ , -- * Classes you need to implement for your types+ ToSample(..)+ , sampleByteString+ , ToParam(..)+ , ToCapture(..)++ , -- * ADTs to represent an 'API'+ Method(..)+ , Endpoint, path, method, defEndpoint+ , API, emptyAPI+ , DocCapture(..), capSymbol, capDesc+ , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind+ , Response, respStatus, respBody, defResponse+ , Action, captures, params, rqbody, response, defAction+ , single++ , -- * Useful modules when defining your doc printers+ module Control.Lens+ , module Data.Monoid+ ) where++import Control.Lens hiding (Action)+import Data.Aeson+import Data.ByteString.Lazy.Char8 (ByteString)+import Data.Hashable+import Data.HashMap.Strict (HashMap)+import Data.List+import Data.Monoid+import Data.Proxy+import Data.Text (Text, pack, unpack)+import Data.String.Conversions+import GHC.Generics+import GHC.TypeLits+import Servant++import qualified Data.HashMap.Strict as HM++-- | Supported HTTP request methods+data Method = DocDELETE -- ^ the DELETE method+ | DocGET -- ^ the GET method+ | DocPOST -- ^ the POST method+ | DocPUT -- ^ the PUT method+ deriving (Eq, Generic)++instance Show Method where+ show DocGET = "GET"+ show DocPOST = "POST"+ show DocDELETE = "DELETE"+ show DocPUT = "PUT"++instance Hashable Method++-- | An 'Endpoint' type that holds the 'path' and the 'method'.+--+-- Gets used as the key in the 'API' hashmap. Modify 'defEndpoint'+-- or any 'Endpoint' value you want using the 'path' and 'method'+-- lenses to tweak.+--+-- @+-- λ> 'defEndpoint'+-- GET /+-- λ> 'defEndpoint' & 'path' '<>~' "foo"+-- GET /foo+-- λ> 'defEndpoint' & 'path' '<>~' "foo" & 'method' '.~' 'DocPOST'+-- POST /foo+-- @+data Endpoint = Endpoint+ { _path :: String -- type collected+ , _method :: Method -- type collected+ } deriving (Eq, Generic)++instance Show Endpoint where+ show (Endpoint p m) =+ show m ++ " " ++ p++-- | An 'Endpoint' whose path is `"/"` and whose method is 'DocGET'+--+-- Here's how you can modify it:+--+-- @+-- λ> 'defEndpoint'+-- GET /+-- λ> 'defEndpoint' & 'path' '<>~' "foo"+-- GET /foo+-- λ> 'defEndpoint' & 'path' '<>~' "foo" & 'method' '.~' 'DocPOST'+-- POST /foo+-- @+defEndpoint :: Endpoint+defEndpoint = Endpoint "/" DocGET++instance Hashable Endpoint++-- | Our API type, a good old hashmap from 'Endpoint' to 'Action'+type API = HashMap Endpoint Action++-- | An empty 'API'+emptyAPI :: API+emptyAPI = HM.empty++-- | A type to represent captures. Holds the name of the capture+-- and a description.+--+-- Write a 'ToCapture' instance for your captured types.+data DocCapture = DocCapture+ { _capSymbol :: String -- type supplied+ , _capDesc :: String -- user supplied+ } deriving (Eq, Show)++-- | A type to represent a /GET/ parameter from the Query String. Holds its name,+-- the possible values (leave empty if there isn't a finite number of them),+-- and a description of how it influences the output or behavior.+--+-- Write a 'ToParam' instance for your GET parameter types+data DocQueryParam = DocQueryParam+ { _paramName :: String -- type supplied+ , _paramValues :: [String] -- user supplied+ , _paramDesc :: String -- user supplied+ , _paramKind :: ParamKind+ } deriving (Eq, Show)++-- | Type of GET parameter:+--+-- - Normal corresponds to @QueryParam@, i.e your usual GET parameter+-- - List corresponds to @QueryParams@, i.e GET parameters with multiple values+-- - Flag corresponds to @QueryFlag@, i.e a value-less GET parameter+data ParamKind = Normal | List | Flag+ deriving (Eq, Show)++-- | A type to represent an HTTP response. Has an 'Int' status and+-- a 'Maybe ByteString' response body. Tweak 'defResponse' using+-- the 'respStatus' and 'respBody' lenses if you want.+--+-- If you want to respond with a non-empty response body, you'll most likely+-- want to write a 'ToSample' instance for the type that'll be represented+-- as some JSON in the response.+--+-- Can be tweaked with two lenses.+--+-- > λ> defResponse+-- > Response {_respStatus = 200, _respBody = Nothing}+-- > λ> defResponse & respStatus .~ 204 & respBody .~ Just "[]"+-- > Response {_respStatus = 204, _respBody = Just "[]"}+data Response = Response+ { _respStatus :: Int+ , _respBody :: Maybe ByteString+ } deriving (Eq, Show)++-- | Default response: status code 200, no response body.+--+-- Can be tweaked with two lenses.+--+-- > λ> defResponse+-- > Response {_respStatus = 200, _respBody = Nothing}+-- > λ> defResponse & respStatus .~ 204 & respBody .~ Just "[]"+-- > Response {_respStatus = 204, _respBody = Just "[]"}+defResponse :: Response+defResponse = Response 200 Nothing++-- | A datatype that represents everything that can happen+-- at an endpoint, with its lenses:+--+-- - List of captures ('captures')+-- - List of GET parameters ('params')+-- - What the request body should look like, if any is requested ('rqbody')+-- - What the response should be if everything goes well ('response')+--+-- You can tweak an 'Action' (like the default 'defAction') with these lenses+-- to transform an action and add some information to it.+data Action = Action+ { _captures :: [DocCapture] -- type collected + user supplied info+ , _headers :: [Text] -- type collected+ , _params :: [DocQueryParam] -- type collected + user supplied info+ , _rqbody :: Maybe ByteString -- user supplied+ , _response :: Response -- user supplied+ } deriving (Eq, Show)++-- Default 'Action'. Has no 'captures', no GET 'params', expects+-- no request body ('rqbody') and the typical response is 'defResponse'.+--+-- Tweakable with lenses.+--+-- > λ> defAction+-- > Action {_captures = [], _params = [], _rqbody = Nothing, _response = Response {_respStatus = 200, _respBody = Nothing}}+-- > λ> defAction & response.respStatus .~ 201+-- > Action {_captures = [], _params = [], _rqbody = Nothing, _response = Response {_respStatus = 201, _respBody = Nothing}}+defAction :: Action+defAction =+ Action []+ []+ []+ Nothing+ defResponse++-- | Create an API that's comprised of a single endpoint.+-- 'API' is a 'Monoid', so combine multiple endpoints with+-- 'mappend' or '<>'.+single :: Endpoint -> Action -> API+single = HM.singleton++-- gimme some lenses+makeLenses ''Endpoint+makeLenses ''DocCapture+makeLenses ''DocQueryParam+makeLenses ''Response+makeLenses ''Action++-- | Generate the docs for a given API that implements 'HasDocs'.+docs :: HasDocs layout => Proxy layout -> API+docs p = docsFor p (defEndpoint, defAction)++-- | The class that abstracts away the impact of API combinators+-- on documentation generation.+class HasDocs layout where+ docsFor :: Proxy layout -> (Endpoint, Action) -> API++-- | The class that lets us display a sample JSON input or output+-- when generating documentation for endpoints that either:+--+-- - expect a request body, or+-- - return a non empty response body+--+-- Example of an instance:+--+-- > {-# LANGUAGE DeriveGeneric #-}+-- > {-# LANGUAGE OverloadedStrings #-}+-- >+-- > import Data.Aeson+-- > import Data.Text+-- > import GHC.Generics+-- >+-- > data Greet = Greet { _msg :: Text }+-- > deriving (Generic, Show)+-- >+-- > instance FromJSON Greet+-- > instance ToJSON Greet+-- >+-- > instance ToSample Greet where+-- > toSample = Just g+-- >+-- > where g = Greet "Hello, haskeller!"+class ToJSON a => ToSample a where+ toSample :: Maybe a++instance ToSample () where+ toSample = Just ()++sampleByteString :: forall a . ToSample a => Proxy a -> Maybe ByteString+sampleByteString Proxy = fmap encode (toSample :: Maybe a)++-- | The class that helps us automatically get documentation+-- for GET parameters.+--+-- Example of an instance:+--+-- > instance ToParam (QueryParam "capital" Bool) where+-- > toParam _ =+-- > DocQueryParam "capital"+-- > ["true", "false"]+-- > "Get the greeting message in uppercase (true) or not (false). Default is false."+class ToParam t where+ toParam :: Proxy t -> DocQueryParam++-- | The class that helps us automatically get documentation+-- for URL captures.+--+-- Example of an instance:+--+-- > instance ToCapture (Capture "name" Text) where+-- > toCapture _ = DocCapture "name" "name of the person to greet"+class ToCapture c where+ toCapture :: Proxy c -> DocCapture++-- | Generate documentation in Markdown format for+-- the given 'API'.+markdown :: API -> String+markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList++ where printEndpoint :: Endpoint -> Action -> [String]+ printEndpoint endpoint action =+ str :+ replicate len '-' :+ "" :+ capturesStr (action ^. captures) +++ headersStr (action ^. headers) +++ paramsStr (action ^. params) +++ rqbodyStr (action ^. rqbody) +++ responseStr (action ^. response) +++ []++ where str = show (endpoint^.method) ++ " " ++ endpoint^.path+ len = length str++ capturesStr :: [DocCapture] -> [String]+ capturesStr [] = []+ capturesStr l =+ "**Captures**: " :+ "" :+ map captureStr l +++ "" :+ []+ captureStr cap =+ "- *" ++ (cap ^. capSymbol) ++ "*: " ++ (cap ^. capDesc)++ headersStr :: [Text] -> [String]+ headersStr [] = []+ headersStr l = [""] ++ map headerStr l ++ [""]++ where headerStr hname = "- This endpoint is sensitive to the value of the **"+ ++ unpack hname ++ "** HTTP header."++ paramsStr :: [DocQueryParam] -> [String]+ paramsStr [] = []+ paramsStr l =+ "**GET Parameters**: " :+ "" :+ map paramStr l +++ "" :+ []+ paramStr param = unlines $+ (" - " ++ param ^. paramName) :+ (if (not (null values) || param ^. paramKind /= Flag)+ then [" - **Values**: *" ++ intercalate ", " values ++ "*"]+ else []) +++ (" - **Description**: " ++ param ^. paramDesc) :+ (if (param ^. paramKind == List)+ then [" - This parameter is a **list**. All GET parameters with the name "+ ++ param ^. paramName ++ "[] will forward their values in a list to the handler."]+ else []) +++ (if (param ^. paramKind == Flag)+ then [" - This parameter is a **flag**. This means no value is expected to be associated to this parameter."]+ else []) +++ []++ where values = param ^. paramValues++ rqbodyStr :: Maybe ByteString -> [String]+ rqbodyStr Nothing = []+ rqbodyStr (Just b) =+ "**Request Body**: " :+ jsonStr b++ jsonStr b =+ "" :+ "``` javascript" :+ cs b :+ "```" :+ "" :+ []++ responseStr :: Response -> [String]+ responseStr resp =+ "**Response**: " :+ "" :+ (" - Status code " ++ show (resp ^. respStatus)) :+ (resp ^. respBody &+ maybe [" - No response body\n"]+ (\b -> " - Response body as below." : jsonStr b))++-- * Instances++-- | The generated docs for @a ':<|>' b@ just appends the docs+-- for @a@ with the docs for @b@.+instance (HasDocs layout1, HasDocs layout2)+ => HasDocs (layout1 :<|> layout2) where++ docsFor Proxy (ep, action) = docsFor p1 (ep, action) <> docsFor p2 (ep, action)++ where p1 :: Proxy layout1+ p1 = Proxy++ p2 :: Proxy layout2+ p2 = Proxy++-- | @"books" :> 'Capture' "isbn" Text@ will appear as+-- @/books/:isbn@ in the docs.+instance (KnownSymbol sym, ToCapture (Capture sym a), HasDocs sublayout)+ => HasDocs (Capture sym a :> sublayout) where++ docsFor Proxy (endpoint, action) =+ docsFor sublayoutP (endpoint', action')++ where sublayoutP = Proxy :: Proxy sublayout+ captureP = Proxy :: Proxy (Capture sym a)++ action' = over captures (|> toCapture captureP) action+ endpoint' = over path (\p -> p++"/:"++symbolVal symP) endpoint+ symP = Proxy :: Proxy sym+++instance HasDocs Delete where+ docsFor Proxy (endpoint, action) =+ single endpoint' action'++ where endpoint' = endpoint & method .~ DocDELETE++ action' = action & response.respBody .~ Nothing+ & response.respStatus .~ 204++instance ToSample a => HasDocs (Get a) where+ docsFor Proxy (endpoint, action) =+ single endpoint' action'++ where endpoint' = endpoint & method .~ DocGET+ action' = action & response.respBody .~ sampleByteString p+ p = Proxy :: Proxy a++instance (KnownSymbol sym, HasDocs sublayout)+ => HasDocs (Header sym a :> sublayout) where+ docsFor Proxy (endpoint, action) =+ docsFor sublayoutP (endpoint, action')++ where sublayoutP = Proxy :: Proxy sublayout+ action' = over headers (|> headername) action+ headername = pack $ symbolVal (Proxy :: Proxy sym)++instance ToSample a => HasDocs (Post a) where+ docsFor Proxy (endpoint, action) =+ single endpoint' action'++ where endpoint' = endpoint & method .~ DocPOST++ action' = action & response.respBody .~ sampleByteString p+ & response.respStatus .~ 201++ p = Proxy :: Proxy a++instance ToSample a => HasDocs (Put a) where+ docsFor Proxy (endpoint, action) =+ single endpoint' action'++ where endpoint' = endpoint & method .~ DocPUT++ action' = action & response.respBody .~ sampleByteString p+ & response.respStatus .~ 200++ p = Proxy :: Proxy a+++instance (KnownSymbol sym, ToParam (QueryParam sym a), HasDocs sublayout)+ => HasDocs (QueryParam sym a :> sublayout) where++ docsFor Proxy (endpoint, action) =+ docsFor sublayoutP (endpoint, action')++ where sublayoutP = Proxy :: Proxy sublayout+ paramP = Proxy :: Proxy (QueryParam sym a)+ action' = over params (|> toParam paramP) action++instance (KnownSymbol sym, ToParam (QueryParams sym a), HasDocs sublayout)+ => HasDocs (QueryParams sym a :> sublayout) where++ docsFor Proxy (endpoint, action) =+ docsFor sublayoutP (endpoint, action')++ where sublayoutP = Proxy :: Proxy sublayout+ paramP = Proxy :: Proxy (QueryParams sym a)+ action' = over params (|> toParam paramP) action+++instance (KnownSymbol sym, ToParam (QueryFlag sym), HasDocs sublayout)+ => HasDocs (QueryFlag sym :> sublayout) where++ docsFor Proxy (endpoint, action) =+ docsFor sublayoutP (endpoint, action')++ where sublayoutP = Proxy :: Proxy sublayout+ paramP = Proxy :: Proxy (QueryFlag sym)+ action' = over params (|> toParam paramP) action++instance HasDocs Raw where+ docsFor _proxy (endpoint, action) =+ single endpoint action++instance (ToSample a, HasDocs sublayout)+ => HasDocs (ReqBody a :> sublayout) where++ docsFor Proxy (endpoint, action) =+ docsFor sublayoutP (endpoint, action')++ where sublayoutP = Proxy :: Proxy sublayout++ action' = action & rqbody .~ sampleByteString p+ p = Proxy :: Proxy a++instance (KnownSymbol path, HasDocs sublayout) => HasDocs (path :> sublayout) where++ docsFor Proxy (endpoint, action) =+ docsFor sublayoutP (endpoint', action)++ where sublayoutP = Proxy :: Proxy sublayout+ endpoint' = endpoint & path <>~ symbolVal pa+ pa = Proxy :: Proxy path++{-++-- | Serve your API's docs as markdown embedded in an html \<pre> tag.+--+-- > type MyApi = "users" :> Get [User]+-- > :<|> "docs :> Raw+-- >+-- > apiProxy :: Proxy MyApi+-- > apiProxy = Proxy+-- >+-- > server :: Server MyApi+-- > server = listUsers+-- > :<|> serveDocumentation apiProxy+serveDocumentation :: HasDocs api => Proxy api -> Server Raw+serveDocumentation proxy _request respond =+ respond $ responseLBS ok200 [] $ cs $ toHtml $ markdown $ docs proxy++toHtml :: String -> String+toHtml md =+ "<html>" +++ "<body>" +++ "<pre>" +++ md +++ "</pre>" +++ "</body>" +++ "</html>"+-}