servant-util-0.3: src/Servant/Util/Swagger.hs
module Servant.Util.Swagger
( ParamDescription
, DescribedOpenApiParam
, DescribedSwaggerParam
, paramDescription
, QueryFlagDescription
, SwaggerrizeApi
) where
import Universum
import qualified Data.OpenApi.Lens as OT
import qualified Data.Swagger.Lens as ST
import Control.Exception (assert)
import Control.Lens (_head, ix, zoom, (?=))
import qualified Data.OpenApi as O
import qualified Data.Swagger as S
import GHC.TypeLits (KnownSymbol, Symbol)
import Servant (Capture', Description, EmptyAPI, NoContent, QueryFlag, QueryParam', Raw, StdMethod,
Verb, (:<|>), (:>))
import Servant.OpenApi (HasOpenApi (..))
import Servant.Swagger (HasSwagger (..))
import Servant.Util.Common
----------------------------------------------------------------------------
-- Parameter description
----------------------------------------------------------------------------
-- | Description of parameter.
--
-- Unfortunately, @servant-swagger@ package, when deriving description of
-- an endpoint parameter, fills its description for you and makes you implement
-- just 'ParamSchema' which has no description field.
-- To circumvent that you can define description in instance of this type family
-- and later override swagger derivation accordingly.
type family ParamDescription a :: Symbol
type DescribedSwaggerParam a = (S.ToParamSchema a, KnownSymbol (ParamDescription a))
type DescribedOpenApiParam a = (O.ToParamSchema a, KnownSymbol (ParamDescription a))
-- | Set description according to 'ParamDescription' definition.
paramDescription
:: forall a proxy.
KnownSymbol (ParamDescription a)
=> proxy a -> Text
paramDescription _ = symbolValT @(ParamDescription a)
----------------------------------------------------------------------------
-- Capture description
----------------------------------------------------------------------------
-- | Like 'Capture', but does not modify description of 404 error (which looks
-- pretty like robot-generated).
-- See 'Servant.Util.Combinators.ErrorResponses' module for manual errors
-- definition.
data SwaggerCapture (mods :: [*]) (sym :: Symbol) a
instance (HasSwagger (Capture' mods sym a :> api), HasSwagger api) =>
HasSwagger (SwaggerCapture mods sym a :> api) where
toSwagger _ =
toSwagger (Proxy @(Capture' mods sym a :> api))
& desc404L .~ fromMaybe "" pureDesc404
where
desc404L :: Traversal' S.Swagger Text
desc404L = S.allOperations . S.responses . S.responses .
ix 404 . ST._Inline . S.description
pureDesc404 = toSwagger (Proxy @api) ^? desc404L
instance (HasOpenApi (Capture' mods sym a :> api), HasOpenApi api) =>
HasOpenApi (SwaggerCapture mods sym a :> api) where
toOpenApi _ =
toOpenApi (Proxy @(Capture' mods sym a :> api))
& desc404L .~ fromMaybe "" pureDesc404
where
desc404L :: Traversal' O.OpenApi Text
desc404L = O.allOperations . O.responses . O.responses .
ix 404 . OT._Inline . O.description
pureDesc404 = toOpenApi (Proxy @api) ^? desc404L
----------------------------------------------------------------------------
-- QueryParam description
----------------------------------------------------------------------------
-- | Like 'QueryParam', but does not modify description of 404 error.
-- See 'Servant.Util.Combinators.ErrorResponses' module for manual errors
-- definition.
data SwaggerQueryParam (mods :: [*]) (sym :: Symbol) a
instance (HasSwagger (QueryParam' mods sym a :> api), HasSwagger api) =>
HasSwagger (SwaggerQueryParam mods sym a :> api) where
toSwagger _ =
toSwagger (Proxy @(QueryParam' mods sym a :> api))
& desc404L .~ fromMaybe "" pureDesc404
where
desc404L :: Traversal' S.Swagger Text
desc404L = S.allOperations . S.responses . S.responses .
ix 404 . ST._Inline . S.description
pureDesc404 = toSwagger (Proxy @api) ^? desc404L
instance (HasOpenApi (QueryParam' mods sym a :> api), HasOpenApi api) =>
HasOpenApi (SwaggerQueryParam mods sym a :> api) where
toOpenApi _ =
toOpenApi (Proxy @(QueryParam' mods sym a :> api))
& desc404L .~ fromMaybe "" pureDesc404
where
desc404L :: Traversal' O.OpenApi Text
desc404L = O.allOperations . O.responses . O.responses .
ix 404 . OT._Inline . O.description
pureDesc404 = toOpenApi (Proxy @api) ^? desc404L
----------------------------------------------------------------------------
-- Query flag description
----------------------------------------------------------------------------
-- | Defines swagger description for the given `QueryFlag` parameter.
type family QueryFlagDescription (name :: Symbol) :: Symbol
-- | Replacement for 'QueryFlag' which has a better documentation.
data SwaggerQueryFlag (name :: Symbol)
type instance QueryFlagDescription "onlyCount" =
"If this parameter is present, return only the total count of items."
instance (HasSwagger subApi, KnownSymbol name, KnownSymbol (QueryFlagDescription name)) =>
HasSwagger (SwaggerQueryFlag name :> subApi) where
toSwagger _ = toSwagger (Proxy @(QueryFlag name :> subApi)) `executingState` do
zoom (S.allOperations . S.parameters . _head . ST._Inline) $ do
paramName <- use S.name
assert (name == paramName) pass
S.description ?= desc
where
name = symbolValT @name
desc = symbolValT @(QueryFlagDescription name)
instance (HasOpenApi subApi, KnownSymbol name, KnownSymbol (QueryFlagDescription name)) =>
HasOpenApi (SwaggerQueryFlag name :> subApi) where
toOpenApi _ = toOpenApi (Proxy @(QueryFlag name :> subApi)) `executingState` do
zoom (O.allOperations . O.parameters . _head . OT._Inline) $ do
paramName <- use O.name
assert (name == paramName) pass
O.description ?= desc
where
name = symbolValT @name
desc = symbolValT @(QueryFlagDescription name)
----------------------------------------------------------------------------
-- Global
----------------------------------------------------------------------------
{- | This applies following transformations to API for the sake of better swagger
documentation.
* Response of methods returning `()` is replaced with `NoContents` (otherwise invalid
swagger is generated).
* `Capture`s and `QueryParam`s are attached a description according to
'ParamDescription' type family (default description is empty).
* @QueryFlag name@ occurences are attached descriptions according to
@ParamsDescription (QueryFlagDescription name)@ (there was no description by default).
-}
type family SwaggerrizeApi api where
SwaggerrizeApi ((path :: Symbol) :> api) =
path :> SwaggerrizeApi api
SwaggerrizeApi (Capture' mods sym a :> api) =
SwaggerCapture (Description (ParamDescription a) ': mods) sym a :> SwaggerrizeApi api
SwaggerrizeApi (QueryParam' mods sym a :> api) =
SwaggerQueryParam (Description (ParamDescription a) ': mods) sym a
:> SwaggerrizeApi api
SwaggerrizeApi (QueryFlag name :> api) =
SwaggerQueryFlag name :> SwaggerrizeApi api
SwaggerrizeApi (arg :> api) =
arg :> SwaggerrizeApi api
SwaggerrizeApi (api1 :<|> api2) =
SwaggerrizeApi api1 :<|> SwaggerrizeApi api2
SwaggerrizeApi (Verb (method :: StdMethod) (code :: Nat) ctx ()) =
Verb method code ctx NoContent
SwaggerrizeApi (Verb (method :: StdMethod) (code :: Nat) ctx a) =
Verb method code ctx a
SwaggerrizeApi Raw = Raw
SwaggerrizeApi EmptyAPI = EmptyAPI