{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Morpheus.Types.Internal.Subscription.Apollo
( ApolloAction(..)
, apolloFormat
, acceptApolloRequest
, toApolloResponse
, Validation
) where
import Data.Maybe ( maybe )
import Control.Monad.IO.Class ( MonadIO(..) )
import Data.Aeson ( FromJSON (..)
, ToJSON (..)
, Value (..)
, eitherDecode
, encode
, pairs
, withObject
, (.:)
, (.:?)
, (.=)
)
import Data.ByteString.Lazy.Char8 ( ByteString
, pack
)
import Data.Semigroup ( (<>) )
import Data.Text ( Text
, unpack
)
import GHC.Generics ( Generic )
import Network.WebSockets ( AcceptRequest (..)
, RequestHead
, getRequestSubprotocols
, PendingConnection
, Connection
, acceptRequestWith
, pendingRequest
)
import Data.Morpheus.Types.IO ( GQLResponse
, GQLRequest (..)
)
type ID = Text
data ApolloSubscription payload =
ApolloSubscription
{ apolloId :: Maybe ID
, apolloType :: Text
, apolloPayload :: Maybe payload
}
deriving (Show, Generic)
instance FromJSON a => FromJSON (ApolloSubscription a) where
parseJSON = withObject "ApolloSubscription" objectParser
where
objectParser o =
ApolloSubscription <$> o .:? "id" <*> o .: "type" <*> o .:? "payload"
data RequestPayload =
RequestPayload
{ payloadOperationName :: Maybe Text
, payloadQuery :: Maybe Text
, payloadVariables :: Maybe Value
}
deriving (Show, Generic)
instance FromJSON RequestPayload where
parseJSON = withObject "ApolloPayload" objectParser
where
objectParser o =
RequestPayload <$> o .:? "operationName" <*> o .:? "query" <*>
o .:? "variables"
instance ToJSON a => ToJSON (ApolloSubscription a) where
toEncoding (ApolloSubscription id' type' payload') =
pairs $ "id" .= id' <> "type" .= type' <> "payload" .= payload'
acceptApolloRequest
:: MonadIO m
=> PendingConnection
-> m Connection
acceptApolloRequest pending
= liftIO
$ acceptRequestWith
pending
(acceptApolloSubProtocol (pendingRequest pending))
acceptApolloSubProtocol :: RequestHead -> AcceptRequest
acceptApolloSubProtocol reqHead =
apolloProtocol (getRequestSubprotocols reqHead)
where
apolloProtocol ["graphql-subscriptions"] =
AcceptRequest (Just "graphql-subscriptions") []
apolloProtocol ["graphql-ws"] = AcceptRequest (Just "graphql-ws") []
apolloProtocol _ = AcceptRequest Nothing []
toApolloResponse :: ID -> GQLResponse -> ByteString
toApolloResponse sid val =
encode $ ApolloSubscription (Just sid) "data" (Just val)
data ApolloAction
= SessionStop ID
| SessionStart ID GQLRequest
| ConnectionInit
type Validation = Either ByteString
apolloFormat :: ByteString -> Validation ApolloAction
apolloFormat = validateReq . eitherDecode
where
validateReq :: Either String (ApolloSubscription RequestPayload) -> Validation ApolloAction
validateReq = either (Left . pack) validateSub
validateSub :: ApolloSubscription RequestPayload -> Validation ApolloAction
validateSub ApolloSubscription { apolloType = "connection_init" }
= pure ConnectionInit
validateSub ApolloSubscription { apolloType = "start", apolloId , apolloPayload }
= do
sessionId <- validateSession apolloId
payload <- validatePayload apolloPayload
pure $ SessionStart sessionId payload
validateSub ApolloSubscription { apolloType = "stop", apolloId }
= SessionStop <$> validateSession apolloId
validateSub ApolloSubscription { apolloType }
= Left $ "Unknown Request type \""<> pack (unpack apolloType) <> "\"."
validateSession :: Maybe ID -> Validation ID
validateSession = maybe (Left "\"id\" was not provided") Right
validatePayload = maybe (Left "\"payload\" was not provided") validatePayloadContent
validatePayloadContent RequestPayload
{ payloadQuery
, payloadOperationName = operationName
, payloadVariables = variables
} = do
query <- maybe (Left "\"payload.query\" was not provided") Right payloadQuery
pure $ GQLRequest {query, operationName, variables}