I am trying to write a Twitch chat bot that will receive messages. The messages will be in JSON and be an object of two other objects, metadata (which appears to be mostly standardized across all message types) and payload (which is vastly different from message type to message type).
What I am trying to achieve is one Message type with a Metadata type and an Existential Quantification of Payload types. The part that is tripping me up is getting it to parse the Payload types in a non-boilerplate fashion. I have a generic fromJSON function within a class (intentionally named different than parseJSON) with a default implementation that should work for all of the Payload types, but the compiler is getting hung up on the Existential Quantification type, because it is not an instance of the MessagePayload class, even though all Payload types (e.g. AccessToken and Session) are.
Is there a way to resolve the Payload's type generically within Message's parseJSON function without writing a case statement that I would have to constantly update as I make more of these Payload types throughout the course of this project?
Here is the code in question that I've got up to this point:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE OverloadedStrings #-}
module Twitch.Types ( AccessToken (..)
, Session (..)
, Message (..)
, MessagePayload (..)
, MessagePayloadType (..)
) where
import Data.Aeson hiding (fromJSON, Key)
import qualified Data.Aeson.Key as K
import Data.Aeson.Casing
import Data.Aeson.Types (Parser)
import Data.Text
import qualified Data.Text as T
import Data.Time
import GHC.Generics
import Internal.Types
import DB (storeToken)
import DB.Internal (runDB)
import DB.Model
data Message = Message { metadata :: Metadata
, payload :: MessagePayloadType
}
deriving (Generic)
instance FromJSON Message where
parseJSON = withObject "" $ \v -> do
metadata <- v .: "metadata"
payloadValue <- v .: "payload"
payload <- MessagePayloadType <$> fromJSON metadata payloadValue
return $ Message metadata payload
data Metadata = Metadata { messageID :: String
, messageType :: String
, messageTimestamp :: UTCTime
}
deriving (Generic, Show)
instance FromJSON Metadata where
parseJSON = genericParseJSON $ aesonPrefix snakeCase
-- TODO: Need to eventually expand so it can return any Key, but Token
-- is the only db table we have for now.
class MessagePayload a where
dbInsert :: App -> a -> IO (Key Token)
fromJSON :: Metadata -> Value -> Parser a
fromJSON m v = withObject "payload"
(\p -> do
obj <- p .: ( K.fromString
$ msgTypeToJSONObjectName
$ messageType m
)
genericParseJSON (aesonPrefix snakeCase) obj
)
v
data MessagePayloadType = forall mp . MessagePayload mp => MessagePayloadType mp
msgTypeToJSONObjectName :: String -> String
msgTypeToJSONObjectName s =
case s of
"session_welcome" -> "session"
_ -> error $ "Twitch Message Type " ++
s ++
" not found. Update Twitch.Types.messageTypeToJSONObjectName."
data Session = Session { sessionId :: Text
, status :: Text
, connectedAt :: UTCTime
, keepaliveTimeoutSeconds :: Int
, reconnectUrl :: Maybe Text
}
deriving (Generic, Show)
instance TokenData Session where
getToken = sessionId
getExpiresIn = keepaliveTimeoutSeconds
getTokenType _ = Nothing
instance MessagePayload Session where
dbInsert app s = runDB (storeToken Twitch Access s) app
data AccessToken = AT { atAccessToken :: Text
, atExpiresIn :: Int
, atTokenType :: Text
}
deriving (Generic, Show)
instance TokenData AccessToken where
getToken = atAccessToken
getExpiresIn = atExpiresIn
getTokenType = Just . atTokenType
instance MessagePayload AccessToken where
dbInsert app at = runDB (storeToken Twitch Access at) app