Browse Source

Add helpers to conver between Status.Code and RPCError.Code (#1688)

Motivation:

It's often useful to conver between RPCError and Status codes but we
don't currently expose any API to do this; users have to go via the
`rawValue` APIs. It should be easier than that.

Modifications:

- Add convenience APIs to `RPCError.Code` and `Status.Code` to create
  one from the other.

Result:

- Easier to convert between status and error codes
George Barnett 2 years ago
parent
commit
6ccafcc6c4

+ 27 - 23
Sources/GRPCCore/RPCError.swift

@@ -75,7 +75,7 @@ public struct RPCError: @unchecked Sendable, Hashable, Error {
   ///
   ///
   /// - Parameter status: The status to convert.
   /// - Parameter status: The status to convert.
   public init?(status: Status) {
   public init?(status: Status) {
-    guard let code = Code(statusCode: status.code) else { return nil }
+    guard let code = Code(status.code) else { return nil }
     self.init(code: code, message: status.message, metadata: [:])
     self.init(code: code, message: status.message, metadata: [:])
   }
   }
 }
 }
@@ -119,16 +119,20 @@ extension RPCError {
     /// The numeric value of the error code.
     /// The numeric value of the error code.
     public var rawValue: Int { Int(self.wrapped.rawValue) }
     public var rawValue: Int { Int(self.wrapped.rawValue) }
 
 
-    private var wrapped: Status.Code.Wrapped
-    private init(_ wrapped: Status.Code.Wrapped) {
-      self.wrapped = wrapped
+    internal var wrapped: Status.Code.Wrapped
+    private init(code: Status.Code.Wrapped) {
+      self.wrapped = code
     }
     }
 
 
-    internal init?(statusCode: Status.Code) {
-      if statusCode == .ok {
+    /// Creates an error code from the given ``Status/Code-swift.struct``; returns `nil` if the
+    /// code is ``Status/Code-swift.struct/ok``.
+    ///
+    /// - Parameter code: The status code to create this ``RPCError/Code-swift.struct`` from.
+    public init?(_ code: Status.Code) {
+      if code == .ok {
         return nil
         return nil
       } else {
       } else {
-        self.wrapped = statusCode.wrapped
+        self.wrapped = code.wrapped
       }
       }
     }
     }
 
 
@@ -140,44 +144,44 @@ extension RPCError {
 
 
 extension RPCError.Code {
 extension RPCError.Code {
   /// The operation was cancelled (typically by the caller).
   /// The operation was cancelled (typically by the caller).
-  public static let cancelled = Self(.cancelled)
+  public static let cancelled = Self(code: .cancelled)
 
 
   /// Unknown error. An example of where this error may be returned is if a
   /// Unknown error. An example of where this error may be returned is if a
   /// Status value received from another address space belongs to an error-space
   /// 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
   /// 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.
   /// do not return enough error information may be converted to this error.
-  public static let unknown = Self(.unknown)
+  public static let unknown = Self(code: .unknown)
 
 
   /// Client specified an invalid argument. Note that this differs from
   /// Client specified an invalid argument. Note that this differs from
   /// ``failedPrecondition``. ``invalidArgument`` indicates arguments that are
   /// ``failedPrecondition``. ``invalidArgument`` indicates arguments that are
   /// problematic regardless of the state of the system (e.g., a malformed file
   /// problematic regardless of the state of the system (e.g., a malformed file
   /// name).
   /// name).
-  public static let invalidArgument = Self(.invalidArgument)
+  public static let invalidArgument = Self(code: .invalidArgument)
 
 
   /// Deadline expired before operation could complete. For operations that
   /// Deadline expired before operation could complete. For operations that
   /// change the state of the system, this error may be returned even if the
   /// change the state of the system, this error may be returned even if the
   /// operation has completed successfully. For example, a successful response
   /// operation has completed successfully. For example, a successful response
   /// from a server could have been delayed long enough for the deadline to
   /// from a server could have been delayed long enough for the deadline to
   /// expire.
   /// expire.
-  public static let deadlineExceeded = Self(.deadlineExceeded)
+  public static let deadlineExceeded = Self(code: .deadlineExceeded)
 
 
   /// Some requested entity (e.g., file or directory) was not found.
   /// Some requested entity (e.g., file or directory) was not found.
-  public static let notFound = Self(.notFound)
+  public static let notFound = Self(code: .notFound)
 
 
   /// Some entity that we attempted to create (e.g., file or directory) already
   /// Some entity that we attempted to create (e.g., file or directory) already
   /// exists.
   /// exists.
-  public static let alreadyExists = Self(.alreadyExists)
+  public static let alreadyExists = Self(code: .alreadyExists)
 
 
   /// The caller does not have permission to execute the specified operation.
   /// The caller does not have permission to execute the specified operation.
   /// ``permissionDenied`` must not be used for rejections caused by exhausting
   /// ``permissionDenied`` must not be used for rejections caused by exhausting
   /// some resource (use ``resourceExhausted`` instead for those errors).
   /// some resource (use ``resourceExhausted`` instead for those errors).
   /// ``permissionDenied`` must not be used if the caller can not be identified
   /// ``permissionDenied`` must not be used if the caller can not be identified
   /// (use ``unauthenticated`` instead for those errors).
   /// (use ``unauthenticated`` instead for those errors).
-  public static let permissionDenied = Self(.permissionDenied)
+  public static let permissionDenied = Self(code: .permissionDenied)
 
 
   /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the
   /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the
   /// entire file system is out of space.
   /// entire file system is out of space.
-  public static let resourceExhausted = Self(.resourceExhausted)
+  public static let resourceExhausted = Self(code: .resourceExhausted)
 
 
   /// Operation was rejected because the system is not in a state required for
   /// Operation was rejected because the system is not in a state required for
   /// the operation's execution. For example, directory to be deleted may be
   /// the operation's execution. For example, directory to be deleted may be
@@ -197,14 +201,14 @@ extension RPCError.Code {
   ///   REST Get/Update/Delete on a resource and the resource on the
   ///   REST Get/Update/Delete on a resource and the resource on the
   ///   server does not match the condition. E.g., conflicting
   ///   server does not match the condition. E.g., conflicting
   ///   read-modify-write on the same resource.
   ///   read-modify-write on the same resource.
-  public static let failedPrecondition = Self(.failedPrecondition)
+  public static let failedPrecondition = Self(code: .failedPrecondition)
 
 
   /// The operation was aborted, typically due to a concurrency issue like
   /// The operation was aborted, typically due to a concurrency issue like
   /// sequencer check failures, transaction aborts, etc.
   /// sequencer check failures, transaction aborts, etc.
   ///
   ///
   /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``,
   /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``,
   /// and ``unavailable``.
   /// and ``unavailable``.
-  public static let aborted = Self(.aborted)
+  public static let aborted = Self(code: .aborted)
 
 
   /// Operation was attempted past the valid range. E.g., seeking or reading
   /// Operation was attempted past the valid range. E.g., seeking or reading
   /// past end of file.
   /// past end of file.
@@ -219,26 +223,26 @@ extension RPCError.Code {
   /// ``outOfRange``. We recommend using ``outOfRange`` (the more specific error)
   /// ``outOfRange``. We recommend using ``outOfRange`` (the more specific error)
   /// when it applies so that callers who are iterating through a space can
   /// when it applies so that callers who are iterating through a space can
   /// easily look for an ``outOfRange`` error to detect when they are done.
   /// easily look for an ``outOfRange`` error to detect when they are done.
-  public static let outOfRange = Self(.outOfRange)
+  public static let outOfRange = Self(code: .outOfRange)
 
 
   /// Operation is not implemented or not supported/enabled in this service.
   /// Operation is not implemented or not supported/enabled in this service.
-  public static let unimplemented = Self(.unimplemented)
+  public static let unimplemented = Self(code: .unimplemented)
 
 
   /// Internal errors. Means some invariants expected by underlying System has
   /// Internal errors. Means some invariants expected by underlying System has
   /// been broken. If you see one of these errors, Something is very broken.
   /// been broken. If you see one of these errors, Something is very broken.
-  public static let internalError = Self(.internalError)
+  public static let internalError = Self(code: .internalError)
 
 
   /// The service is currently unavailable. This is a most likely a transient
   /// The service is currently unavailable. This is a most likely a transient
   /// condition and may be corrected by retrying with a backoff.
   /// condition and may be corrected by retrying with a backoff.
   ///
   ///
   /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``,
   /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``,
   /// and ``unavailable``.
   /// and ``unavailable``.
-  public static let unavailable = Self(.unavailable)
+  public static let unavailable = Self(code: .unavailable)
 
 
   /// Unrecoverable data loss or corruption.
   /// Unrecoverable data loss or corruption.
-  public static let dataLoss = Self(.dataLoss)
+  public static let dataLoss = Self(code: .dataLoss)
 
 
   /// The request does not have valid authentication credentials for the
   /// The request does not have valid authentication credentials for the
   /// operation.
   /// operation.
-  public static let unauthenticated = Self(.unauthenticated)
+  public static let unauthenticated = Self(code: .unauthenticated)
 }
 }

+ 27 - 19
Sources/GRPCCore/Status.swift

@@ -145,8 +145,16 @@ extension Status {
       }
       }
     }
     }
 
 
-    private init(_ wrapped: Wrapped) {
-      self.wrapped = wrapped
+    /// Creates a status code from an ``RPCError/Code-swift.struct``.
+    ///
+    /// - Parameters:
+    ///   - code: The error code to create this ``Status/Code-swift.struct`` from.
+    public init(_ code: RPCError.Code) {
+      self.wrapped = code.wrapped
+    }
+
+    private init(code: Wrapped) {
+      self.wrapped = code
     }
     }
 
 
     public var description: String {
     public var description: String {
@@ -157,47 +165,47 @@ extension Status {
 
 
 extension Status.Code {
 extension Status.Code {
   /// The operation completed successfully.
   /// The operation completed successfully.
-  public static let ok = Self(.ok)
+  public static let ok = Self(code: .ok)
 
 
   /// The operation was cancelled (typically by the caller).
   /// The operation was cancelled (typically by the caller).
-  public static let cancelled = Self(.cancelled)
+  public static let cancelled = Self(code: .cancelled)
 
 
   /// Unknown error. An example of where this error may be returned is if a
   /// Unknown error. An example of where this error may be returned is if a
   /// Status value received from another address space belongs to an error-space
   /// 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
   /// 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.
   /// do not return enough error information may be converted to this error.
-  public static let unknown = Self(.unknown)
+  public static let unknown = Self(code: .unknown)
 
 
   /// Client specified an invalid argument. Note that this differs from
   /// Client specified an invalid argument. Note that this differs from
   /// ``failedPrecondition``. ``invalidArgument`` indicates arguments that are
   /// ``failedPrecondition``. ``invalidArgument`` indicates arguments that are
   /// problematic regardless of the state of the system (e.g., a malformed file
   /// problematic regardless of the state of the system (e.g., a malformed file
   /// name).
   /// name).
-  public static let invalidArgument = Self(.invalidArgument)
+  public static let invalidArgument = Self(code: .invalidArgument)
 
 
   /// Deadline expired before operation could complete. For operations that
   /// Deadline expired before operation could complete. For operations that
   /// change the state of the system, this error may be returned even if the
   /// change the state of the system, this error may be returned even if the
   /// operation has completed successfully. For example, a successful response
   /// operation has completed successfully. For example, a successful response
   /// from a server could have been delayed long enough for the deadline to
   /// from a server could have been delayed long enough for the deadline to
   /// expire.
   /// expire.
-  public static let deadlineExceeded = Self(.deadlineExceeded)
+  public static let deadlineExceeded = Self(code: .deadlineExceeded)
 
 
   /// Some requested entity (e.g., file or directory) was not found.
   /// Some requested entity (e.g., file or directory) was not found.
-  public static let notFound = Self(.notFound)
+  public static let notFound = Self(code: .notFound)
 
 
   /// Some entity that we attempted to create (e.g., file or directory) already
   /// Some entity that we attempted to create (e.g., file or directory) already
   /// exists.
   /// exists.
-  public static let alreadyExists = Self(.alreadyExists)
+  public static let alreadyExists = Self(code: .alreadyExists)
 
 
   /// The caller does not have permission to execute the specified operation.
   /// The caller does not have permission to execute the specified operation.
   /// ``permissionDenied`` must not be used for rejections caused by exhausting
   /// ``permissionDenied`` must not be used for rejections caused by exhausting
   /// some resource (use ``resourceExhausted`` instead for those errors).
   /// some resource (use ``resourceExhausted`` instead for those errors).
   /// ``permissionDenied`` must not be used if the caller can not be identified
   /// ``permissionDenied`` must not be used if the caller can not be identified
   /// (use ``unauthenticated`` instead for those errors).
   /// (use ``unauthenticated`` instead for those errors).
-  public static let permissionDenied = Self(.permissionDenied)
+  public static let permissionDenied = Self(code: .permissionDenied)
 
 
   /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the
   /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the
   /// entire file system is out of space.
   /// entire file system is out of space.
-  public static let resourceExhausted = Self(.resourceExhausted)
+  public static let resourceExhausted = Self(code: .resourceExhausted)
 
 
   /// Operation was rejected because the system is not in a state required for
   /// Operation was rejected because the system is not in a state required for
   /// the operation's execution. For example, directory to be deleted may be
   /// the operation's execution. For example, directory to be deleted may be
@@ -217,14 +225,14 @@ extension Status.Code {
   ///   REST Get/Update/Delete on a resource and the resource on the
   ///   REST Get/Update/Delete on a resource and the resource on the
   ///   server does not match the condition. E.g., conflicting
   ///   server does not match the condition. E.g., conflicting
   ///   read-modify-write on the same resource.
   ///   read-modify-write on the same resource.
-  public static let failedPrecondition = Self(.failedPrecondition)
+  public static let failedPrecondition = Self(code: .failedPrecondition)
 
 
   /// The operation was aborted, typically due to a concurrency issue like
   /// The operation was aborted, typically due to a concurrency issue like
   /// sequencer check failures, transaction aborts, etc.
   /// sequencer check failures, transaction aborts, etc.
   ///
   ///
   /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``,
   /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``,
   /// and ``unavailable``.
   /// and ``unavailable``.
-  public static let aborted = Self(.aborted)
+  public static let aborted = Self(code: .aborted)
 
 
   /// Operation was attempted past the valid range. E.g., seeking or reading
   /// Operation was attempted past the valid range. E.g., seeking or reading
   /// past end of file.
   /// past end of file.
@@ -239,26 +247,26 @@ extension Status.Code {
   /// ``outOfRange``. We recommend using ``outOfRange`` (the more specific error)
   /// ``outOfRange``. We recommend using ``outOfRange`` (the more specific error)
   /// when it applies so that callers who are iterating through a space can
   /// when it applies so that callers who are iterating through a space can
   /// easily look for an ``outOfRange`` error to detect when they are done.
   /// easily look for an ``outOfRange`` error to detect when they are done.
-  public static let outOfRange = Self(.outOfRange)
+  public static let outOfRange = Self(code: .outOfRange)
 
 
   /// Operation is not implemented or not supported/enabled in this service.
   /// Operation is not implemented or not supported/enabled in this service.
-  public static let unimplemented = Self(.unimplemented)
+  public static let unimplemented = Self(code: .unimplemented)
 
 
   /// Internal errors. Means some invariants expected by underlying System has
   /// Internal errors. Means some invariants expected by underlying System has
   /// been broken. If you see one of these errors, Something is very broken.
   /// been broken. If you see one of these errors, Something is very broken.
-  public static let internalError = Self(.internalError)
+  public static let internalError = Self(code: .internalError)
 
 
   /// The service is currently unavailable. This is a most likely a transient
   /// The service is currently unavailable. This is a most likely a transient
   /// condition and may be corrected by retrying with a backoff.
   /// condition and may be corrected by retrying with a backoff.
   ///
   ///
   /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``,
   /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``,
   /// and ``unavailable``.
   /// and ``unavailable``.
-  public static let unavailable = Self(.unavailable)
+  public static let unavailable = Self(code: .unavailable)
 
 
   /// Unrecoverable data loss or corruption.
   /// Unrecoverable data loss or corruption.
-  public static let dataLoss = Self(.dataLoss)
+  public static let dataLoss = Self(code: .dataLoss)
 
 
   /// The request does not have valid authentication credentials for the
   /// The request does not have valid authentication credentials for the
   /// operation.
   /// operation.
-  public static let unauthenticated = Self(.unauthenticated)
+  public static let unauthenticated = Self(code: .unauthenticated)
 }
 }

+ 20 - 0
Tests/GRPCCoreTests/RPCErrorTests.swift

@@ -61,6 +61,26 @@ final class RPCErrorTests: XCTestCase {
     XCTAssertEqual(error.metadata, [:])
     XCTAssertEqual(error.metadata, [:])
   }
   }
 
 
+  func testErrorCodeFromStatusCode() throws {
+    XCTAssertNil(RPCError.Code(Status.Code.ok))
+    XCTAssertEqual(RPCError.Code(Status.Code.cancelled), .cancelled)
+    XCTAssertEqual(RPCError.Code(Status.Code.unknown), .unknown)
+    XCTAssertEqual(RPCError.Code(Status.Code.invalidArgument), .invalidArgument)
+    XCTAssertEqual(RPCError.Code(Status.Code.deadlineExceeded), .deadlineExceeded)
+    XCTAssertEqual(RPCError.Code(Status.Code.notFound), .notFound)
+    XCTAssertEqual(RPCError.Code(Status.Code.alreadyExists), .alreadyExists)
+    XCTAssertEqual(RPCError.Code(Status.Code.permissionDenied), .permissionDenied)
+    XCTAssertEqual(RPCError.Code(Status.Code.resourceExhausted), .resourceExhausted)
+    XCTAssertEqual(RPCError.Code(Status.Code.failedPrecondition), .failedPrecondition)
+    XCTAssertEqual(RPCError.Code(Status.Code.aborted), .aborted)
+    XCTAssertEqual(RPCError.Code(Status.Code.outOfRange), .outOfRange)
+    XCTAssertEqual(RPCError.Code(Status.Code.unimplemented), .unimplemented)
+    XCTAssertEqual(RPCError.Code(Status.Code.internalError), .internalError)
+    XCTAssertEqual(RPCError.Code(Status.Code.unavailable), .unavailable)
+    XCTAssertEqual(RPCError.Code(Status.Code.dataLoss), .dataLoss)
+    XCTAssertEqual(RPCError.Code(Status.Code.unauthenticated), .unauthenticated)
+  }
+
   func testEquatableConformance() {
   func testEquatableConformance() {
     XCTAssertEqual(
     XCTAssertEqual(
       RPCError(code: .cancelled, message: ""),
       RPCError(code: .cancelled, message: ""),

+ 19 - 0
Tests/GRPCCoreTests/StatusTests.swift

@@ -50,6 +50,25 @@ final class StatusTests: XCTestCase {
     }
     }
   }
   }
 
 
+  func testStatusCodeFromErrorCode() throws {
+    XCTAssertEqual(Status.Code(RPCError.Code.cancelled), .cancelled)
+    XCTAssertEqual(Status.Code(RPCError.Code.unknown), .unknown)
+    XCTAssertEqual(Status.Code(RPCError.Code.invalidArgument), .invalidArgument)
+    XCTAssertEqual(Status.Code(RPCError.Code.deadlineExceeded), .deadlineExceeded)
+    XCTAssertEqual(Status.Code(RPCError.Code.notFound), .notFound)
+    XCTAssertEqual(Status.Code(RPCError.Code.alreadyExists), .alreadyExists)
+    XCTAssertEqual(Status.Code(RPCError.Code.permissionDenied), .permissionDenied)
+    XCTAssertEqual(Status.Code(RPCError.Code.resourceExhausted), .resourceExhausted)
+    XCTAssertEqual(Status.Code(RPCError.Code.failedPrecondition), .failedPrecondition)
+    XCTAssertEqual(Status.Code(RPCError.Code.aborted), .aborted)
+    XCTAssertEqual(Status.Code(RPCError.Code.outOfRange), .outOfRange)
+    XCTAssertEqual(Status.Code(RPCError.Code.unimplemented), .unimplemented)
+    XCTAssertEqual(Status.Code(RPCError.Code.internalError), .internalError)
+    XCTAssertEqual(Status.Code(RPCError.Code.unavailable), .unavailable)
+    XCTAssertEqual(Status.Code(RPCError.Code.dataLoss), .dataLoss)
+    XCTAssertEqual(Status.Code(RPCError.Code.unauthenticated), .unauthenticated)
+  }
+
   func testStatusCodeFromValidRawValue() {
   func testStatusCodeFromValidRawValue() {
     for (expected, rawValue) in Self.statusCodeRawValue {
     for (expected, rawValue) in Self.statusCodeRawValue {
       XCTAssertEqual(
       XCTAssertEqual(