solga-swagger-0.1.0.0: src/Solga/Swagger.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DataKinds #-}
module Solga.Swagger
( genSwagger
, RouterSwagger(..)
-- * Implementation
, GenPathsM
, Paths
, Context(..)
, passPaths
, noPaths
) where
import Control.Monad.State
import Control.Monad.Except
import qualified Network.HTTP.Types as HTTP
import Control.Lens hiding (Context)
import qualified Data.ByteString.Char8 as BSC
import qualified Data.DList as DL
import qualified Data.HashMap.Strict as HMS
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding
import Data.Typeable
import GHC.Generics
import GHC.TypeLits
import Data.Swagger as Swagger
import Data.Swagger.Declare
import Solga
data Context = Context
{ contextMethod :: Maybe HTTP.Method -- ^ Any method currently set.
, pathSegments :: DL.DList Text -- ^ The current path.
, operationContext :: Operation -- ^ The current template operation.
, paramScope :: HMS.HashMap Text Int -- ^ The parameter names in use.
} deriving (Show)
noContext :: Context
noContext = Context
{ contextMethod = mempty
, pathSegments = mempty
, operationContext = mempty
, paramScope = mempty
}
type GenPathsM = ExceptT ( Text, Context ) (Declare (Definitions Schema))
type Paths = HMS.HashMap Text PathItem
-- | A type for which we can generate a Swagger specification.
class RouterSwagger r where
genPathsFor :: Proxy r -> Context -> GenPathsM Paths
default genPathsFor :: (Generic r, RouterSwagger (Rep r ())) => Proxy r -> Context -> GenPathsM Paths
genPathsFor _ = genPathsFor (Proxy :: Proxy (Rep r ()))
-- | For a Router @f next@, produce the same paths as @next@ without modification.
passPaths :: (r ~ f next, RouterSwagger next) => Proxy r -> Context -> GenPathsM Paths
passPaths p = genPathsFor (nextProxy p)
-- | Produce no paths.
noPaths :: Proxy r -> Context -> GenPathsM Paths
noPaths _ _ = return mempty
-- | Generate a Swagger specification for a given type.
genSwagger :: RouterSwagger r => Proxy r -> Either ( Text, Context ) Swagger
genSwagger p = case runDeclare (runExceptT (genPathsFor p noContext)) mempty of
( _, Left err ) -> Left err
( defs, Right ps ) ->
let
fpPaths = HMS.fromList $ map (\(k, v) -> ( T.unpack k, v )) $ HMS.toList ps
in Right (mempty & paths .~ fpPaths & definitions .~ defs)
nextProxy :: Proxy (r next) -> Proxy next
nextProxy _ = Proxy
pathsFromContext :: Response -> Context -> GenPathsM Paths
pathsFromContext response ctx@Context { contextMethod, pathSegments, operationContext } = do
let path = foldMap (\seg -> "/" <> seg) (DL.toList pathSegments)
methodSetter <- case contextMethod of
Just m -> case m of
"GET" -> return Swagger.get
"PUT" -> return Swagger.put
"POST" -> return Swagger.post
"DELETE" -> return Swagger.delete
"OPTIONS" -> return Swagger.options
"HEAD" -> return Swagger.head_
"PATCH" -> return Swagger.patch
_ -> throwError ( "Unsupported method " <> decodeUtf8 m, ctx )
_ -> throwError ( "Missing method in context.", ctx )
let resps = mempty & responses .~ HMS.singleton 200 (Inline response)
let operation = operationContext & responses .~ resps
let pathItem = mempty & methodSetter ?~ operation
return $ HMS.singleton path pathItem
instance RouterSwagger RawResponse where
genPathsFor _ = pathsFromContext mempty
instance ToSchema a => RouterSwagger (JSON a) where
genPathsFor p ctx = do
respSchemaRef <- lift $ declareSchemaRef (nextProxy p)
let resp = mempty & schema ?~ respSchemaRef
pathsFromContext resp ctx
instance (KnownSymbol m, RouterSwagger next) => RouterSwagger (Method m next) where
genPathsFor p ctx = case ctx of
Context { contextMethod = Just ctxMeth } | ctxMeth /= method -> throwError ( "Conflicting method specification.", ctx )
_ -> genPathsFor (nextProxy p) ctx { contextMethod = Just method }
where
method = BSC.pack (symbolVal (Proxy :: Proxy m))
instance (KnownSymbol seg, RouterSwagger next) => RouterSwagger (Seg seg next) where
genPathsFor p ctx = do
let seg = T.pack $ symbolVal (Proxy :: Proxy seg)
genPathsFor (nextProxy p) ctx { pathSegments = pathSegments ctx `DL.snoc` seg }
instance RouterSwagger next => RouterSwagger (WithIO next) where
genPathsFor = passPaths
instance RouterSwagger next => RouterSwagger (End next) where
genPathsFor = passPaths
instance RouterSwagger next => RouterSwagger (NoCache next) where
genPathsFor = passPaths
instance RouterSwagger next => RouterSwagger (ExtraHeaders next) where
genPathsFor = passPaths
instance RouterSwagger (ReqBodyMultipart a next) where
genPathsFor = noPaths
instance RouterSwagger next => RouterSwagger (OneOfSegs '[] next) where
genPathsFor = noPaths
instance (KnownSymbol seg, RouterSwagger next, RouterSwagger (OneOfSegs segs next)) => RouterSwagger (OneOfSegs (seg ': segs) next) where
genPathsFor p ctx = do
let seg = T.pack $ symbolVal (Proxy :: Proxy seg)
nextPaths <- genPathsFor (nextProxy p) ctx { pathSegments = pathSegments ctx `DL.snoc` seg }
nextSegPaths <- genPathsFor (Proxy :: Proxy (OneOfSegs segs next)) ctx
return (nextPaths `HMS.union` nextSegPaths)
instance RouterSwagger Raw where
genPathsFor = noPaths
instance (RouterSwagger left, RouterSwagger right) => RouterSwagger (left :<|> right) where
genPathsFor _ ctx =
HMS.unionWith mappend
<$> genPathsFor (Proxy :: Proxy left) ctx
<*> genPathsFor (Proxy :: Proxy right) ctx
-- Generic paths
instance RouterSwagger r => RouterSwagger (K1 i r p) where
genPathsFor _ = genPathsFor (Proxy :: Proxy r)
instance RouterSwagger (f p) => RouterSwagger (M1 i c f p) where
genPathsFor _ = genPathsFor (Proxy :: Proxy (f p))
instance (RouterSwagger (left p), RouterSwagger (right p)) => RouterSwagger ((left :*: right) p) where
genPathsFor _ ctx =
HMS.unionWith mappend
<$> genPathsFor (Proxy :: Proxy (left p)) ctx
<*> genPathsFor (Proxy :: Proxy (right p)) ctx
instance (ToSchema a, RouterSwagger next) => RouterSwagger (ReqBodyJSON a next) where
genPathsFor p ctx@Context { operationContext } = do
let hasOtherBody = notNullOf (parameters . folded . _Inline . schema . _ParamBody) operationContext
if hasOtherBody
then throwError ( "Conflicting request body schemas.", ctx )
else do
bodySchemaRef <- lift $ declareSchemaRef (Proxy :: Proxy a)
let param = mempty & name .~ "requestBody" & required .~ Just True & schema .~ (ParamBody bodySchemaRef)
genPathsFor (nextProxy p) ctx { operationContext = operationContext & parameters <>~ [ Inline param ] }
newName :: Text -> Context -> ( Text, Context )
newName desiredName ctx@Context { paramScope } = case HMS.lookup desiredName paramScope of
Nothing -> ( desiredName, ctx { paramScope = HMS.insert desiredName 1 paramScope } )
Just count -> let newCount = count + 1 in ( desiredName <> T.pack (show newCount), ctx { paramScope = HMS.insert desiredName newCount paramScope } )
instance (Typeable a, ToParamSchema a, RouterSwagger next) => RouterSwagger (Capture a next) where
genPathsFor p ctx = do
let desiredName = T.pack $ tyConName $ typeRepTyCon $ typeRep (Proxy :: Proxy a)
let ( paramName, newCtx ) = newName desiredName ctx
let pSchema = toParamSchema (Proxy :: Proxy a)
let pOtherSchema = mempty & in_ .~ ParamPath & paramSchema .~ pSchema
let param = mempty & name .~ paramName & required .~ Just True & schema .~ ParamOther pOtherSchema
genPathsFor (nextProxy p) newCtx
{ pathSegments = pathSegments ctx `DL.snoc` paramName
, operationContext = operationContext newCtx & parameters <>~ [ Inline param ]
}