grpc-spec-1.0.0: src/Network/GRPC/Spec/RPC.hs
{-# LANGUAGE OverloadedStrings #-}
module Network.GRPC.Spec.RPC (
IsRPC(..)
, Input
, Output
, SupportsServerRpc(..)
, SupportsClientRpc(..)
, defaultRpcContentType
) where
import Control.DeepSeq (NFData)
import Data.ByteString qualified as Strict (ByteString)
import Data.ByteString.Lazy qualified as Lazy
import Data.Kind
import Data.Typeable
import GHC.Stack
import Network.GRPC.Spec.CustomMetadata.Typed
{-------------------------------------------------------------------------------
RPC call
-------------------------------------------------------------------------------}
-- | Messages from the client to the server
type family Input (rpc :: k) :: Type
-- | Messages from the server to the client
type family Output (rpc :: k) :: Type
-- | Abstract definition of an RPC
--
-- Note on encoding: the gRPC specification does not say anything about text
-- encoding issues for paths (service names and method names) or message types.
-- The Protobuf compiler (by far the most common instantation of gRPC) does not
-- allow for non-ASCII character at all ("interpreting non ascii codepoint").
-- We therefore punt on the encoding issue here, and use bytestrings. /If/
-- applications want to use non-ASCII characters, they can choose their own
-- encoding.
class ( -- Serialization
--
-- We force messages to NF before enqueueing them. This ensures that
-- if those messages contain any pure exceptions (due to a bug in a
-- client or a server), we detect the problem when the message is
-- enqueued, and can throw an appropriate exception.
NFData (Input rpc)
, NFData (Output rpc)
-- Debug constraints
--
-- For debugging it is useful when we have 'Show' instances in scope.
-- This is not that strong a requirement; after all, we must be able
-- to serialize inputs and deserialize outputs, so they must also be
-- 'Show'able.
, Show (Input rpc)
, Show (Output rpc)
, Show (RequestMetadata rpc)
, Show (ResponseInitialMetadata rpc)
, Show (ResponseTrailingMetadata rpc)
) => IsRPC (rpc :: k) where
-- | Content-type
--
-- gRPC is agnostic to the message format; the spec defines the @Content-Type@
-- header as
--
-- > Content-Type →
-- > "content-type"
-- > "application/grpc"
-- > [("+proto" / "+json" / {custom})]
--
-- 'defaultRpcContentType' can be used in the case that the format (such as
-- @proto@) is known.
--
-- Note on terminology: throughout this codebase we avoid the terms "encoding"
-- and "decoding", which can be ambiguous. Instead we use
-- \"serialize\"\/\"deserialize\" and \"compress\"\/\"decompress\".
rpcContentType :: Proxy rpc -> Strict.ByteString
-- | Service name
--
-- For Protobuf, this is the fully qualified service name.
rpcServiceName :: HasCallStack => Proxy rpc -> Strict.ByteString
-- | Method name
--
-- For Protobuf, this is /just/ the method name (no qualifier required).
rpcMethodName :: HasCallStack => Proxy rpc -> Strict.ByteString
-- | Message type, if specified
--
-- This is used to set the (optional) @grpc-message-type@ header.
-- For Protobuf, this is the fully qualified message type.
rpcMessageType :: HasCallStack => Proxy rpc -> Maybe Strict.ByteString
-- | Default content type string
--
-- This is equal to @"application/grpc+format@ for some @format@ such as
-- @proto@ or @json@. See also 'rpcContentType'.
defaultRpcContentType :: Strict.ByteString -> Strict.ByteString
defaultRpcContentType format = "application/grpc+" <> format
-- | Client-side RPC
class ( IsRPC rpc
-- Serialization
, BuildMetadata (RequestMetadata rpc)
, ParseMetadata (ResponseInitialMetadata rpc)
, ParseMetadata (ResponseTrailingMetadata rpc)
) => SupportsClientRpc rpc where
-- | Serialize RPC input
--
-- We don't ask for a builder here, but instead ask for the complete
-- serialized form. gRPC insists that individual messages are length prefixed,
-- so we /must/ compute the full serialization in memory before we can send
-- anything.
--
-- We use the terms \"serialize\" and \"deserialize\" here, and
-- \"compress\"/\"decompress\" for compression, rather than
-- \"encode\"/\"decode\", which could refer to either process.
rpcSerializeInput :: Proxy rpc -> Input rpc -> Lazy.ByteString
-- | Deserialize RPC output
--
-- Discussion of 'rpcDeserializeInput' applies here, also.
rpcDeserializeOutput ::
Proxy rpc
-> Lazy.ByteString
-> Either String (Output rpc)
-- | Server-side RPC
class ( IsRPC rpc
-- Serialization
, ParseMetadata (RequestMetadata rpc)
, BuildMetadata (ResponseInitialMetadata rpc)
, StaticMetadata (ResponseTrailingMetadata rpc)
) => SupportsServerRpc rpc where
-- | Deserialize RPC input
--
-- This function does not have to deal with compression or length prefixes,
-- and can assume fully consume the given bytestring (if there are unconsumed
-- bytes, this should be considered a parse failure).
rpcDeserializeInput ::
Proxy rpc
-> Lazy.ByteString
-> Either String (Input rpc)
-- | Serialize RPC output
rpcSerializeOutput :: Proxy rpc -> Output rpc -> Lazy.ByteString