Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 46 additions & 15 deletions Sources/GRPCProtobuf/Errors/GoogleRPCStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

public import GRPCCore
internal import SwiftProtobuf
public import SwiftProtobuf

/// An error containing structured details which can be delivered to the client.
///
Expand Down Expand Up @@ -74,31 +74,63 @@ public struct GoogleRPCStatus: Error {
}
}

extension GoogleRPCStatus: GoogleProtobufAnyPackable {
// See https://protobuf.dev/programming-guides/proto3/#any
internal static var typeURL: String { "type.googleapis.com/google.rpc.Status" }

init?(unpacking any: Google_Protobuf_Any) throws {
guard any.isA(Google_Rpc_Status.self) else { return nil }
let status = try Google_Rpc_Status(serializedBytes: any.value)
extension GoogleRPCStatus {
/// Creates a new message by decoding the given `SwiftProtobufContiguousBytes` value
/// containing a serialized message in Protocol Buffer binary format.
///
/// - Parameters:
/// - bytes: The binary-encoded message data to decode.
/// - extensions: An `ExtensionMap` used to look up and decode any
/// extensions in this message or messages nested within this message's
/// fields.
/// - partial: If `false` (the default), this method will check if the `Message`
/// is initialized after decoding to verify that all required fields are present.
/// If any are missing, this method throws `BinaryDecodingError`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be making promises in the docs about the type of the error we're throwing? Can't we use typed throws instead, or omit it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. These are from the protobuf docs and we're really just wrapping the protobuf call so I think it's okay. We shouldn't use typed throws as that paints us into a corner (also, protobuf doesn't use it) if the error type does change.

/// - options: The `BinaryDecodingOptions` to use.
/// - Throws: `BinaryDecodingError` if decoding fails.
public init<Bytes: SwiftProtobufContiguousBytes>(
serializedBytes bytes: Bytes,
extensions: (any ExtensionMap)? = nil,
partial: Bool = false,
options: BinaryDecodingOptions = BinaryDecodingOptions()
) throws {
let status = try Google_Rpc_Status(
serializedBytes: bytes,
extensions: extensions,
partial: partial,
options: options
)

let statusCode = Status.Code(rawValue: Int(status.code))
self.code = statusCode.flatMap { RPCError.Code($0) } ?? .unknown
self.message = status.message
self.details = try status.details.map { try ErrorDetails(unpacking: $0) }
}

func pack() throws -> Google_Protobuf_Any {
/// Returns a `SwiftProtobufContiguousBytes` instance containing the Protocol Buffer binary
/// format serialization of the message.
///
/// - Parameters:
/// - partial: If `false` (the default), this method will check
/// `Message.isInitialized` before encoding to verify that all required
/// fields are present. If any are missing, this method throws.
/// `BinaryEncodingError/missingRequiredFields`.
/// - options: The `BinaryEncodingOptions` to use.
/// - Returns: A `SwiftProtobufContiguousBytes` instance containing the binary serialization
/// of the message.
///
/// - Throws: `SwiftProtobufError` or `BinaryEncodingError` if encoding fails.
public func serializedBytes<Bytes: SwiftProtobufContiguousBytes>(
partial: Bool = false,
options: BinaryEncodingOptions = BinaryEncodingOptions()
) throws -> Bytes {
let status = try Google_Rpc_Status.with {
$0.code = Int32(self.code.rawValue)
$0.message = self.message
$0.details = try self.details.map { try $0.pack() }
}

return try .with {
$0.typeURL = Self.typeURL
$0.value = try status.serializedBytes()
}
return try status.serializedBytes(partial: partial, options: options)
}
}

Expand All @@ -107,8 +139,7 @@ extension GoogleRPCStatus: RPCErrorConvertible {
public var rpcErrorMessage: String { self.message }
public var rpcErrorMetadata: Metadata {
do {
let any = try self.pack()
let bytes: [UInt8] = try any.serializedBytes()
let bytes: [UInt8] = try self.serializedBytes()
return [Metadata.statusDetailsBinKey: .binary(bytes)]
} catch {
// Failed to serialize error details. Not a lot can be done here.
Expand Down
4 changes: 1 addition & 3 deletions Sources/GRPCProtobuf/Errors/RPCError+GoogleRPCStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ extension RPCError {
public func unpackGoogleRPCStatus() throws -> GoogleRPCStatus? {
let values = self.metadata[binaryValues: Metadata.statusDetailsBinKey]
guard let bytes = values.first(where: { _ in true }) else { return nil }

let any = try Google_Protobuf_Any(serializedBytes: bytes)
return try GoogleRPCStatus(unpacking: any)
return try GoogleRPCStatus(serializedBytes: bytes)
}
}
13 changes: 13 additions & 0 deletions Tests/GRPCProtobufTests/Errors/DetailedErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ struct DetailedErrorTests {
func errorInfoDescription(_ details: ErrorDetails, expected: String) {
#expect(String(describing: details) == expected)
}

@Test("Round-trip encoding of GoogleRPCStatus")
func googleRPCStatusRoundTripCoding() throws {
let detail = ErrorDetails.BadRequest(violations: [.init(field: "foo", description: "bar")])
let status = GoogleRPCStatus(code: .dataLoss, message: "Uh oh", details: [.badRequest(detail)])

let serialized: [UInt8] = try status.serializedBytes()
let deserialized = try GoogleRPCStatus(serializedBytes: serialized)
#expect(deserialized.code == status.code)
#expect(deserialized.message == status.message)
#expect(deserialized.details.count == status.details.count)
#expect(deserialized.details.first?.badRequest == detail)
}
}

private struct ErrorThrowingService: ErrorService.SimpleServiceProtocol {
Expand Down
Loading