packages feed

webgear-openapi-1.3.0: src/WebGear/OpenApi/Handler.hs

{-# LANGUAGE CPP #-}

{- | An implementation of `Handler` to generate `OpenApi` documentation
 from WebGear API specifications.
-}
module WebGear.OpenApi.Handler (
  OpenApiHandler (..),
  Documentation (..),
  consumeDescription,
  consumeSummary,
  addRouteDocumentation,
  addRootPath,
  toOpenApi,
) where

import Control.Applicative ((<|>))
import Control.Arrow (Arrow (..), ArrowChoice (..), ArrowPlus (..), ArrowZero (..))
import Control.Arrow.Operations (ArrowError (..))
import qualified Control.Category as Cat
import Control.Lens (at, (%~), (&), (.~), (?~), (^.))
import Control.Monad ((<=<))
import Control.Monad.State.Strict (MonadState, State, evalState, state)
import Data.Coerce (coerce)
import qualified Data.HashMap.Strict.InsOrd as Map
import Data.OpenApi (
  OpenApi,
  Operation,
  PathItem,
  Referenced (..),
  Response,
  allOperations,
  delete,
  description,
  externalDocs,
  get,
  head_,
  options,
  parameters,
  patch,
  paths,
  post,
  put,
  servers,
  summary,
  trace,
 )
import WebGear.Core.Handler (
  Description (..),
  Handler (..),
  RouteMismatch,
  RoutePath (..),
  Summary (..),
 )

-- | A handler that captures `OpenApi` documentation of API specifications.
newtype OpenApiHandler m a b = OpenApiHandler (OpenApi -> State Documentation OpenApi)

data Documentation = Documentation !(Maybe Description) !(Maybe Summary)

consumeDescription :: (MonadState Documentation m) => m (Maybe Description)
consumeDescription = state $ \(Documentation d s) -> (d, Documentation Nothing s)

consumeSummary :: (MonadState Documentation m) => m (Maybe Summary)
consumeSummary = state $ \(Documentation d s) -> (s, Documentation d Nothing)

addRouteDocumentation :: (MonadState Documentation m) => OpenApi -> m OpenApi
addRouteDocumentation doc = do
  desc <- consumeDescription
  summ <- consumeSummary
  pure $
    doc
      -- keep any existing documentation
      & allOperations . summary %~ (<|> fmap getSummary summ)
      & allOperations . description %~ (<|> fmap getDescription desc)

addRootPath :: OpenApi -> OpenApi
addRootPath doc = doc & paths .~ [("/", rootPathItem)]
  where
    rootPathItem :: PathItem
    rootPathItem =
      mempty @PathItem
        & delete ?~ opr
        & get ?~ opr
        & head_ ?~ opr
        & options ?~ opr
        & patch ?~ opr
        & post ?~ opr
        & put ?~ opr
        & trace ?~ opr

    opr :: Operation
    opr = mempty @Operation & at 0 ?~ Inline (mempty @Response)

instance Cat.Category (OpenApiHandler m) where
  {-# INLINE id #-}
  id :: OpenApiHandler m a a
  id = OpenApiHandler pure

  {-# INLINE (.) #-}
  (.) :: OpenApiHandler m b c -> OpenApiHandler m a b -> OpenApiHandler m a c
  OpenApiHandler g . OpenApiHandler f = OpenApiHandler $ f <=< g

instance Arrow (OpenApiHandler m) where
  {-# INLINE arr #-}
  arr :: (a -> b) -> OpenApiHandler m a b
  arr _ = OpenApiHandler pure

  {-# INLINE first #-}
  first :: OpenApiHandler m b c -> OpenApiHandler m (b, d) (c, d)
  first = coerce

  {-# INLINE second #-}
  second :: OpenApiHandler m b c -> OpenApiHandler m (d, b) (d, c)
  second = coerce

instance ArrowZero (OpenApiHandler m) where
  {-# INLINE zeroArrow #-}
  zeroArrow :: OpenApiHandler m b c
  zeroArrow = OpenApiHandler pure

newtype MergeOpenApi = MergeOpenApi (OpenApi -> State Documentation OpenApi)

instance Semigroup MergeOpenApi where
  MergeOpenApi f <> MergeOpenApi g =
    MergeOpenApi $ \doc -> do
      a <- f doc
      b <- g doc
      pure $
        (a <> b)
          & paths .~ Map.unionWith mergePathItem (a ^. paths) (b ^. paths)
          & externalDocs .~ (a ^. externalDocs <|> b ^. externalDocs)
    where
      mergePathItem :: PathItem -> PathItem -> PathItem
      mergePathItem x y =
        mempty @PathItem
          & delete .~ x ^. delete <> y ^. delete
          & get .~ x ^. get <> y ^. get
          & head_ .~ x ^. head_ <> y ^. head_
          & options .~ x ^. options <> y ^. options
          & patch .~ x ^. patch <> y ^. patch
          & post .~ x ^. post <> y ^. post
          & put .~ x ^. put <> y ^. put
          & trace .~ x ^. trace <> y ^. trace
          & summary .~ (x ^. summary <|> y ^. summary)
          & description .~ (x ^. description <|> y ^. description)
          & parameters .~ (x ^. parameters <> y ^. parameters)
          & servers .~ (x ^. servers <> y ^. servers)

instance ArrowPlus (OpenApiHandler m) where
  {-# INLINE (<+>) #-}
  (<+>) :: OpenApiHandler m b c -> OpenApiHandler m b c -> OpenApiHandler m b c
  OpenApiHandler f <+> OpenApiHandler g = coerce $ MergeOpenApi f <> MergeOpenApi g

instance ArrowChoice (OpenApiHandler m) where
  {-# INLINE left #-}
  left :: OpenApiHandler m b c -> OpenApiHandler m (Either b d) (Either c d)
  left (OpenApiHandler doc) = OpenApiHandler doc

  {-# INLINE right #-}
  right :: OpenApiHandler m b c -> OpenApiHandler m (Either d b) (Either d c)
  right (OpenApiHandler doc) = OpenApiHandler doc

  {-# INLINE (+++) #-}
  (+++) :: OpenApiHandler m b c -> OpenApiHandler m b' c' -> OpenApiHandler m (Either b b') (Either c c')
  OpenApiHandler f +++ OpenApiHandler g = coerce $ MergeOpenApi f <> MergeOpenApi g

  {-# INLINE (|||) #-}
  (|||) :: OpenApiHandler m b d -> OpenApiHandler m c d -> OpenApiHandler m (Either b c) d
  OpenApiHandler f ||| OpenApiHandler g = coerce $ MergeOpenApi f <> MergeOpenApi g

instance ArrowError RouteMismatch (OpenApiHandler m) where
  {-# INLINE raise #-}
  raise = OpenApiHandler pure

  {-# INLINE handle #-}
  OpenApiHandler f `handle` OpenApiHandler g = coerce $ MergeOpenApi f <> MergeOpenApi g

  {-# INLINE tryInUnless #-}
  tryInUnless (OpenApiHandler f) (OpenApiHandler g) (OpenApiHandler h) =
    coerce $ MergeOpenApi f <> MergeOpenApi g <> MergeOpenApi h

instance (Monad m) => Handler (OpenApiHandler m) m where
  {-# INLINE arrM #-}
  arrM :: (a -> m b) -> OpenApiHandler m a b
  arrM _ = OpenApiHandler pure

  {-# INLINE consumeRoute #-}
  consumeRoute :: OpenApiHandler m RoutePath a -> OpenApiHandler m () a
  consumeRoute (OpenApiHandler f) = OpenApiHandler f

  {-# INLINE setDescription #-}
  setDescription :: Description -> OpenApiHandler m a a
  setDescription d = OpenApiHandler $ \doc ->
    state $ \(Documentation _ s) -> (doc, Documentation (Just d) s)

  {-# INLINE setSummary #-}
  setSummary :: Summary -> OpenApiHandler m a a
  setSummary s = OpenApiHandler $ \doc ->
    state $ \(Documentation d _) -> (doc, Documentation d (Just s))

-- | Generate OpenApi documentation from a handler
toOpenApi :: OpenApiHandler m a b -> OpenApi
toOpenApi (OpenApiHandler f) = evalState (f mempty) (Documentation Nothing Nothing)