packages feed

grpc-spec-1.0.0: src/Network/GRPC/Spec/Status.hs

module Network.GRPC.Spec.Status (
    -- * GRPC status
    GrpcStatus(..)
  , GrpcError(..)
  , fromGrpcStatus
  , fromGrpcError
  , toGrpcStatus
  , toGrpcError
    -- * Exceptions
  , GrpcException(..)
  , throwGrpcError
    -- * Details
  , Status
  ) where

import Control.Exception
import Data.ByteString qualified as Strict (ByteString)
import Data.List (intercalate)
import Data.Text (Text)
import Data.Text qualified as Text
import GHC.Generics (Generic)

import Network.GRPC.Spec.CustomMetadata.Raw (CustomMetadata)

import Proto.Status

{-------------------------------------------------------------------------------
  gRPC status
-------------------------------------------------------------------------------}

-- | gRPC status
--
-- Defined in <https://github.com/grpc/grpc/blob/master/doc/statuscodes.md>.
data GrpcStatus =
    GrpcOk
  | GrpcError GrpcError
  deriving stock (Show, Eq, Generic)

-- | gRPC error code
--
-- This is a subset of the gRPC status codes. See 'GrpcStatus'.
data GrpcError =
    -- | Cancelled
    --
    -- The operation was cancelled, typically by the caller.
    GrpcCancelled

    -- | Unknown error
    --
    -- For example, this error may be returned when a @Status@ value received
    -- from another address space belongs to an error space that is not known in
    -- this address space. Also errors raised by APIs that do not return enough
    -- error information may be converted to this error.
  | GrpcUnknown

    -- | Invalid argument
    --
    -- The client specified an invalid argument. Note that this differs from
    -- 'GrpcFailedPrecondition': 'GrpcInvalidArgument' indicates arguments that
    -- are problematic regardless of the state of the system (e.g., a malformed
    -- file name).
  | GrpcInvalidArgument

    -- | Deadline exceeded
    --
    -- The deadline expired before the operation could complete. For operations
    -- that change the state of the system, this error may be returned even if
    -- the operation has completed successfully. For example, a successful
    -- response from a server could have been delayed long.
  | GrpcDeadlineExceeded

    -- | Not found
    --
    -- Some requested entity (e.g., file or directory) was not found.
    --
    -- Note to server developers: if a request is denied for an entire class of
    -- users, such as gradual feature rollout or undocumented allowlist,
    -- 'GrpcNotFound' may be used.
    --
    -- If a request is denied for some users within a class of users, such as
    -- user-based access control, 'GrpcPermissionDenied' must be used.
  | GrpcNotFound

    -- | Already exists
    --
    -- The entity that a client attempted to create (e.g., file or directory)
    -- already exists.
  | GrpcAlreadyExists

    -- | Permission denied
    --
    -- The caller does not have permission to execute the specified operation.
    --
    -- * 'GrpcPermissionDenied' must not be used for rejections caused by
    --   exhausting some resource (use 'GrpcResourceExhausted' instead for those
    --   errors).
    -- * 'GrpcPermissionDenied' must not be used if the caller can not be
    --   identified (use 'GrpcUnauthenticated' instead for those errors).
    --
    -- This error code does not imply the request is valid or the requested
    -- entity exists or satisfies other pre-conditions.
  | GrpcPermissionDenied

    -- | Resource exhausted
    --
    -- Some resource has been exhausted, perhaps a per-user quota, or perhaps
    -- the entire file system is out of space.
  | GrpcResourceExhausted

    -- | Failed precondition
    --
    -- The operation was rejected because the system is not in a state required
    -- for the operation's execution. For example, the directory to be deleted
    -- is non-empty, an rmdir operation is applied to a non-directory, etc.
    --
    -- Service implementors can use the following guidelines to decide between
    -- 'GrpcFailedPrecondition', 'GrpcAborted', and 'GrpcUnavailable':
    --
    -- (a) Use 'GrpcUnavailable' if the client can retry just the failing call.
    -- (b) Use 'GrpcAborted' if the client should retry at a higher level (e.g.,
    --     when a client-specified test-and-set fails, indicating the client
    --     should restart a read-modify-write sequence).
    -- (c) Use `GrpcFailedPrecondition` if the client should not retry until the
    --     system state has been explicitly fixed. E.g., if an @rmdir@ fails
    --     because the directory is non-empty, 'GrpcFailedPrecondition' should
    --     be returned since the client should not retry unless the files are
    --     deleted from the directory.
  | GrpcFailedPrecondition

    -- | Aborted
    --
    -- The operation was aborted, typically due to a concurrency issue such as a
    -- sequencer check failure or transaction abort. See the guidelines above
    -- for deciding between 'GrpcFailedPrecondition', 'GrpcAborted', and
    -- 'GrpcUnavailable'.
  | GrpcAborted

    -- | Out of range
    --
    -- The operation was attempted past the valid range. E.g., seeking or
    -- reading past end-of-file.
    --
    -- Unlike 'GrpcInvalidArgument', this error indicates a problem that may be
    -- fixed if the system state changes. For example, a 32-bit file system will
    -- generate 'GrpcInvalidArgument' if asked to read at an offset that is not
    -- in the range @[0, 2^32-1]@, but it will generate 'GrpcOutOfRange' if
    -- asked to read from an offset past the current file size.
    --
    -- There is a fair bit of overlap between 'GrpcFailedPrecondition' and
    -- 'GrpcOutOfRange'. We recommend using 'GrpcOutOfRange' (the more specific
    -- error) when it applies so that callers who are iterating through a space
    -- can easily look for an 'GrpcOutOfRange' error to detect when they are
    -- done.
  | GrpcOutOfRange

    -- | Unimplemented
    --
    -- The operation is not implemented or is not supported/enabled in this
    -- service.
  | GrpcUnimplemented

    -- | Internal errors
    --
    -- This means that some invariants expected by the underlying system have
    -- been broken. This error code is reserved for serious errors.
  | GrpcInternal

    -- | Unavailable
    --
    -- The service is currently unavailable. This is most likely a transient
    -- condition, which can be corrected by retrying with a backoff. Note that
    -- it is not always safe to retry non-idempotent operations.
  | GrpcUnavailable

    -- | Data loss
    --
    -- Unrecoverable data loss or corruption.
  | GrpcDataLoss

    -- | Unauthenticated
    --
    -- The request does not have valid authentication credentials for the
    -- operation.
  | GrpcUnauthenticated
  deriving stock (Show, Eq, Ord, Generic)
  deriving anyclass (Exception)

