Skip to content

Commit 98faec4

Browse files
Updates matching API review and Refactored Errors
1 parent f55305a commit 98faec4

File tree

13 files changed

+570
-172
lines changed

13 files changed

+570
-172
lines changed

Sources/DataConnectError.swift

Lines changed: 253 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Google LLC
1+
// Copyright 2025 Google LLC
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -14,74 +14,277 @@
1414

1515
import Foundation
1616

17-
/// Represents an error returned by the DataConnect service
17+
// MARK: Base Error Definitions
18+
19+
/// Protocol representing an error returned by the DataConnect service
20+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
21+
public protocol DataConnectError: Error, CustomDebugStringConvertible, CustomStringConvertible {
22+
var message: String? { get }
23+
var cause: Error? { get }
24+
}
25+
1826
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
19-
public enum DataConnectError: Error {
20-
/// no firebase app specified. configure not complete
21-
case appNotConfigured
27+
public extension DataConnectError {
28+
var debugDescription: String {
29+
return "{\(Self.self), message: \(message ?? "nil"), cause: \(String(describing: cause))}"
30+
}
31+
32+
var description: String {
33+
return debugDescription
34+
}
35+
}
2236

23-
/// failed to configure gRPC
24-
case grpcNotConfigured
37+
/// Type erased DataConnectError
38+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
39+
public struct AnyDataConnectError: DataConnectError {
2540

26-
/// Invalid uuid format during encoding / decoding of data
27-
case invalidUUID
41+
private let dataConnectError: DataConnectError
2842

29-
/// date components specified to initialize LocalDate are invalid
30-
case invalidLocalDateFormat
43+
init<E: DataConnectError>(dataConnectError: E) {
44+
self.dataConnectError = dataConnectError
45+
}
3146

32-
/// timestamp components specified to initialize Timestamp are invalid
33-
case invalidTimestampFormat
47+
public var message: String? {
48+
return dataConnectError.message
49+
}
3450

35-
/// generic operation execution error
36-
case operationExecutionFailed(messages: String?, response: OperationFailureResponse)
51+
public var cause: (any Error)? {
52+
return dataConnectError.cause
53+
}
3754
}
3855

39-
// The data and errors sent to us from the backend in its response.
4056
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
41-
public protocol OperationFailureResponse {
42-
// JSON string whose value is the "data" property provided by the backend in its response
43-
// payload; may be `nil` if the "data" property was not provided in the backend response and/or
44-
// was `null` in the backend response.
45-
var jsonData: String? { get }
46-
47-
// The list of errors in the "error" property provided by the backend in its response payload;
48-
// may be empty if the "errors" property was not provided in the backend response and/or was an
49-
// empty list in the backend response.
50-
var errorInfoList: [OperationFailureResponseErrorInfo] { get }
51-
52-
// Returns `jsonData` string decoded into the given type, if decoding was successful when the
53-
// operation was executed. Returns `nil` if `jsonData` is `nil`, if `jsonData` was _not_ able to
54-
// be decoded when the operation was executed, or if the given type is _not_ equal to the `Data`
55-
// type that was used when the operation was executed.
56-
//
57-
// This function does _not_ do the decoding itself, but simply returns the decoded data, if any,
58-
// that was decoded at the time of the operation's execution.
59-
func decodedData<Data: Decodable>(asType: Data.Type) -> Data?
57+
/// Represents an error domain which can have more granular error codes
58+
public protocol DataConnectDomainError: DataConnectError {
59+
associatedtype ErrorCode: DataConnectErrorCode
60+
61+
var code: ErrorCode { get }
62+
}
63+
64+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
65+
public extension DataConnectDomainError {
66+
var debugDescription: String {
67+
return "{\(Self.self), code: \(code), message: \(message ?? "nil"), cause: \(String(describing: cause))}"
68+
}
69+
70+
var description: String {
71+
return debugDescription
72+
}
73+
}
74+
75+
76+
/// Error code within an error domain
77+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
78+
public protocol DataConnectErrorCode: CustomStringConvertible, Equatable, Sendable, CaseIterable { }
79+
80+
// MARK: Data Connect Initialization Errors
81+
82+
/// Error initializing Data Connect
83+
public struct DataConnectInitError: DataConnectDomainError {
84+
85+
public struct Code: DataConnectErrorCode {
86+
private let code: String
87+
private init(_ code: String) { self.code = code }
88+
89+
90+
public static let appNotConfigured = Code("appNotConfigured")
91+
public static let grpcNotConfigured = Code("grpcNotConfigured")
92+
93+
public static var allCases: [DataConnectInitError.Code] {
94+
return [appNotConfigured, grpcNotConfigured]
95+
}
96+
97+
public var description: String { return code }
98+
}
99+
100+
public let code: Code
101+
102+
public private(set) var message: String?
103+
104+
public private(set) var cause: Error?
105+
106+
private init(code: Code, message: String? = nil, cause: Error? = nil) {
107+
self.code = code
108+
self.cause = cause
109+
self.message = message
110+
}
111+
112+
static func appNotConfigured(message: String? = nil, cause: Error? = nil) -> DataConnectInitError {
113+
return DataConnectInitError(code: .appNotConfigured, message: message, cause: cause)
114+
}
115+
116+
static func grpcNotConfigured(message: String? = nil, cause: Error? = nil) -> DataConnectInitError {
117+
return DataConnectInitError(code: .grpcNotConfigured, message: message, cause: cause)
118+
}
119+
}
120+
121+
// MARK: Data Codec Errors
122+
123+
/// Data Encoding / Decoding Error
124+
public struct DataConnectCodecError: DataConnectDomainError {
125+
public struct Code: DataConnectErrorCode {
126+
private let code: String
127+
128+
private init(_ code: String) { self.code = code }
129+
130+
public static let encodingFailed = Code("encodingFailed")
131+
public static let decodingFailed = Code("decodingFailed")
132+
public static let invalidUUID = Code("invalidUUID")
133+
public static let invalidTimestampFormat = Code("invalidTimestampFormat")
134+
public static let invalidLocalDateFormat = Code("invalidLocalDateFormat")
135+
136+
public static var allCases: [DataConnectCodecError.Code] {
137+
return [
138+
encodingFailed,
139+
decodingFailed,
140+
invalidUUID,
141+
invalidTimestampFormat,
142+
invalidLocalDateFormat
143+
]
144+
}
145+
146+
public var description: String { return code }
147+
}
148+
149+
public let code: Code
150+
151+
public var message: String?
152+
153+
public var cause: (any Error)?
154+
155+
private init(code: Code, message: String? = nil, cause: Error? = nil) {
156+
self.code = code
157+
self.message = message
158+
self.cause = cause
159+
}
160+
161+
static func encodingFailed(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
162+
return DataConnectCodecError(code: .encodingFailed, message: message, cause: cause)
163+
}
164+
165+
static func decodingFailed(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
166+
return DataConnectCodecError(code: .decodingFailed, message: message, cause: cause)
167+
}
168+
169+
static func invalidUUID(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
170+
return DataConnectCodecError(code: .invalidUUID, message: message, cause: cause)
171+
}
172+
173+
static func invalidTimestampFormat(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
174+
return DataConnectCodecError(code: .invalidTimestampFormat, message: message, cause: cause)
175+
}
176+
177+
static func invalidLocalDateFormat(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
178+
return DataConnectCodecError(code: .invalidLocalDateFormat, message: message, cause: cause)
179+
}
180+
181+
}
182+
183+
184+
// MARK: Operation Execution Error including Partial Errors
185+
186+
/// Data Connect Operation Failed
187+
public struct DataConnectOperationError: DataConnectError {
188+
public var kind: String {
189+
return "operationError"
190+
}
191+
192+
public var message: String?
193+
194+
public var cause: (any Error)?
195+
196+
public private(set) var response: OperationFailureResponse? = nil
197+
198+
private init(message: String? = nil, cause: Error? = nil, response: OperationFailureResponse?) {
199+
self.response = response
200+
self.message = message
201+
}
202+
203+
static func executionFailed(message: String? = nil, cause: Error? = nil, response: OperationFailureResponse? = nil) -> DataConnectOperationError {
204+
return DataConnectOperationError(message: message, cause: cause, response: response)
205+
}
206+
60207
}
61208

62-
struct OperationFailureResponseImpl : OperationFailureResponse {
63-
public let jsonData: String?
64209

65-
public let errorInfoList: [OperationFailureResponseErrorInfo]
210+
// The data and errors sent to us from the backend in its response.
211+
// New struct, that contains the data and errors sent to us
212+
// from the backend in its response.
213+
public struct OperationFailureResponse : Sendable {
214+
// JSON string whose value is the "data" property provided by the backend in
215+
// its response payload; may be `nil` if the "data" property was not provided
216+
// in the backend response and/or was `null` in the backend response.
217+
public private(set) var rawJsonData: String?
218+
219+
// The list of errors in the "error" property provided by the backend in
220+
// its response payload; may be empty if the "errors" property was not
221+
// provided in the backend response and/or was an empty list in the backend response.
222+
public private(set) var errors: [ErrorInfo]
223+
224+
// (Partially) decoded data
225+
private let data: Sendable?
66226

67-
func decodedData<Data: Decodable>(asType: Data.Type = Data.self) -> Data? {
68-
return nil;
227+
// Returns `jsonData` string decoded into the given type, if decoding was
228+
// successful when the operation was executed. Returns `nil` if `jsonData`
229+
// is `nil`, if `jsonData` was _not_ able to be decoded when the operation
230+
// was executed, or if the given type is _not_ equal to the `Data` type that
231+
// was used when the operation was executed.
232+
//
233+
// This function does _not_ do the decoding itself, but simply returns
234+
// the decoded data, if any, that was decoded at the time of the
235+
// operation's execution.
236+
func data<Data: Decodable>(asType: Data.Type = Data.self) -> Data? {
237+
return data as? Data
238+
}
239+
240+
internal init(
241+
rawJsonData: String? = nil,
242+
errors: [ErrorInfo],
243+
data: Sendable?
244+
) {
245+
self.rawJsonData = rawJsonData
246+
self.errors = errors
247+
self.data = data
248+
}
249+
250+
public struct ErrorInfo: Codable, Sendable {
251+
// The error message.
252+
public let message: String
253+
// The path to the field to which this error applies.
254+
public let path: [PathSegment]
255+
256+
public enum PathSegment: Codable, Equatable, Sendable {
257+
case field(String)
258+
case listIndex(Int)
259+
}
69260
}
261+
70262
}
71263

72264
// Information about an error provided by the backend in its response.
73265
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
74-
public struct OperationFailureResponseErrorInfo: Codable {
75-
// The error message.
76-
public let message: String
77266

78-
// The path to the field to which this error applies.
79-
public let path: [PathSegment]
267+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
268+
public extension OperationFailureResponse.ErrorInfo.PathSegment {
269+
init(from decoder: any Decoder) throws {
270+
let container = try decoder.singleValueContainer()
80271

81-
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
82-
public enum PathSegment: Codable, Equatable {
83-
case field(String)
84-
case listIndex(Int)
272+
do {
273+
let field = try container.decode(String.self)
274+
self = .field(field)
275+
} catch {
276+
let index = try container.decode(Int.self)
277+
self = .listIndex(index)
278+
}
85279
}
86-
}
87280

281+
func encode(to encoder: any Encoder) throws {
282+
var container = encoder.singleValueContainer()
283+
switch self {
284+
case .field(let fieldVal):
285+
try container.encode(fieldVal)
286+
case .listIndex(let indexVal):
287+
try container.encode(indexVal)
288+
}
289+
}
290+
}

Sources/Internal/CodableHelpers.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Google LLC
1+
// Copyright 2025 Google LLC
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -40,7 +40,7 @@ class Int64CodableConverter: CodableConverter {
4040
}
4141

4242
guard let int64Value = Int64(input) else {
43-
throw DataConnectError.appNotConfigured
43+
throw DataConnectInitError.appNotConfigured()
4444
}
4545
return int64Value
4646
}

Sources/Internal/CodableTimestamp.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Google LLC
1+
// Copyright 2025 Google LLC
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -55,7 +55,7 @@ extension CodableTimestamp {
5555
DataConnectLogger.error(
5656
"Timestamp string format \(timestampString) is not supported."
5757
)
58-
throw DataConnectError.invalidTimestampFormat
58+
throw DataConnectCodecError.invalidTimestampFormat()
5959
}
6060

6161
let buf: Google_Protobuf_Timestamp =

Sources/Internal/Codec.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Codec {
3535
}
3636

3737
// Decode Protos to Codable
38-
func decode<T: Decodable>(result: Google_Protobuf_Struct, asType: T.Type) throws -> T? {
38+
func decode<T: Decodable>(result: Google_Protobuf_Struct, asType: T.Type) throws -> T {
3939
do {
4040
let jsonData = try result.jsonUTF8Data()
4141
let jsonDecoder = JSONDecoder()

0 commit comments

Comments
 (0)