{-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-} module Network.GRPC.HighLevel.Client ( ClientError(..) , ClientRegisterable(..) , ClientRequest(..) , ClientResult(..) , GRPCMethodType(..) , MetadataMap(..) , RegisteredMethod , ServiceClient , StatusCode(..) , StatusDetails(..) , StreamRecv , StreamSend , TimeoutSeconds , WritesDone , LL.Client , LL.ClientConfig(..) , LL.ClientSSLConfig(..) , LL.ClientSSLKeyCertPair(..) , LL.Host(..) , LL.Port(..) , clientRequest -- * Client utility functions , acquireClient , simplifyServerStreaming , simplifyUnary ) where import Control.Monad.Managed (Managed, liftIO, managed) import qualified Data.ByteString.Lazy as BL import Network.GRPC.HighLevel.Server (convertRecv, convertSend) import Network.GRPC.LowLevel (GRPCIOError (..), GRPCMethodType (..), MetadataMap (..), StatusCode (..), StatusDetails (..), StreamRecv, StreamSend) import qualified Network.GRPC.LowLevel as LL import Network.GRPC.LowLevel.CompletionQueue (TimeoutSeconds) import Network.GRPC.LowLevel.Op (WritesDone) import Proto3.Suite (Message, fromByteString, toLazyByteString) import Proto3.Wire.Decode (ParseError) newtype RegisteredMethod (mt :: GRPCMethodType) request response = RegisteredMethod (LL.RegisteredMethod mt) deriving Show type ServiceClient service = service ClientRequest ClientResult data ClientError = ClientErrorNoParse ParseError | ClientIOError GRPCIOError deriving (Show, Eq) data ClientRequest (streamType :: GRPCMethodType) request response where ClientNormalRequest :: request -> TimeoutSeconds -> MetadataMap -> ClientRequest 'Normal request response ClientWriterRequest :: TimeoutSeconds -> MetadataMap -> (StreamSend request -> IO ()) -> ClientRequest 'ClientStreaming request response -- | The final field will be invoked once, and it should repeatedly -- invoke its final argument (of type @(StreamRecv response)@) -- in order to obtain the streaming response incrementally. ClientReaderRequest :: request -> TimeoutSeconds -> MetadataMap -> (LL.ClientCall -> MetadataMap -> StreamRecv response -> IO ()) -> ClientRequest 'ServerStreaming request response ClientBiDiRequest :: TimeoutSeconds -> MetadataMap -> (LL.ClientCall -> MetadataMap -> StreamRecv response -> StreamSend request -> WritesDone -> IO ()) -> ClientRequest 'BiDiStreaming request response data ClientResult (streamType :: GRPCMethodType) response where ClientNormalResponse :: response -> MetadataMap -> MetadataMap -> StatusCode -> StatusDetails -> ClientResult 'Normal response ClientWriterResponse :: Maybe response -> MetadataMap -> MetadataMap -> StatusCode -> StatusDetails -> ClientResult 'ClientStreaming response ClientReaderResponse :: MetadataMap -> StatusCode -> StatusDetails -> ClientResult 'ServerStreaming response ClientBiDiResponse :: MetadataMap -> StatusCode -> StatusDetails -> ClientResult 'BiDiStreaming response ClientErrorResponse :: ClientError -> ClientResult streamType response class ClientRegisterable (methodType :: GRPCMethodType) where clientRegisterMethod :: LL.Client -> LL.MethodName -> IO (RegisteredMethod methodType request response) instance ClientRegisterable 'Normal where clientRegisterMethod client methodName = RegisteredMethod <$> LL.clientRegisterMethodNormal client methodName instance ClientRegisterable 'ClientStreaming where clientRegisterMethod client methodName = RegisteredMethod <$> LL.clientRegisterMethodClientStreaming client methodName instance ClientRegisterable 'ServerStreaming where clientRegisterMethod client methodName = RegisteredMethod <$> LL.clientRegisterMethodServerStreaming client methodName instance ClientRegisterable 'BiDiStreaming where clientRegisterMethod client methodName = RegisteredMethod <$> LL.clientRegisterMethodBiDiStreaming client methodName clientRequest :: (Message request, Message response) => LL.Client -> RegisteredMethod streamType request response -> ClientRequest streamType request response -> IO (ClientResult streamType response) clientRequest client (RegisteredMethod method) (ClientNormalRequest req timeout meta) = mkResponse <$> LL.clientRequest client method timeout (BL.toStrict (toLazyByteString req)) meta where mkResponse (Left ioError_) = ClientErrorResponse (ClientIOError ioError_) mkResponse (Right rsp) = case fromByteString (LL.rspBody rsp) of Left err -> ClientErrorResponse (ClientErrorNoParse err) Right parsedRsp -> ClientNormalResponse parsedRsp (LL.initMD rsp) (LL.trailMD rsp) (LL.rspCode rsp) (LL.details rsp) clientRequest client (RegisteredMethod method) (ClientWriterRequest timeout meta handler) = mkResponse <$> LL.clientWriter client method timeout meta (handler . convertSend) where mkResponse (Left ioError_) = ClientErrorResponse (ClientIOError ioError_) mkResponse (Right (rsp_, initMD_, trailMD_, rspCode_, details_)) = case maybe (Right Nothing) (fmap Just . fromByteString) rsp_ of Left err -> ClientErrorResponse (ClientErrorNoParse err) Right parsedRsp -> ClientWriterResponse parsedRsp initMD_ trailMD_ rspCode_ details_ clientRequest client (RegisteredMethod method) (ClientReaderRequest req timeout meta handler) = mkResponse <$> LL.clientReader client method timeout (BL.toStrict (toLazyByteString req)) meta (\cc m recv -> handler cc m (convertRecv recv)) where mkResponse (Left ioError_) = ClientErrorResponse (ClientIOError ioError_) mkResponse (Right (meta_, rspCode_, details_)) = ClientReaderResponse meta_ rspCode_ details_ clientRequest client (RegisteredMethod method) (ClientBiDiRequest timeout meta handler) = mkResponse <$> LL.clientRW client method timeout meta (\cc _m recv send writesDone -> handler cc meta (convertRecv recv) (convertSend send) writesDone) where mkResponse (Left ioError_) = ClientErrorResponse (ClientIOError ioError_) mkResponse (Right (meta_, rspCode_, details_)) = ClientBiDiResponse meta_ rspCode_ details_ acquireClient :: LL.ClientConfig -- ^ The client configuration (host, port, SSL settings, etc) -> (LL.Client -> IO (ServiceClient service)) -- ^ The client implementation (typically generated) -> Managed (ServiceClient service) acquireClient cfg impl = do g <- managed LL.withGRPC c <- managed (LL.withClient g cfg) liftIO (impl c) -- | A utility for simplifying server-streaming gRPC client requests; you can -- use this to avoid 'ClientRequest' and 'ClientResult' pattern-matching -- boilerplate at call sites. simplifyServerStreaming :: TimeoutSeconds -- ^ RPC call timeout, in seconds -> MetadataMap -- ^ RPC call metadata -> (ClientError -> IO StatusDetails) -- ^ Handler for client errors -> (StatusCode -> StatusDetails -> IO StatusDetails) -- ^ Handler for non-StatusOk response -> (ClientRequest 'ServerStreaming request response -> IO (ClientResult 'ServerStreaming response)) -- ^ Endpoint implementation (typically generated by grpc-haskell) -> request -- ^ Request payload -> (LL.ClientCall -> MetadataMap -> StreamRecv response -> IO ()) -- ^ Stream handler; note that the 'StreamRecv' -- action must be called repeatedly in order to -- consume the stream -> IO StatusDetails simplifyServerStreaming timeout meta clientError nonStatusOkError f x handler = do let request = ClientReaderRequest x timeout meta handler response <- f request case response of ClientReaderResponse _ StatusOk details -> pure details ClientReaderResponse _ statusCode details -> nonStatusOkError statusCode details ClientErrorResponse err -> clientError err -- | A utility for simplifying unary gRPC client requests; you can use this to -- avoid 'ClientRequest' and 'ClientResult' pattern-matching boilerplate at -- call sites. simplifyUnary :: TimeoutSeconds -- ^ RPC call timeout, in seconds -> MetadataMap -- ^ RPC call metadata -> (ClientError -> IO (response, StatusDetails)) -- ^ Handler for client errors -> (response -> StatusCode -> StatusDetails -> IO (response, StatusDetails)) -- ^ Handler for non-StatusOK responses -> (ClientRequest 'Normal request response -> IO (ClientResult 'Normal response)) -- ^ Endpoint implementation (typically generated by grpc-haskell) -> (request -> IO (response, StatusDetails)) -- ^ The simplified happy-path (StatusOk) unary call action simplifyUnary timeout meta clientError nonStatusOkError f x = do let request = ClientNormalRequest x timeout meta response <- f request case response of ClientNormalResponse y _ _ StatusOk details -> pure (y, details) ClientNormalResponse y _ _ code details -> nonStatusOkError y code details ClientErrorResponse err -> clientError err