Skip to content

Commit

Permalink
Add support for the detailed error model (#22)
Browse files Browse the repository at this point in the history
Motivation:

A common extension to gRPC is a the detailed error model. While not a
standard gRPC feature it is widely used. The standard error model only
allows for a status code and message to be propagated to the client. The
detailed error model allows users to use a common set of structured
errors and send them to the client in metadata. The model uses the
'google.rpc.Status' protobuf message to describe the error and any error
details.

Modifications:

- Update the fetch and generate protobuf scripts to get and build the
  relevant standard protobuf messages.
- Add `ErrorDetails` which is a container for common error types. Each
  of the error types maps to one a standard error detail from the
  detailed error model.
- Add a `GoogleRPCStatus` error which maps to the 'google.rpc.Status'
  protobuf. This also conforms to `RPCErrorConvertible` so gRPC will
  automaticlly turn it into an appropriate status and metadata.
- Add a helper for unpacking the `GoogleRPCStatus` from an `RPCError`.

Result:

Support for the detailed error mode.

---------

Co-authored-by: Gus Cairo <[email protected]>
  • Loading branch information
glbrntt and gjcairo authored Dec 17, 2024
1 parent 405d74c commit fd197ad
Show file tree
Hide file tree
Showing 20 changed files with 3,963 additions and 2 deletions.
1 change: 1 addition & 0 deletions .licenseignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Snippets/*
dev/git.commit.template
dev/version-bump.commit.template
dev/protos/local/*
dev/protos/upstream/*
.unacceptablelanguageignore
LICENSE
**/*.swift
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ let products: [Product] = [
let dependencies: [Package.Dependency] = [
.package(
url: "https://github.com/grpc/grpc-swift.git",
exact: "2.0.0-beta.1"
branch: "main"
),
.package(
url: "https://github.com/apple/swift-protobuf.git",
Expand Down Expand Up @@ -73,6 +73,7 @@ let targets: [Target] = [
dependencies: [
.target(name: "GRPCProtobuf"),
.product(name: "GRPCCore", package: "grpc-swift"),
.product(name: "GRPCInProcessTransport", package: "grpc-swift"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
],
swiftSettings: defaultSwiftSettings
Expand Down
151 changes: 151 additions & 0 deletions Sources/GRPCProtobuf/Errors/ErrorDetails+AnyPacking.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

internal import SwiftProtobuf

/// A type which can be packed and unpacked from a `Google_Protobuf_Any` message.
internal protocol GoogleProtobufAnyPackable {
static var typeURL: String { get }

/// Pack the value into a `Google_Protobuf_Any`, if possible.
func pack() throws -> Google_Protobuf_Any

/// Unpack the value from a `Google_Protobuf_Any`, if possible.
init?(unpacking any: Google_Protobuf_Any) throws
}

/// A type which is backed by a Protobuf message.
///
/// This is a convenience protocol to allow for automatic packing/unpacking of messages
/// where possible.
internal protocol ProtobufBacked {
associatedtype Message: SwiftProtobuf.Message
var storage: Message { get set }
init(storage: Message)
}

extension GoogleProtobufAnyPackable where Self: ProtobufBacked {
func pack() throws -> Google_Protobuf_Any {
try .with {
$0.typeURL = Self.typeURL
$0.value = try self.storage.serializedBytes()
}
}

init?(unpacking any: Google_Protobuf_Any) throws {
guard let storage = try any.unpack(Message.self) else { return nil }
self.init(storage: storage)
}
}

extension Google_Protobuf_Any {
func unpack<Unpacked: Message>(_ as: Unpacked.Type) throws -> Unpacked? {
if self.isA(Unpacked.self) {
return try Unpacked(serializedBytes: self.value)
} else {
return nil
}
}
}

extension ErrorDetails {
// Note: this type isn't packable into an 'Any' protobuf so doesn't conform
// to 'GoogleProtobufAnyPackable' despite holding types which are packable.

func pack() throws -> Google_Protobuf_Any {
switch self.wrapped {
case .errorInfo(let info):
return try info.pack()
case .retryInfo(let info):
return try info.pack()
case .debugInfo(let info):
return try info.pack()
case .quotaFailure(let info):
return try info.pack()
case .preconditionFailure(let info):
return try info.pack()
case .badRequest(let info):
return try info.pack()
case .requestInfo(let info):
return try info.pack()
case .resourceInfo(let info):
return try info.pack()
case .help(let info):
return try info.pack()
case .localizedMessage(let info):
return try info.pack()
case .any(let any):
return any
}
}

init(unpacking any: Google_Protobuf_Any) throws {
if let unpacked = try Self.unpack(any: any) {
self = unpacked
} else {
self = .any(any)
}
}

private static func unpack(any: Google_Protobuf_Any) throws -> Self? {
switch any.typeURL {
case ErrorInfo.typeURL:
if let unpacked = try ErrorInfo(unpacking: any) {
return .errorInfo(unpacked)
}
case RetryInfo.typeURL:
if let unpacked = try RetryInfo(unpacking: any) {
return .retryInfo(unpacked)
}
case DebugInfo.typeURL:
if let unpacked = try DebugInfo(unpacking: any) {
return .debugInfo(unpacked)
}
case QuotaFailure.typeURL:
if let unpacked = try QuotaFailure(unpacking: any) {
return .quotaFailure(unpacked)
}
case PreconditionFailure.typeURL:
if let unpacked = try PreconditionFailure(unpacking: any) {
return .preconditionFailure(unpacked)
}
case BadRequest.typeURL:
if let unpacked = try BadRequest(unpacking: any) {
return .badRequest(unpacked)
}
case RequestInfo.typeURL:
if let unpacked = try RequestInfo(unpacking: any) {
return .requestInfo(unpacked)
}
case ResourceInfo.typeURL:
if let unpacked = try ResourceInfo(unpacking: any) {
return .resourceInfo(unpacked)
}
case Help.typeURL:
if let unpacked = try Help(unpacking: any) {
return .help(unpacked)
}
case LocalizedMessage.typeURL:
if let unpacked = try LocalizedMessage(unpacking: any) {
return .localizedMessage(unpacked)
}
default:
return .any(any)
}

return nil
}
}
101 changes: 101 additions & 0 deletions Sources/GRPCProtobuf/Errors/ErrorDetails+CustomStringConvertible.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

extension ErrorDetails: CustomStringConvertible {
public var description: String {
switch self.wrapped {
case .errorInfo(let info):
return String(describing: info)
case .retryInfo(let info):
return String(describing: info)
case .debugInfo(let info):
return String(describing: info)
case .quotaFailure(let info):
return String(describing: info)
case .preconditionFailure(let info):
return String(describing: info)
case .badRequest(let info):
return String(describing: info)
case .requestInfo(let info):
return String(describing: info)
case .resourceInfo(let info):
return String(describing: info)
case .help(let info):
return String(describing: info)
case .localizedMessage(let info):
return String(describing: info)
case .any(let any):
return String(describing: any)
}
}
}

// Some errors use protobuf messages as their storage so the default description isn't
// representative

extension ErrorDetails.ErrorInfo: CustomStringConvertible {
public var description: String {
"\(Self.self)(reason: \"\(self.reason)\", domain: \"\(self.domain)\", metadata: \(self.metadata))"
}
}

extension ErrorDetails.DebugInfo: CustomStringConvertible {
public var description: String {
"\(Self.self)(stack: \(self.stack), detail: \"\(self.detail)\")"
}
}

extension ErrorDetails.QuotaFailure.Violation: CustomStringConvertible {
public var description: String {
"\(Self.self)(subject: \"\(self.subject)\", violationDescription: \"\(self.violationDescription)\")"
}
}

extension ErrorDetails.PreconditionFailure.Violation: CustomStringConvertible {
public var description: String {
"\(Self.self)(subject: \"\(self.subject)\", type: \"\(self.type)\", violationDescription: \"\(self.violationDescription)\")"
}
}

extension ErrorDetails.BadRequest.FieldViolation: CustomStringConvertible {
public var description: String {
"\(Self.self)(field: \"\(self.field)\", violationDescription: \"\(self.violationDescription)\")"
}
}

extension ErrorDetails.RequestInfo: CustomStringConvertible {
public var description: String {
"\(Self.self)(requestID: \"\(self.requestID)\", servingData: \"\(self.servingData)\")"
}
}

extension ErrorDetails.ResourceInfo: CustomStringConvertible {
public var description: String {
"\(Self.self)(name: \"\(self.name)\", owner: \"\(self.owner)\", type: \"\(self.type)\", errorDescription: \"\(self.errorDescription)\")"
}
}

extension ErrorDetails.Help.Link: CustomStringConvertible {
public var description: String {
"\(Self.self)(url: \"\(self.url)\", linkDescription: \"\(self.linkDescription)\")"
}
}

extension ErrorDetails.LocalizedMessage: CustomStringConvertible {
public var description: String {
"\(Self.self)(locale: \"\(self.locale)\", message: \"\(self.message)\")"
}
}
Loading

0 comments on commit fd197ad

Please sign in to comment.