packages feed

rfc-servant-0.0.0.1: src/RFC/Servant/Server/ApiDoc.hs

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DefaultSignatures     #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude     #-}
{-# LANGUAGE UndecidableInstances  #-}

module RFC.Servant.Server.ApiDoc
  ( apiToHtml
  , apiToAscii
  , apiToSwagger
  , apiMiddleware
  , swaggerSchemaOptions
  , ToSchemaRFC
  , module Data.Swagger
  , module Servant.Swagger.UI
  , module Servant.Swagger
  ) where

import           Control.Lens
import qualified Data.Aeson                      as Aeson
import           Data.Aeson.Types                ( fromEncoding, toEncoding )
import qualified Data.Binary.Builder             as Builder
import           Data.Default                    ( def )
import           Data.Monoid                     ( (<>) )
import           Data.Swagger
import           Data.Swagger.Declare
import           Data.Swagger.Internal.Schema    ( GToSchema )
import           Data.Swagger.Internal.TypeShape ( TypeHasSimpleShape )
import           GHC.Generics                    ( Rep )
import           Network.HTTP.Types.Header       ( hContentType )
import           Network.HTTP.Types.Status
import           Network.Wai
import           RFC.Prelude                     hiding ( (<>) )
import           RFC.Servant.Server
import           Servant.Swagger
import           Servant.Swagger.UI
import qualified Text.Blaze.Html.Renderer.String as Blaze
import qualified Text.Markdown                   as MD

apiToHtml :: (HasDocs a) => Proxy a -> Html
apiToHtml = preEscapedToHtml . (MD.markdown mdSettings) . toLazyText . markdown . docs
  where
    mdSettings = def
      { MD.msLinkNewTab = False
      , MD.msAddHeadingId = True
      }

apiToAscii :: HasDocs a => Proxy a -> String
apiToAscii = markdown . docs

apiToSwagger :: (HasSwagger a) => Proxy a -> Swagger
apiToSwagger = toSwagger

apiMiddleware :: (HasDocs a, HasSwagger a) => Proxy a -> Swagger -> Application -> Application
apiMiddleware api addlSwagger application request callback =
  case (reqMethod, reqPath) of
    ("GET", Just doIt) -> doIt
    _                  -> application request callback
  where
    bsToStr :: StrictByteString -> String
    bsToStr = fromMaybe "" . fromUTF8
    html = Blaze.renderHtml $ apiToHtml api
    ascii = apiToAscii api
    swaggerToLbs = Builder.toLazyByteString . fromEncoding . toEncoding
    swagger = swaggerToLbs $ apiToSwagger api <> addlSwagger
    reqMethod = fmap charToUpper . bsToStr $ requestMethod request
    pathInfo = fmap charToLower . bsToStr $ rawPathInfo request
    reqPath =
      case pathInfo of
        "swagger.json"  -> Just serveSwagger
        "/swagger.json" -> Just serveSwagger
        "api.html"      -> Just serveHtml
        "/api.html"     -> Just serveHtml
        "api.txt"       -> Just serveTxt
        "/api.txt"      -> Just serveTxt
        _               -> Nothing
    response contentType body = callback $
      responseLBS status200 [(hContentType, contentType)] body
    serveHtml = response (toUTF8 "text/html") (toUTF8 html)
    serveTxt = response (toUTF8 "text/plain") (toUTF8 ascii)
    serveSwagger = response (toUTF8 "application/json") swagger


instance ToSample () where
  toSamples _ = [(toText "No value", ())]

swaggerSchemaOptions :: SchemaOptions
swaggerSchemaOptions = SchemaOptions
  { fieldLabelModifier = Aeson.fieldLabelModifier jsonOptions
  , constructorTagModifier = Aeson.constructorTagModifier jsonOptions
  , unwrapUnaryRecords = Aeson.unwrapUnaryRecords jsonOptions
  , datatypeNameModifier = id
  , allNullaryToStringTag = True
  }

class ToSchemaRFC a where
  declareNamedSchemaRFC :: proxy a -> Declare (Definitions Schema) NamedSchema
  default declareNamedSchemaRFC ::
    (Generic a
    , GToSchema (Rep a)
    , TypeHasSimpleShape a "genericDeclareNamedSchemaUnrestricted"
    ) => proxy a -> Declare (Definitions Schema) NamedSchema
  declareNamedSchemaRFC = genericDeclareNamedSchema swaggerSchemaOptions

instance {-# OVERLAPPABLE #-} ToSchemaRFC a => ToSchema a where
  declareNamedSchema = declareNamedSchemaRFC

instance {-# OVERLAPPABLE #-} ToSchema (Ratio a) where
  declareNamedSchema _ = do
    integerSchema <- declareSchemaRef (Proxy :: Proxy Integer)
    return . NamedSchema (Just . toText $ "Ratio") $ mempty
      & type_ .~ SwaggerObject
      & properties .~ (fromList
        [ (toText "numerator", integerSchema)
        , (toText "denominator", integerSchema)
        ])
      & required .~ (toText <$> [ "numerator", "denominator" ])

instance {-# OVERLAPPING #-} ToSample Int16 where
    toSamples _ =  [] -- TODO Impl this