packages feed

relay-pagination-servant-0.1.0.0: src/Relay/Pagination/Servant/OpenApi.hs

{-# OPTIONS_GHC -Wno-orphans #-}

-- | OpenAPI 3.1 documentation for the 'RelayPage' combinator, plus the
-- schema instances for the core pagination types.
--
-- This module is the __canonical (sole permitted) home__ of the OpenAPI
-- orphan instances for the core @relay-pagination@ types ('Cursor',
-- 'PageInfo', 'Edge', 'Connection') and for 'RelayPageError'. The core
-- package must not depend on @openapi-hs@, and @openapi-hs@ cannot know
-- about this library, so orphans are the standard escape hatch — the danger
-- of orphans is duplicate definitions elsewhere, therefore no other package
-- in this repository or downstream may define these instances.
--
-- The 'HasOpenApi' instance documents the four Relay query parameters,
-- including the type-level default and maximum page sizes. It deliberately
-- does not add a synthetic 400 response: the terminal 'MultiVerb' declares
-- @Respond 400 ... RelayPageError@ and @servant-openapi-hs@ derives the
-- response from that type, which makes a plain @Get@ terminal visibly
-- incomplete rather than letting documentation pretend its client can decode
-- an error the route type omitted.
module Relay.Pagination.Servant.OpenApi () where

import Control.Lens ((&), (.~), (?~))
import Data.Aeson (toJSON)
import Data.OpenApi
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import Data.Text qualified as Text
import GHC.TypeLits (KnownNat, natVal)
import Relay.Pagination
import Relay.Pagination.Servant
import Servant.API ((:>))
import Servant.OpenApi (HasOpenApi (..))
import Servant.OpenApi.Internal (addParam)

instance
  (KnownNat d, KnownNat mx, HasOpenApi sub) =>
  HasOpenApi (RelayPage d mx :> sub)
  where
  toOpenApi _ =
    -- addParam prepends to each operation's parameter list, so apply in
    -- reverse to document the parameters in first, after, last, before order.
    toOpenApi (Proxy @sub)
      & addParam beforeParam
      & addParam lastParam
      & addParam afterParam
      & addParam firstParam
    where
      defSize, maxSize :: Integer
      defSize = natVal (Proxy @d)
      maxSize = natVal (Proxy @mx)

      sizeSchema =
        mempty
          & type_ ?~ OpenApiTypeSingle OpenApiInteger
          & minimum_ ?~ 0
          & maximum_ ?~ fromInteger maxSize

      cursorSchema = toParamSchema (Proxy @Cursor)

      queryParam paramName paramDescription paramSchema =
        mempty
          & name .~ paramName
          & in_ .~ ParamQuery
          & required ?~ False
          & description ?~ paramDescription
          & schema ?~ Inline paramSchema

      firstParam =
        queryParam
          "first"
          ( "Forward page size (0.."
              <> tshow maxSize
              <> "). Defaults to "
              <> tshow defSize
              <> " when neither 'first' nor 'last' is given. "
              <> "Cannot be combined with 'last' or 'before'."
          )
          (sizeSchema & default_ ?~ toJSON defSize)
      lastParam =
        queryParam
          "last"
          ( "Backward page size (0.."
              <> tshow maxSize
              <> "). Cannot be combined with 'first' or 'after'."
          )
          sizeSchema
      afterParam =
        queryParam
          "after"
          "Opaque cursor: return items after this position (forward pagination)."
          cursorSchema
      beforeParam =
        queryParam
          "before"
          "Opaque cursor: return items before this position (backward pagination)."
          cursorSchema

      tshow :: (Show a) => a -> Text
      tshow = Text.pack . show

instance ToParamSchema Cursor where
  toParamSchema _ =
    mempty
      & type_ ?~ OpenApiTypeSingle OpenApiString
      & format ?~ "base64url"

instance ToSchema Cursor where
  declareNamedSchema _ =
    pure . NamedSchema (Just "Cursor") $
      mempty
        & type_ ?~ OpenApiTypeSingle OpenApiString
        & format ?~ "base64url"
        & description ?~ "opaque pagination cursor"

instance ToSchema RelayPageError

instance ToSchema PageInfo

instance (ToSchema a) => ToSchema (Edge a) where
  declareNamedSchema = genericDeclareNamedSchema defaultSchemaOptions

instance (ToSchema a) => ToSchema (Connection a) where
  declareNamedSchema = genericDeclareNamedSchema defaultSchemaOptions