{-------------------------------------------------------------------------------
  Status codes
-------------------------------------------------------------------------------}

-- | Translate 'GrpcStatus' to numerical status code
--
-- See <https://grpc.github.io/grpc/core/md_doc_statuscodes.html>
fromGrpcStatus :: GrpcStatus -> Word
fromGrpcStatus  GrpcOk         =  0
fromGrpcStatus (GrpcError err) = fromGrpcError err

-- | Translate 'GrpcError' to numerical status code
--
-- See also 'fromGrpcStatus'
fromGrpcError :: GrpcError -> Word
fromGrpcError GrpcCancelled          =  1
fromGrpcError GrpcUnknown            =  2
fromGrpcError GrpcInvalidArgument    =  3
fromGrpcError GrpcDeadlineExceeded   =  4
fromGrpcError GrpcNotFound           =  5
fromGrpcError GrpcAlreadyExists      =  6
fromGrpcError GrpcPermissionDenied   =  7
fromGrpcError GrpcResourceExhausted  =  8
fromGrpcError GrpcFailedPrecondition =  9
fromGrpcError GrpcAborted            = 10
fromGrpcError GrpcOutOfRange         = 11
fromGrpcError GrpcUnimplemented      = 12
fromGrpcError GrpcInternal           = 13
fromGrpcError GrpcUnavailable        = 14
fromGrpcError GrpcDataLoss           = 15
fromGrpcError GrpcUnauthenticated    = 16

-- | Inverse to 'fromGrpcStatus'
toGrpcStatus :: Word -> Maybe GrpcStatus
toGrpcStatus 0 = Just $ GrpcOk
toGrpcStatus s = GrpcError <$> toGrpcError s

-- | Inverse to 'fromGrpcError'
toGrpcError :: Word -> Maybe GrpcError
toGrpcError  1 = Just $ GrpcCancelled
toGrpcError  2 = Just $ GrpcUnknown
toGrpcError  3 = Just $ GrpcInvalidArgument
toGrpcError  4 = Just $ GrpcDeadlineExceeded
toGrpcError  5 = Just $ GrpcNotFound
toGrpcError  6 = Just $ GrpcAlreadyExists
toGrpcError  7 = Just $ GrpcPermissionDenied
toGrpcError  8 = Just $ GrpcResourceExhausted
toGrpcError  9 = Just $ GrpcFailedPrecondition
toGrpcError 10 = Just $ GrpcAborted
toGrpcError 11 = Just $ GrpcOutOfRange
toGrpcError 12 = Just $ GrpcUnimplemented
toGrpcError 13 = Just $ GrpcInternal
toGrpcError 14 = Just $ GrpcUnavailable
toGrpcError 15 = Just $ GrpcDataLoss
toGrpcError 16 = Just $ GrpcUnauthenticated
toGrpcError _  = Nothing

{-------------------------------------------------------------------------------
  gRPC exceptions
-------------------------------------------------------------------------------}

-- | Server indicated a gRPC error
--
-- For the common case where you just want to set 'grpcError', you can use
-- 'throwGrpcError'.
data GrpcException = GrpcException {
      grpcError          :: GrpcError
    , grpcErrorMessage   :: Maybe Text
    , grpcErrorDetails   :: Maybe Strict.ByteString
    , grpcErrorMetadata  :: [CustomMetadata]
    }
  deriving stock (Show, Eq)

instance Exception GrpcException where
  displayException GrpcException{
                       grpcError
                     , grpcErrorMessage
                     , grpcErrorDetails
                     , grpcErrorMetadata
                     } = (intercalate "\n" . concat) [
        [ concat [
            "gRPC exception "
          , show grpcError
          , " ("
          , show (fromGrpcError grpcError)
          , ")"
          ]
        ]
      , [ intercalate "\n" $
              "Error message:"
            : (map ("| " ++) . lines $ Text.unpack msg)
        | Just msg <- [grpcErrorMessage]
        ]
      , [ "Additional details are available (see 'grpcErrorDetails')."
        | Just _details <- [grpcErrorDetails]
        ]
      , [ show md
        | md <- grpcErrorMetadata
        ]
      ]


-- | Convenience function to throw an t'GrpcException' with the specified error
throwGrpcError :: GrpcError -> IO a
throwGrpcError grpcError = throwIO $ GrpcException {
      grpcError
    , grpcErrorMessage  = Nothing
    , grpcErrorDetails  = Nothing
    , grpcErrorMetadata = []
    }