grpc-spec-1.0.0: src/Network/GRPC/Spec/RPC/Protobuf.hs
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE OverloadedStrings #-}
-- | gRPC with Protobuf
module Network.GRPC.Spec.RPC.Protobuf (
Protobuf
, Proto(..)
, getProto
) where
import Control.DeepSeq (NFData)
import Control.Lens hiding (lens)
import Data.ByteString qualified as Strict (ByteString)
import Data.ByteString.Char8 qualified as BS.Char8
import Data.Int
import Data.Kind
import Data.Map (Map)
import Data.ProtoLens
import Data.ProtoLens.Field qualified as ProtoLens
import Data.ProtoLens.Service.Types
import Data.Proxy
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Vector qualified as Boxed (Vector)
import Data.Vector.Unboxed qualified as Unboxed (Vector)
import Data.Word
import GHC.Exts (Proxy#, proxy#)
import GHC.Records qualified as GHC
import GHC.Records.Compat qualified as GHC.Compat
import GHC.TypeLits
import Network.GRPC.Spec.CustomMetadata.Typed
import Network.GRPC.Spec.RPC
import Network.GRPC.Spec.RPC.StreamType
import Network.GRPC.Spec.Util.Protobuf qualified as Protobuf
{-------------------------------------------------------------------------------
The spec defines the following in Appendix A, "GRPC for Protobuf":
> Service-Name → ?( {proto package name} "." ) {service name}
> Message-Type → {fully qualified proto message name}
> Content-Type → "application/grpc+proto"
-------------------------------------------------------------------------------}
-- | Protobuf RPC
--
-- This exists only as a type-level marker
data Protobuf (serv :: Type) (meth :: Symbol)
type instance Input (Protobuf serv meth) = Proto (MethodInput serv meth)
type instance Output (Protobuf serv meth) = Proto (MethodOutput serv meth)
instance ( HasMethodImpl serv meth
-- Debugging
, Show (MethodInput serv meth)
, Show (MethodOutput serv meth)
-- Serialization
, NFData (MethodInput serv meth)
, NFData (MethodOutput serv meth)
-- Metadata constraints
, Show (RequestMetadata (Protobuf serv meth))
, Show (ResponseInitialMetadata (Protobuf serv meth))
, Show (ResponseTrailingMetadata (Protobuf serv meth))
) => IsRPC (Protobuf serv meth) where
rpcContentType _ = defaultRpcContentType "proto"
rpcServiceName _ = BS.Char8.pack $ concat [
symbolVal $ Proxy @(ServicePackage serv)
, "."
, symbolVal $ Proxy @(ServiceName serv)
]
rpcMethodName _ = BS.Char8.pack . symbolVal $
Proxy @(MethodName serv meth)
rpcMessageType _ = Just . BS.Char8.pack . Text.unpack . messageName $
Proxy @(MethodInput serv meth)
instance ( IsRPC (Protobuf serv meth)
, HasMethodImpl serv meth
-- Metadata constraints
, BuildMetadata (RequestMetadata (Protobuf serv meth))
, ParseMetadata (ResponseInitialMetadata (Protobuf serv meth))
, ParseMetadata (ResponseTrailingMetadata (Protobuf serv meth))
) => SupportsClientRpc (Protobuf serv meth) where
rpcSerializeInput _ = Protobuf.buildLazy
rpcDeserializeOutput _ = Protobuf.parseLazy
instance ( IsRPC (Protobuf serv meth)
, HasMethodImpl serv meth
-- Metadata constraints
, ParseMetadata (RequestMetadata (Protobuf serv meth))
, BuildMetadata (ResponseInitialMetadata (Protobuf serv meth))
, StaticMetadata (ResponseTrailingMetadata (Protobuf serv meth))
) => SupportsServerRpc (Protobuf serv meth) where
rpcDeserializeInput _ = Protobuf.parseLazy
rpcSerializeOutput _ = Protobuf.buildLazy
instance ( styp ~ MethodStreamingType serv meth
, ValidStreamingType styp
)
=> SupportsStreamingType (Protobuf serv meth) styp
instance ValidStreamingType (MethodStreamingType serv meth)
=> HasStreamingType (Protobuf serv meth) where
type RpcStreamingType (Protobuf serv meth) = MethodStreamingType serv meth
{-------------------------------------------------------------------------------
Wrapper around Protobuf messages
-------------------------------------------------------------------------------}
-- | Wrapper around Protobuf messages and Protobuf enums
--
-- Protobuf messages and enums behave differently to normal Haskell datatypes.
-- Fields in messages always have defaults, enums can have unknown values, etc.
-- We therefore mark them at the type-level with this t'Proto' wrapper. Most of
-- the time you can work with t'Proto' values as if the wrapper is not there,
-- because @Proto msg@ inherits 'Message' and @Data.ProtoLens.Field@
-- 'ProtoLens.HasField' instances from @msg@. For example, you can create a
-- 'Proto Point' value as
--
-- > p = defMessage
-- > & #latitude .~ ..
-- > & #longitude .~ ..
--
-- and access fields /from/ such a value using
--
-- > p ^. #latitude
--
-- as per usual.
--
-- One advantage of the t'Proto' wrapper is that we can give blanket instances
-- for /all/ Protobuf messages; we use this to provide @GHC.Records@
-- 'GHC.HasField' and @GHC.Records.Compat@ 'GHC.Compat.HasField' instances.
-- This means that you can also use @OverloadedRecordDot@ to access fields
--
-- > p.latitude
--
-- or even @OverloadedRecordUpdate@ to set fields
--
-- > p{latitude = ..}
newtype Proto msg = Proto msg
deriving stock (Show)
deriving newtype (
Eq
, Ord
, Bounded
, Enum
, FieldDefault
, MessageEnum
, NFData
)
-- | Field accessor for t'Proto'
getProto :: Proto msg -> msg
-- Implementation note: This /must/ be defined separately from the 'Proto'
-- newtype, otherwise ghc won't let us define a 'GHC.HasField' instance.
getProto (Proto msg) = msg
instance Message msg => Message (Proto msg) where
messageName _ = messageName (Proxy @msg)
packedMessageDescriptor _ = packedMessageDescriptor (Proxy @msg)
packedFileDescriptor _ = packedFileDescriptor (Proxy @msg)
defMessage = Proto defMessage
buildMessage = buildMessage . getProto
parseMessage = Proto <$> parseMessage
unknownFields = _proto . unknownFields
fieldsByTag = protoFieldDescriptor <$> fieldsByTag
{-------------------------------------------------------------------------------
ProtoLens.HasField instance for Proto
We want to piggy-back on the generated HasField instance, but re-wrap nested
messages in 'Proto' (but not primitive fields).
See
* <https://protobuf.dev/programming-guides/proto3/>
* @Data.ProtoLens.Compiler.Generate.Field@ in @proto-lens-protoc@
-------------------------------------------------------------------------------}
-- | Field description
data FieldDesc = MkFieldDesc FieldLabel IsScalar
-- | Is this a scalar field or an enum/nested message?
data IsScalar = Scalar | NotScalar
-- | Field label
--
-- <https://protobuf.dev/programming-guides/proto3/#field-labels>
data FieldLabel =
LabelImplicit
| LabelOptional
| LabelRepeated
| LabelMap
type family Describe (a :: Type) :: FieldDesc where
Describe (Maybe a) = MkFieldDesc LabelOptional (CheckIsScalar a)
Describe [a] = MkFieldDesc LabelRepeated (CheckIsScalar a)
Describe (Unboxed.Vector a) = MkFieldDesc LabelRepeated Scalar
Describe (Boxed.Vector a) = MkFieldDesc LabelRepeated (CheckIsScalar a)
Describe (Map _ a) = MkFieldDesc LabelMap (CheckIsScalar a)
Describe a = MkFieldDesc LabelImplicit (CheckIsScalar a)
type family CheckIsScalar (a :: Type) :: IsScalar where
CheckIsScalar Bool = Scalar
CheckIsScalar Double = Scalar
CheckIsScalar Float = Scalar
CheckIsScalar Int32 = Scalar
CheckIsScalar Int64 = Scalar
CheckIsScalar Strict.ByteString = Scalar
CheckIsScalar Text = Scalar
CheckIsScalar Word32 = Scalar
CheckIsScalar Word64 = Scalar
CheckIsScalar _ = NotScalar
class RewrapField (desc :: FieldDesc) (x :: Type) (y :: Type) | desc x -> y where
rewrapField :: Proxy# desc -> Lens' x y
instance RewrapField (MkFieldDesc label Scalar) a a where
rewrapField _ = coerced
instance RewrapField (MkFieldDesc LabelImplicit NotScalar) a (Proto a) where
rewrapField _ = coerced
instance RewrapField (MkFieldDesc LabelOptional NotScalar) (Maybe a) (Maybe (Proto a)) where
rewrapField _ = coerced
instance RewrapField (MkFieldDesc LabelRepeated NotScalar) [a] [Proto a] where
rewrapField _ = coerced
instance RewrapField (MkFieldDesc LabelRepeated NotScalar) (Boxed.Vector a) (Boxed.Vector (Proto a)) where
rewrapField _ = coerced
instance RewrapField (MkFieldDesc LabelMap NotScalar) (Map k a) (Map k (Proto a)) where
rewrapField _ = coerced
instance
( ProtoLens.HasField rec fldName x
, RewrapField (Describe x) x fldType
)
=> ProtoLens.HasField (Proto rec) fldName fldType where
fieldOf p = _proto . ProtoLens.fieldOf p . rewrapField (proxy# @(Describe x))
instance ProtoLens.HasField (Proto rec) fldName fldType
=> GHC.HasField fldName (Proto rec) fldType where
getField r = (
r ^. ProtoLens.fieldOf (proxy# @fldName)
)
instance ProtoLens.HasField (Proto rec) fldName fldType
=> GHC.Compat.HasField fldName (Proto rec) fldType where
hasField r = (
\a -> r & ProtoLens.fieldOf (proxy# @fldName) .~ a
, r ^. ProtoLens.fieldOf (proxy# @fldName)
)
{-------------------------------------------------------------------------------
Internal auxiliary: Proto wrapper
-------------------------------------------------------------------------------}
_proto :: Iso' (Proto msg) msg
_proto = coerced
protoFieldDescriptor :: FieldDescriptor msg -> FieldDescriptor (Proto msg)
protoFieldDescriptor (FieldDescriptor name typ acc) =
FieldDescriptor name typ (protoFieldAccessor acc)
protoFieldAccessor :: FieldAccessor msg value -> FieldAccessor (Proto msg) value
protoFieldAccessor (PlainField def lens) =
PlainField def (coerced . lens)
protoFieldAccessor (OptionalField lens) =
OptionalField (coerced . lens)
protoFieldAccessor (RepeatedField packing lens) =
RepeatedField packing (coerced . lens)
protoFieldAccessor (MapField lensKey lensValue lensMap) =
MapField lensKey lensValue (coerced . lensMap)