{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveGeneric #-} -- | Implementation of basic JSON-RPC data types. module Network.JsonRpc.Data ( -- * Requests Request(..) -- ** Parsing , FromRequest(..) , parseRequest -- ** Encoding , ToRequest(..) , buildRequest -- * Responses , Response(..) -- ** Parsing , FromResponse(..) , parseResponse -- * Notifications , Notif(..) -- ** Parsing , FromNotif(..) , parseNotif -- ** Encoding , ToNotif(..) , buildNotif -- * Errors , ErrorObj(..) -- ** Error Messages , errorParse , errorInvalid , errorParams , errorMethod , errorId -- * Others , Message(..) , Method , Id(..) , Ver(..) ) where import Control.Applicative ((<$>), (<*>), (<|>)) import Control.DeepSeq (NFData, rnf) import Control.Monad (when, guard, mzero) import Data.Aeson.Types import Data.Hashable (Hashable) import Data.Text (Text) import GHC.Generics (Generic) -- -- Requests -- -- Request data type. data Request q = Request { getReqVer :: !Ver -- Version , getReqMethod :: !Method -- Method , getReqParams :: !q -- Params , getReqId :: !Id -- Id } deriving (Eq, Show, Read) instance NFData q => NFData (Request q) where rnf (Request v m q i) = rnf v `seq` rnf m `seq` rnf q `seq` rnf i instance ToJSON q => ToJSON (Request q) where toJSON (Request V2 m p i) = object $ case toJSON p of Null -> [jr2, "method" .= m, "id" .= i] v -> [jr2, "method" .= m, "id" .= i, "params" .= v] toJSON (Request V1 m p i) = object $ case toJSON p of Null -> ["method" .= m, "params" .= emptyArray, "id" .= i] v -> ["method" .= m, "params" .= v, "id" .= i] -- | Class for data that can be received in JSON-RPC requests. class FromRequest q where -- | Parser for params field. paramsParser :: Method -> Maybe (Value -> Parser q) instance FromRequest Value where paramsParser _ = Just return instance FromRequest () where paramsParser _ = Nothing -- | Parse JSON-RPC request. parseRequest :: FromRequest q => Value -> Parser (Either ErrorObj (Request q)) parseRequest = withObject "request" $ \o -> do j <- o .:? "jsonrpc" i <- o .: "id" when (i == IdNull) $ fail "Request must have non-null id" m <- o .: "method" p <- o .:? "params" .!= Null let ver = if j == Just ("2.0" :: Text) then V2 else V1 case paramsParser m of Nothing -> return (Left (errorMethod ver m i)) Just x -> parseIt ver m i x p <|> return (Left (errorParams ver p i)) where parseIt ver m i x p = (\y -> Right (Request ver m y i)) <$> x p -- | Class for data that can be sent as JSON-RPC requests. -- Define a method name for each request. class ToRequest q where requestMethod :: q -> Method instance ToRequest Value where requestMethod _ = "" instance ToRequest () where requestMethod _ = undefined -- Build JSON-RPC request. buildRequest :: ToRequest q => Ver -- ^ JSON-RPC version -> q -- ^ Request data -> Request q buildRequest ver q = Request ver (requestMethod q) q IdNull -- -- Responses -- -- | JSON-RPC response data type data Response r = Response { getResVer :: !Ver -- ^ Version , getResult :: !r -- ^ Result , getResId :: !Id -- ^ Id } deriving (Eq, Show, Read) instance NFData r => NFData (Response r) where rnf (Response v r i) = rnf v `seq` rnf r `seq` rnf i instance ToJSON r => ToJSON (Response r) where toJSON (Response V1 r i) = object ["id" .= i, "result" .= r, "error" .= Null] toJSON (Response V2 r i) = object [jr2, "id" .= i, "result" .= r] -- | Class for data that can be received inside JSON-RPC responses. class FromResponse r where -- | Parse result field from JSON-RPC response. parseResult :: Method -> Value -> Parser r instance FromResponse Value where parseResult _ = return instance FromResponse () where parseResult _ _ = return () -- | Parse JSON-RPC response. parseResponse :: FromResponse r => Request q -> Value -> Parser (Either ErrorObj (Response r)) parseResponse rq = withObject "response" $ \o -> do let m = getReqMethod rq qi = getReqId rq j <- o .:? "jsonrpc" i <- o .: "id" when (i == IdNull) $ fail "Response must have non-null id" when (qi /= i) $ fail "Response id mismatch" let ver = if j == Just ("2.0" :: Text) then V2 else V1 (Right <$> parseRes ver i m o) <|> (Left <$> parseJSON (Object o)) where parseRes ver i m o = do v <- o .: "result" guard $ v /= Null r <- parseResult m v return $ Response ver r i -- -- Notifications -- -- | Class for JSON-RPC notifications. data Notif n = Notif { getNotifVer :: !Ver -- ^ Version , getNotifMethod :: !Method -- ^ Method , getNotifParams :: !n -- ^ Params } deriving (Eq, Show, Read) instance NFData n => NFData (Notif n) where rnf (Notif v m n) = rnf v `seq` rnf m `seq` rnf n instance ToJSON n => ToJSON (Notif n) where toJSON (Notif V2 m p) = object $ case toJSON p of Null -> [jr2, "method" .= m] v -> [jr2, "method" .= m, "params" .= v] toJSON (Notif V1 m p) = object $ case toJSON p of Null -> ["method" .= m, "params" .= emptyArray, "id" .= Null] v -> ["method" .= m, "params" .= v, "id" .= Null] -- | Class for data that can be received in JSON-RPC notifications. class FromNotif n where -- | Parser for notification params field notifParamsParser :: Method -> Maybe (Value -> Parser n) instance FromNotif Value where notifParamsParser _ = Just return instance FromNotif () where notifParamsParser _ = Nothing -- | Parse notifications. parseNotif :: FromNotif n => Value -> Parser (Either ErrorObj (Notif n)) parseNotif = withObject "notification" $ \o -> do j <- o .:? "jsonrpc" i <- o .:? "id" .!= IdNull m <- o .: "method" p <- o .:? "params" .!= Null guard $ i == IdNull let ver = if j == Just ("2.0" :: Text) then V2 else V1 case notifParamsParser m of Nothing -> return (Left $ errorMethod ver m IdNull) Just x -> f ver m x p <|> return (Left (errorParams ver p i)) where f ver m x p = (Right . Notif ver m) <$> x p class ToNotif n where notifMethod :: n -> Method instance ToNotif Value where notifMethod _ = "" instance ToNotif () where notifMethod _ = undefined -- | Build notifications. buildNotif :: ToNotif n => Ver -- ^ Version -> n -- ^ Notification data -> Notif n buildNotif ver n = Notif ver (notifMethod n) n -- -- Errors -- -- | JSON-RPC errors. data ErrorObj = ErrorObj { getErrVer :: !Ver -- ^ Version , getErrMsg :: !String -- ^ Message , getErrCode :: !Int -- ^ Error code (2.0) , getErrData :: !Value -- ^ Error data (2.0) , getErrId :: !Id -- ^ Error id } deriving (Eq, Show) instance NFData ErrorObj where rnf (ErrorObj v m c d i) = rnf v `seq` rnf m `seq` rnf c `seq` rnf d `seq` rnf i instance FromJSON ErrorObj where parseJSON = withObject "error" $ \o -> do i <- o .:? "id" .!= IdNull j <- o .:? "jsonrpc" let ver = if j == Just ("2.0" :: Text) then V2 else V1 case ver of V2 -> o .: "error" >>= \e -> case e of (Object b) -> ErrorObj V2 <$> b .: "message" <*> b .: "code" <*> b .:? "data" .!= Null <*> return i _ -> fail "JSON-RPC 2.0 error must be a JSON object" V1 -> (o .: "error" >>= \e -> return $ ErrorObj V1 e 0 Null i) <|> (o .: "result" >>= \e -> return $ ErrorObj V1 e 0 Null i) -- Buggy servers sometimes put errors in result field instance ToJSON ErrorObj where toJSON (ErrorObj V2 m c d i) = object [jr2, "id" .= i, "error" .= o] where o = case d of Null -> object ["code" .= c, "message" .= m] _ -> object ["code" .= c, "message" .= m, "data" .= d] toJSON (ErrorObj V1 m _ _ i) = object ["id" .= i, "error" .= m, "result" .= Null] -- | Parse error. errorParse :: Ver -> Value -> ErrorObj errorParse ver v = ErrorObj ver "Parse error" (-32700) v IdNull -- | Invalid request. errorInvalid :: Ver -> Value -> ErrorObj errorInvalid ver v = ErrorObj ver "Invalid request" (-32600) v IdNull -- | Invalid params. errorParams :: Ver -> Value -> Id -> ErrorObj errorParams ver v i = ErrorObj ver "Invalid params" (-32602) v i -- | Method not found. errorMethod :: Ver -> Method -> Id -> ErrorObj errorMethod ver m i = ErrorObj ver "Method not found" (-32601) (toJSON m) i -- | Id not recognized. errorId :: Ver -> Id -> ErrorObj errorId ver i = ErrorObj ver "Id not recognized" (-32000) (toJSON i) IdNull -- -- Messages -- -- | Class for any JSON-RPC message. data Message q n r = MsgRequest { getMsgRequest :: !(Request q) } | MsgNotif { getMsgNotif :: !(Notif n) } | MsgResponse { getMsgResponse :: !(Response r) } | MsgError { getMsgError :: !ErrorObj } deriving (Eq, Show) instance (NFData q, NFData n, NFData r) => NFData (Message q n r) where rnf (MsgRequest q) = rnf q rnf (MsgNotif n) = rnf n rnf (MsgResponse r) = rnf r rnf (MsgError e) = rnf e instance (ToJSON q, ToJSON n, ToJSON r) => ToJSON (Message q n r) where toJSON (MsgRequest rq) = toJSON rq toJSON (MsgNotif rn) = toJSON rn toJSON (MsgResponse rs) = toJSON rs toJSON (MsgError e) = toJSON e -- -- Types -- -- | JSON-RPC methods in requests and notifications. type Method = Text -- | JSON-RPC message id. data Id = IdInt { getIdInt :: !Int } | IdTxt { getIdTxt :: !Text } | IdNull deriving (Eq, Show, Read, Generic) instance Hashable Id instance NFData Id where rnf (IdInt i) = rnf i rnf (IdTxt t) = rnf t rnf _ = () instance Enum Id where toEnum i = IdInt i fromEnum (IdInt i) = i fromEnum _ = error $ "Can't enumerate non-integral ids" instance FromJSON Id where parseJSON s@(String _) = IdTxt <$> parseJSON s parseJSON n@(Number _) = IdInt <$> parseJSON n parseJSON Null = return $ IdNull parseJSON _ = mzero instance ToJSON Id where toJSON (IdTxt s) = toJSON s toJSON (IdInt n) = toJSON n toJSON IdNull = Null -- | JSON-RPC version data Ver = V1 -- ^ JSON-RPC 1.0 | V2 -- ^ JSON-RPC 2.0 deriving (Eq, Show, Read) instance NFData Ver -- -- Helpers -- jr2 :: Pair jr2 = "jsonrpc" .= ("2.0" :: Text)