Generate docs with samples encoded in all types
Request and response body documentation now includes sample values encoded in all supported media types.
This commit is contained in:
parent
dba8689acd
commit
508b9f9791
1 changed files with 80 additions and 49 deletions
|
@ -1,13 +1,16 @@
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE ConstraintKinds #-}
|
||||||
{-# LANGUAGE DataKinds #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
{-# LANGUAGE UndecidableInstances #-}
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
-- | This module lets you get API docs for free. It lets generate
|
-- | This module lets you get API docs for free. It lets generate
|
||||||
|
@ -154,25 +157,27 @@ module Servant.Docs
|
||||||
, module Data.Monoid
|
, module Data.Monoid
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import Control.Applicative
|
||||||
import Control.Lens
|
import Control.Lens
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.Aeson.Encode.Pretty (encodePretty)
|
|
||||||
import Data.Ord(comparing)
|
|
||||||
import Data.ByteString.Lazy.Char8 (ByteString)
|
import Data.ByteString.Lazy.Char8 (ByteString)
|
||||||
import Data.Hashable
|
import Data.Hashable
|
||||||
import Data.HashMap.Strict (HashMap)
|
import Data.HashMap.Strict (HashMap)
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe (listToMaybe)
|
import Data.Maybe
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
import Data.Ord (comparing)
|
||||||
import Data.Proxy
|
import Data.Proxy
|
||||||
import Data.Text (Text, pack, unpack)
|
|
||||||
import Data.String.Conversions
|
import Data.String.Conversions
|
||||||
|
import Data.Text (Text, pack, unpack)
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
import GHC.TypeLits
|
import GHC.TypeLits
|
||||||
import Servant.API
|
import Servant.API
|
||||||
|
import Servant.API.ContentTypes
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import qualified Network.HTTP.Media as M
|
||||||
|
|
||||||
-- | Supported HTTP request methods
|
-- | Supported HTTP request methods
|
||||||
data Method = DocDELETE -- ^ the DELETE method
|
data Method = DocDELETE -- ^ the DELETE method
|
||||||
|
@ -315,7 +320,7 @@ data ParamKind = Normal | List | Flag
|
||||||
-- > Response {_respStatus = 204, _respBody = [("If everything goes well", "{ \"status\": \"ok\" }")]}
|
-- > Response {_respStatus = 204, _respBody = [("If everything goes well", "{ \"status\": \"ok\" }")]}
|
||||||
data Response = Response
|
data Response = Response
|
||||||
{ _respStatus :: Int
|
{ _respStatus :: Int
|
||||||
, _respBody :: [(Text, ByteString)]
|
, _respBody :: [(Text, M.MediaType, ByteString)]
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
-- | Default response: status code 200, no response body.
|
-- | Default response: status code 200, no response body.
|
||||||
|
@ -345,7 +350,7 @@ data Action = Action
|
||||||
, _params :: [DocQueryParam] -- type collected + user supplied info
|
, _params :: [DocQueryParam] -- type collected + user supplied info
|
||||||
, _notes :: [DocNote] -- user supplied
|
, _notes :: [DocNote] -- user supplied
|
||||||
, _mxParams :: [(String, [DocQueryParam])] -- type collected + user supplied info
|
, _mxParams :: [(String, [DocQueryParam])] -- type collected + user supplied info
|
||||||
, _rqbody :: Maybe ByteString -- user supplied
|
, _rqbody :: Maybe [(M.MediaType, ByteString)] -- user supplied
|
||||||
, _response :: Response -- user supplied
|
, _response :: Response -- user supplied
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
@ -428,24 +433,38 @@ class HasDocs layout where
|
||||||
-- You can also instantiate this class using 'toSamples' instead of
|
-- You can also instantiate this class using 'toSamples' instead of
|
||||||
-- 'toSample': it lets you specify different responses along with
|
-- 'toSample': it lets you specify different responses along with
|
||||||
-- some context (as 'Text') that explains when you're supposed to
|
-- some context (as 'Text') that explains when you're supposed to
|
||||||
-- get the corresponding response.
|
-- get the corresponding response.
|
||||||
class ToJSON a => ToSample a where
|
class ToJSON a => ToSample a where
|
||||||
{-# MINIMAL (toSample | toSamples) #-}
|
{-# MINIMAL (toSample | toSamples) #-}
|
||||||
toSample :: Maybe a
|
toSample :: Maybe a
|
||||||
toSample = fmap snd $ listToMaybe samples
|
toSample = snd <$> listToMaybe samples
|
||||||
where samples = toSamples :: [(Text, a)]
|
where samples = toSamples :: [(Text, a)]
|
||||||
|
|
||||||
toSamples :: [(Text, a)]
|
toSamples :: [(Text, a)]
|
||||||
toSamples = maybe [] (return . ("",)) s
|
toSamples = maybe [] (return . ("",)) s
|
||||||
where s = toSample :: Maybe a
|
where s = toSample :: Maybe a
|
||||||
|
|
||||||
sampleByteString :: forall a. ToSample a => Proxy a -> Maybe ByteString
|
-- | Synthesise a sample value of a type, encoded in the specified media types.
|
||||||
sampleByteString Proxy = fmap encodePretty (toSample :: Maybe a)
|
sampleByteString
|
||||||
|
:: forall ctypes a. (ToSample a, IsNonEmpty ctypes, AllMimeRender ctypes a)
|
||||||
|
=> Proxy ctypes
|
||||||
|
-> Proxy a
|
||||||
|
-> Maybe [(M.MediaType, ByteString)]
|
||||||
|
sampleByteString ctypes@Proxy Proxy =
|
||||||
|
fmap (amr ctypes) (toSample :: Maybe a)
|
||||||
|
|
||||||
sampleByteStrings :: forall a. ToSample a => Proxy a -> [(Text, ByteString)]
|
-- | Synthesise a list of sample values of a particular type, encoded in the
|
||||||
sampleByteStrings Proxy = samples & traverse._2 %~ encodePretty
|
-- specified media types.
|
||||||
|
sampleByteStrings
|
||||||
where samples = toSamples :: [(Text, a)]
|
:: forall ctypes a. (ToSample a, IsNonEmpty ctypes, AllMimeRender ctypes a)
|
||||||
|
=> Proxy ctypes
|
||||||
|
-> Proxy a
|
||||||
|
-> [(Text, M.MediaType, ByteString)]
|
||||||
|
sampleByteStrings ctypes@Proxy Proxy =
|
||||||
|
let samples = toSamples :: [(Text, a)]
|
||||||
|
ext t (a,b) = (t,a,b)
|
||||||
|
enc (t, s) = ext t <$> amr ctypes s
|
||||||
|
in concatMap enc samples
|
||||||
|
|
||||||
-- | The class that helps us automatically get documentation
|
-- | The class that helps us automatically get documentation
|
||||||
-- for GET parameters.
|
-- for GET parameters.
|
||||||
|
@ -574,16 +593,26 @@ markdown api = unlines $
|
||||||
|
|
||||||
where values = param ^. paramValues
|
where values = param ^. paramValues
|
||||||
|
|
||||||
rqbodyStr :: Maybe ByteString -> [String]
|
rqbodyStr :: Maybe [(M.MediaType, ByteString)]-> [String]
|
||||||
rqbodyStr Nothing = []
|
rqbodyStr Nothing = []
|
||||||
rqbodyStr (Just b) =
|
rqbodyStr (Just b) = concatMap formatBody b
|
||||||
"#### Request Body:" :
|
|
||||||
jsonStr b
|
|
||||||
|
|
||||||
jsonStr b =
|
formatBody (m, b) =
|
||||||
|
"#### Request Body: `" <> show (M.mainType m <> "/" <> M.subType m) <> "`" :
|
||||||
|
contentStr m b
|
||||||
|
|
||||||
|
markdownForType mime_type =
|
||||||
|
case (M.mainType mime_type, M.subType mime_type) of
|
||||||
|
("text", "html") -> "html"
|
||||||
|
("application", "xml") -> "xml"
|
||||||
|
("application", "json") -> "javascript"
|
||||||
|
("application", "javascript") -> "javascript"
|
||||||
|
(_, _) -> ""
|
||||||
|
|
||||||
|
contentStr mime_type body =
|
||||||
"" :
|
"" :
|
||||||
"``` javascript" :
|
"``` " <> markdownForType mime_type :
|
||||||
cs b :
|
cs body :
|
||||||
"```" :
|
"```" :
|
||||||
"" :
|
"" :
|
||||||
[]
|
[]
|
||||||
|
@ -597,9 +626,9 @@ markdown api = unlines $
|
||||||
|
|
||||||
where bodies = case resp ^. respBody of
|
where bodies = case resp ^. respBody of
|
||||||
[] -> [" - No response body\n"]
|
[] -> [" - No response body\n"]
|
||||||
[("", r)] -> " - Response body as below." : jsonStr r
|
[("", t, r)] -> " - Response body as below." : contentStr t r
|
||||||
xs ->
|
xs ->
|
||||||
concatMap (\(ctx, r) -> (" - " <> T.unpack ctx) : jsonStr r) xs
|
concatMap (\(ctx, t, r) -> (" - " <> T.unpack ctx) : contentStr t r) xs
|
||||||
|
|
||||||
-- * Instances
|
-- * Instances
|
||||||
|
|
||||||
|
@ -641,14 +670,16 @@ instance HasDocs Delete where
|
||||||
action' = action & response.respBody .~ []
|
action' = action & response.respBody .~ []
|
||||||
& response.respStatus .~ 204
|
& response.respStatus .~ 204
|
||||||
|
|
||||||
instance ToSample a => HasDocs (Get cts a) where
|
instance (ToSample a, IsNonEmpty cts, AllMimeRender cts a) => HasDocs (Get cts a) where
|
||||||
docsFor Proxy (endpoint, action) =
|
docsFor Proxy (endpoint, action) =
|
||||||
single endpoint' action'
|
single endpoint' action'
|
||||||
|
|
||||||
where endpoint' = endpoint & method .~ DocGET
|
where endpoint' = endpoint & method .~ DocGET
|
||||||
action' = action & response.respBody .~ sampleByteStrings p
|
action' = action & response.respBody .~ sampleByteStrings t p
|
||||||
|
t = Proxy :: Proxy cts
|
||||||
p = Proxy :: Proxy a
|
p = Proxy :: Proxy a
|
||||||
|
|
||||||
|
|
||||||
instance (KnownSymbol sym, HasDocs sublayout)
|
instance (KnownSymbol sym, HasDocs sublayout)
|
||||||
=> HasDocs (Header sym a :> sublayout) where
|
=> HasDocs (Header sym a :> sublayout) where
|
||||||
docsFor Proxy (endpoint, action) =
|
docsFor Proxy (endpoint, action) =
|
||||||
|
@ -658,29 +689,26 @@ instance (KnownSymbol sym, HasDocs sublayout)
|
||||||
action' = over headers (|> headername) action
|
action' = over headers (|> headername) action
|
||||||
headername = pack $ symbolVal (Proxy :: Proxy sym)
|
headername = pack $ symbolVal (Proxy :: Proxy sym)
|
||||||
|
|
||||||
instance ToSample a => HasDocs (Post cts a) where
|
instance (ToSample a, IsNonEmpty cts, AllMimeRender cts a) => HasDocs (Post cts a) where
|
||||||
docsFor Proxy (endpoint, action) =
|
docsFor Proxy (endpoint, action) =
|
||||||
single endpoint' action'
|
single endpoint' action'
|
||||||
|
|
||||||
where endpoint' = endpoint & method .~ DocPOST
|
where endpoint' = endpoint & method .~ DocPOST
|
||||||
|
action' = action & response.respBody .~ sampleByteStrings t p
|
||||||
action' = action & response.respBody .~ sampleByteStrings p
|
|
||||||
& response.respStatus .~ 201
|
& response.respStatus .~ 201
|
||||||
|
t = Proxy :: Proxy cts
|
||||||
p = Proxy :: Proxy a
|
p = Proxy :: Proxy a
|
||||||
|
|
||||||
instance ToSample a => HasDocs (Put cts a) where
|
instance (ToSample a, IsNonEmpty cts, AllMimeRender cts a) => HasDocs (Put cts a) where
|
||||||
docsFor Proxy (endpoint, action) =
|
docsFor Proxy (endpoint, action) =
|
||||||
single endpoint' action'
|
single endpoint' action'
|
||||||
|
|
||||||
where endpoint' = endpoint & method .~ DocPUT
|
where endpoint' = endpoint & method .~ DocPUT
|
||||||
|
action' = action & response.respBody .~ sampleByteStrings t p
|
||||||
action' = action & response.respBody .~ sampleByteStrings p
|
|
||||||
& response.respStatus .~ 200
|
& response.respStatus .~ 200
|
||||||
|
t = Proxy :: Proxy cts
|
||||||
p = Proxy :: Proxy a
|
p = Proxy :: Proxy a
|
||||||
|
|
||||||
|
|
||||||
instance (KnownSymbol sym, ToParam (QueryParam sym a), HasDocs sublayout)
|
instance (KnownSymbol sym, ToParam (QueryParam sym a), HasDocs sublayout)
|
||||||
=> HasDocs (QueryParam sym a :> sublayout) where
|
=> HasDocs (QueryParam sym a :> sublayout) where
|
||||||
|
|
||||||
|
@ -755,20 +783,23 @@ instance (KnownSymbol sym, {- ToParam (MatrixFlag sym), -} HasDocs sublayout)
|
||||||
endpoint' = over path (\p -> p ++ [";" ++ symbolVal symP]) endpoint
|
endpoint' = over path (\p -> p ++ [";" ++ symbolVal symP]) endpoint
|
||||||
symP = Proxy :: Proxy sym
|
symP = Proxy :: Proxy sym
|
||||||
|
|
||||||
|
|
||||||
instance HasDocs Raw where
|
instance HasDocs Raw where
|
||||||
docsFor _proxy (endpoint, action) =
|
docsFor _proxy (endpoint, action) =
|
||||||
single endpoint action
|
single endpoint action
|
||||||
|
|
||||||
instance (ToSample a, HasDocs sublayout)
|
-- TODO: We use 'AllMimeRender' here because we need to be able to show the
|
||||||
|
-- example data. However, there's no reason to believe that the instances of
|
||||||
|
-- 'AllMimeUnrender' and 'AllMimeRender' actually agree (or to suppose that
|
||||||
|
-- both are even defined) for any particular type.
|
||||||
|
instance (ToSample a, IsNonEmpty cts, AllMimeRender cts a, HasDocs sublayout)
|
||||||
=> HasDocs (ReqBody cts a :> sublayout) where
|
=> HasDocs (ReqBody cts a :> sublayout) where
|
||||||
|
|
||||||
docsFor Proxy (endpoint, action) =
|
docsFor Proxy (endpoint, action) =
|
||||||
docsFor sublayoutP (endpoint, action')
|
docsFor sublayoutP (endpoint, action')
|
||||||
|
|
||||||
where sublayoutP = Proxy :: Proxy sublayout
|
where sublayoutP = Proxy :: Proxy sublayout
|
||||||
|
action' = action & rqbody .~ sampleByteString t p
|
||||||
action' = action & rqbody .~ sampleByteString p
|
t = Proxy :: Proxy cts
|
||||||
p = Proxy :: Proxy a
|
p = Proxy :: Proxy a
|
||||||
|
|
||||||
instance (KnownSymbol path, HasDocs sublayout) => HasDocs (path :> sublayout) where
|
instance (KnownSymbol path, HasDocs sublayout) => HasDocs (path :> sublayout) where
|
||||||
|
|
Loading…
Reference in a new issue