{-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE ScopedTypeVariables #-} -- | -- Module: JsonRpc -- Copyright: Copyright © 2021 Kadena LLC. -- License: MIT -- Maintainer: Lars Kuhtz -- Stability: experimental -- -- TODO -- module JsonRpc ( -- * JSON Utils Static(..) , pattern StaticNull , pattern StaticTrue , pattern StaticFalse , T1(..) -- JSON RPC Message Ids , MsgId(..) -- JSON RPC Error , Error(..) -- * JSON RPC Request Messages , requestProperties -- * JSON RPC Result Messages , responseProperties , parseResponse , parseResponse' ) where import qualified Data.Aeson as A import qualified Data.Aeson.Encoding.Internal as A import qualified Data.Aeson.Types as A (Pair, Parser) import Data.Foldable import qualified Data.Text as T -- internal modules -- -------------------------------------------------------------------------- -- -- JSON Utils data Static (a :: k) = Static deriving (Show, Eq, Ord) pattern StaticNull :: Static 'Nothing pattern StaticNull = Static pattern StaticTrue :: Static 'True pattern StaticTrue = Static pattern StaticFalse :: Static 'False pattern StaticFalse = Static instance A.ToJSON (Static 'Nothing) where toEncoding _ = A.null_ toJSON _ = A.Null {-# INLINE toEncoding #-} {-# INLINE toJSON #-} instance A.FromJSON (Static 'Nothing) where parseJSON A.Null = return Static parseJSON a = fail $ "expected \'null\' but got " <> show a {-# INLINE parseJSON #-} instance A.ToJSON (Static 'True) where toEncoding _ = A.toEncoding True toJSON _ = A.toJSON True {-# INLINE toEncoding #-} {-# INLINE toJSON #-} instance A.FromJSON (Static 'True) where parseJSON = A.withBool "True" $ \b -> if b then return Static else fail "expected constant \'true\' but got \'false\'" {-# INLINE parseJSON #-} instance A.ToJSON (Static 'False) where toEncoding _ = A.toEncoding False toJSON _ = A.toJSON False {-# INLINE toEncoding #-} {-# INLINE toJSON #-} instance A.FromJSON (Static 'False) where parseJSON = A.withBool "False" $ \b -> if b then fail "expected constant \'false\' but got \'true\'" else return Static {-# INLINE parseJSON #-} -- | Unary Tuple -- newtype T1 a = T1 { _getT1 :: a } deriving (Show, Eq, Ord) instance A.ToJSON a => A.ToJSON (T1 a) where toEncoding (T1 a) = A.toEncoding [a] toJSON (T1 a) = A.toJSON [a] {-# INLINE toEncoding #-} {-# INLINE toJSON #-} instance A.FromJSON a => A.FromJSON (T1 a) where parseJSON = A.withArray "T1" $ \x -> case toList x of [a] -> T1 <$> A.parseJSON a l -> fail $ "Expected array of length 1, got length " <> show (length l) {-# INLINE parseJSON #-} -- -------------------------------------------------------------------------- -- -- | JSON RPC Message Id -- -- This is used to match result message to requests within a JSON RPC session. -- -- It usually is an integral number, the can be encoded either as a string or -- a number. -- newtype MsgId = MsgId A.Value deriving (Show, Eq, Ord) deriving newtype (A.ToJSON, A.FromJSON) -- -------------------------------------------------------------------------- -- -- JSON RPC Error newtype Error = Error (Int, T.Text, A.Value) deriving (Show, Eq, Ord) deriving newtype (A.ToJSON, A.FromJSON) -- -------------------------------------------------------------------------- -- -- | JSON RPC Request Messages -- -- A remote method is invoked by sending a request to a remote service. The -- request is a single object serialized using JSON. -- -- A notification is a special request which does not have a response. The -- notification is a single object serialized using JSON. -- -- * method - A String containing the name of the method to be invoked. -- -- * params - An Array of objects to pass as arguments to the method. -- -- * id - The request id. This can be of any type. It is used to match the -- response with the request that it is replying to. -- -- This is @null@ when the request if a notification. In that case no response -- is expected. -- -- In this implementation we limit the value to be an integral number of @null@. -- requestProperties :: A.KeyValue kv => A.ToJSON a => T.Text -> a -> Maybe MsgId -> [kv] requestProperties method params i = [ "method" A..= method , "params" A..= params , "id" A..= i ] {-# INLINE requestProperties #-} {-# SPECIALIZE requestProperties :: A.ToJSON a => T.Text -> a -> Maybe MsgId -> [A.Series] #-} {-# SPECIALIZE requestProperties :: A.ToJSON a => T.Text -> a -> Maybe MsgId -> [A.Pair] #-} -- -------------------------------------------------------------------------- -- -- | JSON RPC Result Messages -- -- When the method invocation completes, the service must reply with a response. -- The response is a single object serialized using JSON. -- -- It has three properties: -- -- * result - The Object that was returned by the invoked method. This must be -- null in case there was an error invoking the method. -- -- It seems that for Stratum this actully is an array -- * error - An Error object if there was an error invoking the method. It must be -- null if there was no error. -- * id - This must be the same id as the request it is responding to. -- -- In this implementation we limit the value to be an integral number or @null@. -- responseProperties :: A.KeyValue kv => A.ToJSON a => A.ToJSON b => MsgId -> Either a b -> [kv] responseProperties i r = [ "result" A..= either (const Nothing) Just r , "error" A..= either Just (const Nothing) r , "id" A..= i ] {-# INLINE responseProperties #-} {-# SPECIALIZE responseProperties :: A.ToJSON a => A.ToJSON b => MsgId -> Either a b -> [A.Series] #-} {-# SPECIALIZE responseProperties :: A.ToJSON a => A.ToJSON b => MsgId -> Either a b -> [A.Pair] #-} parseResponse :: A.FromJSON a => A.FromJSON b => A.Object -> A.Parser (Either a b) parseResponse o = o A..: "error" >>= \case Nothing -> o A..: "result" Just e -> return $ Left e {-# INLINE parseResponse #-} parseResponse' :: A.FromJSON a => (A.Value -> A.Parser b) -> A.Object -> A.Parser (Either a b) parseResponse' paramsParser o = o A..: "error" >>= \case Nothing -> Right <$> (o A..: "result" >>= paramsParser) Just e -> return $ Left e {-# INLINE parseResponse' #-}