mig-0.2.0.0: src/Mig/Core/OpenApi.hs
-- | Renders mig-servers as OpenApi schemas
module Mig.Core.OpenApi (
toOpenApi,
) where
import Control.Lens (at, (%~), (&), (.~), (?~))
import Data.Aeson (ToJSON (..))
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
import Data.HashSet.InsOrd qualified as Set
import Data.Monoid (Endo (..))
import Data.OpenApi hiding (Server (..))
import Data.Proxy
import Data.Text (Text)
import Data.Text qualified as Text
import Mig.Core.Api (Api)
import Mig.Core.Api qualified as Api
import Mig.Core.Class.Route (Route (..))
import Mig.Core.Server (Server (..), fillCaptures)
import Mig.Core.Types.Info (IsRequired (..), RouteInfo)
import Mig.Core.Types.Info qualified as Info
import Network.HTTP.Types.Method
import Network.HTTP.Types.Status (Status (..))
addCapture :: Text -> OpenApi -> OpenApi
addCapture captureName =
prependPath ("{" <> Text.unpack captureName <> "}")
-- | Reads OpenApi schema for a server
toOpenApi :: Server m -> OpenApi
toOpenApi (Server x) = fromApiInfo (fmap (.info) $ fillCaptures x)
fromApiInfo :: Api RouteInfo -> OpenApi
fromApiInfo = \case
Api.Empty -> mempty
Api.Append a b -> fromApiInfo a <> fromApiInfo b
Api.WithPath (Api.Path path) a -> foldr withPath (fromApiInfo a) path
Api.HandleRoute route -> fromRoute route
withPath :: Api.PathItem -> OpenApi -> OpenApi
withPath = \case
Api.StaticPath pathName -> prependPath (Text.unpack pathName)
Api.CapturePath captureName -> addCapture captureName
fromRoute :: RouteInfo -> OpenApi
fromRoute routeInfo =
appEndo
(foldMap (Endo . fromRouteInput) routeInfo.inputs)
(fromRouteOutput routeInfo)
fromRouteOutput :: RouteInfo -> OpenApi
fromRouteOutput routeInfo =
mempty
& components . schemas .~ defs
& paths . at "/"
?~ ( mempty
& method
?~ ( mempty
& at code
?~ Inline
( mempty
& content
.~ InsOrdHashMap.fromList
[(t, mempty & schema .~ mref) | t <- responseContentTypes]
& headers .~ responseHeaders
)
& tags .~ Set.fromList routeInfo.tags
& summary .~ nonEmptyText routeInfo.summary
& description .~ nonEmptyText routeInfo.description
)
)
where
method = case routeInfo.method of
Just m | m == methodGet -> get
Just m | m == methodPost -> post
Just m | m == methodPut -> put
Just m | m == methodDelete -> delete
Just m | m == methodOptions -> options
Just m | m == methodHead -> head_
Just m | m == methodPatch -> patch
Just m | m == methodTrace -> trace
_ -> mempty
code = routeInfo.output.status.statusCode
responseContentTypes = [routeInfo.output.media]
-- TODO: is it always empty?
responseHeaders = Inline <$> mempty
Info.SchemaDefs defs mref = routeInfo.output.schema
fromRouteInput :: Info.Describe Info.RouteInput -> OpenApi -> OpenApi
fromRouteInput descInput base = case descInput.content of
Info.ReqBodyInput inputType bodySchema -> onRequestBody inputType bodySchema
Info.RawBodyInput -> base
Info.CaptureInput captureName captureSchema -> onCapture captureName captureSchema
Info.QueryInput isRequired queryName querySchema -> onQuery isRequired queryName querySchema
Info.QueryFlagInput queryName -> onQueryFlag queryName
Info.HeaderInput isRequired headerName headerSchema -> onHeader isRequired headerName headerSchema
where
onCapture = onParam addDefaultResponse404 ParamPath (IsRequired True)
onQuery = onParam addDefaultResponse400 ParamQuery
onHeader = onParam addDefaultResponse400 ParamHeader
onParam defResponse paramType (IsRequired isRequired) paramName paramSchema =
base
& addParam param
& defResponse paramName
where
param =
mempty
& name .~ paramName
& description .~ (nonEmptyText =<< descInput.description)
& required ?~ isRequired
& in_ .~ paramType
& schema ?~ (Inline paramSchema)
onRequestBody bodyInputType (Info.SchemaDefs defs ref) =
base
& addRequestBody reqBody
& addDefaultResponse400 "body"
& components . schemas %~ (<> defs)
where
reqBody =
(mempty :: RequestBody)
& description .~ (nonEmptyText =<< descInput.description)
& content .~ InsOrdHashMap.fromList [(t, mempty & schema .~ ref) | t <- [bodyContentType]]
bodyContentType = bodyInputType
onQueryFlag queryName =
base
& addParam param
& addDefaultResponse400 queryName
where
param =
mempty
& name .~ queryName
& in_ .~ ParamQuery
& allowEmptyValue ?~ True
& schema
?~ ( Inline $
(toParamSchema (Proxy :: Proxy Bool))
& default_ ?~ toJSON False
)
-------------------------------------------------------------------------------------
-- openapi utils
nonEmptyText :: Text -> Maybe Text
nonEmptyText txt
| Text.null txt = Nothing
| otherwise = Just txt
-- | Add RequestBody to every operations in the spec.
addRequestBody :: RequestBody -> OpenApi -> OpenApi
addRequestBody rb = allOperations . requestBody ?~ Inline rb
-- | Add parameter to every operation in the spec.
addParam :: Param -> OpenApi -> OpenApi
addParam param = allOperations . parameters %~ (Inline param :)
addDefaultResponse404 :: ParamName -> OpenApi -> OpenApi
addDefaultResponse404 pname = setResponseWith (\old _new -> alter404 old) 404 (return response404)
where
sname = markdownCode pname
description404 = sname <> " not found"
alter404 = description %~ ((sname <> " or ") <>)
response404 = mempty & description .~ description404
addDefaultResponse400 :: ParamName -> OpenApi -> OpenApi
addDefaultResponse400 pname = setResponseWith (\old _new -> alter400 old) 400 (return response400)
where
sname = markdownCode pname
description400 = "Invalid " <> sname
alter400 = description %~ (<> (" or " <> sname))
response400 = mempty & description .~ description400
-- | Format given text as inline code in Markdown.
markdownCode :: Text -> Text
markdownCode s = "`" <> s <> "`"