grapesy-1.0.0: src/Network/GRPC/Common/Protobuf.hs
{-# LANGUAGE OverloadedLabels #-}
-- | Common functionality for working with Protobuf
module Network.GRPC.Common.Protobuf (
Protobuf
, Proto(..)
, getProto
-- * Exceptions
, ProtobufError(..)
, throwProtobufError
, throwProtobufErrorHom
, toProtobufError
, toProtobufErrorHom
-- * Re-exports
-- ** "Data.Function"
, (&)
-- ** "Control.Lens"
, (.~)
, (^.)
, (%~)
-- ** "Data.ProtoLens"
, StreamingType(..)
, HasField(..)
, FieldDefault(..)
, Message(defMessage)
) where
import Control.Exception
import Control.Lens ((.~), (^.), (%~))
import Control.Monad
import Control.Monad.Except
import Data.Bifunctor
import Data.Function ((&))
import Data.Int
import Data.Maybe (fromMaybe)
import Data.ProtoLens.Field (HasField(..))
import Data.ProtoLens.Labels () -- provides instances for OverloadedLabels
import Data.ProtoLens.Message (FieldDefault(..), Message(defMessage))
import Data.Text (Text)
import Network.GRPC.Common.Protobuf.Any (Any)
import Network.GRPC.Common.Protobuf.Any qualified as Any
import Network.GRPC.Spec
import Network.GRPC.Spec.Serialization
{-------------------------------------------------------------------------------
Protobuf-specific errors
-------------------------------------------------------------------------------}
-- | gRPC exception with protobuf-specific error details
--
-- See also 'Status' and @google.rpc.Status@.
data ProtobufError a = ProtobufError {
protobufErrorCode :: GrpcError
, protobufErrorMessage :: Maybe Text
, protobufErrorDetails :: [a]
}
deriving stock (Show, Eq, Ord, Functor, Foldable, Traversable)
-- | Throw 'GrpcException' with Protobuf-specific details
throwProtobufError :: ProtobufError (Proto Any) -> IO x
throwProtobufError ProtobufError{
protobufErrorCode
, protobufErrorMessage
, protobufErrorDetails
} = throwIO $ GrpcException {
grpcError = protobufErrorCode
, grpcErrorMessage = protobufErrorMessage
, grpcErrorDetails = Just $ buildStatus status
, grpcErrorMetadata = []
}
where
status :: Proto Status
status =
defMessage
& #code .~ fromIntegral (fromGrpcError protobufErrorCode)
& #message .~ fromMaybe fieldDefault protobufErrorMessage
& #details .~ protobufErrorDetails
-- | Variation of 'throwProtobufError' for a homogenous list of details
--
-- The @google.rpc.Status@ message uses the Protobuf 'Any' type to store a
-- heterogenous list of details. In case that all elements in this list are
-- actually of the /same/ type, we can provide a simpler API.
throwProtobufErrorHom :: Message a => ProtobufError (Proto a) -> IO x
throwProtobufErrorHom = throwProtobufError . fmap Any.pack
-- | Construct 'ProtobufError' by parsing 'grpcErrorDetails' as 'Status'
--
-- See also 'throwProtobufError'.
toProtobufError :: GrpcException -> Either String (ProtobufError (Proto Any))
toProtobufError err =
case grpcErrorDetails err of
Nothing ->
return ProtobufError{
protobufErrorCode = grpcError err
, protobufErrorMessage = grpcErrorMessage err
, protobufErrorDetails = []
}
Just statusEnc -> do
status :: Proto Status <- parseStatus statusEnc
protobufErrorCode <- checkErrorCode (status ^. #code)
return ProtobufError{
protobufErrorCode
, protobufErrorMessage = constructErrorMessage (status ^. #message)
, protobufErrorDetails = status ^. #details
}
where
-- The gRPC specification mandates
--
-- > If [Status-Details] contains a status code field, it MUST NOT
-- > contradict the Status header. The consumer MUST verify this
-- > requirement.
--
-- We cannot do this check without knowing the format of the status details,
-- so we do it here for the specific case of Protobuf. Since we cannot
-- distinguish between an absent field and a field with a default value in
-- Protobuf, we add a special case for this.
checkErrorCode :: Int32 -> Either String GrpcError
checkErrorCode statusCode
| statusCode == fieldDefault
= return $ grpcError err
| fromGrpcError (grpcError err) == fromIntegral statusCode
= return $ grpcError err
| otherwise
= throwError $ "'Status.code' does not match 'grpc-status'"
-- If the Status-Details does not contain a message (that is, the field is
-- at its default value), we fall back to the status message in the
-- exception itself (if any).
constructErrorMessage :: Text -> Maybe Text
constructErrorMessage msg =
if msg == fieldDefault
then grpcErrorMessage err
else Just msg
-- | Variation of 'toProtobufError' for a homogenous list of details
toProtobufErrorHom :: forall a.
Message a
=> GrpcException -> Either String (ProtobufError (Proto a))
toProtobufErrorHom = traverse aux <=< toProtobufError
where
aux :: Proto Any -> Either String (Proto a)
aux = first show . Any.unpack