Skip to content

Commit 5f80782

Browse files
Partial Errors Implementation (#42)
Co-authored-by: Aashish <[email protected]> Co-authored-by: Aashish Patil <[email protected]>
1 parent 804fece commit 5f80782

File tree

16 files changed

+922
-124
lines changed

16 files changed

+922
-124
lines changed

Sources/DataConnectError.swift

Lines changed: 241 additions & 17 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,27 +14,251 @@
1414

1515
import Foundation
1616

17-
/// Represents an error returned by the DataConnect service
17+
// MARK: - Base Error Definitions
18+
19+
/// A type representing an error returned by the DataConnect service
20+
///
21+
/// - SeeAlso: ``DataConnectError`` for the base error type.
22+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
23+
public protocol DataConnectError: Error, CustomDebugStringConvertible, CustomStringConvertible {
24+
var message: String? { get }
25+
var underlyingError: Error? { get }
26+
}
27+
28+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
29+
public extension DataConnectError {
30+
var debugDescription: String {
31+
return "{\(Self.self), message: \(message ?? "nil"), underlyingError: \(String(describing: underlyingError))}"
32+
}
33+
34+
var description: String {
35+
return debugDescription
36+
}
37+
}
38+
39+
/// A structure representing a type-erased ``DataConnectError``.
1840
@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
41+
public struct AnyDataConnectError: Error {
42+
/// Contained ``DataConnectError``
43+
public let dataConnectError: DataConnectError
44+
45+
init<E: DataConnectError>(dataConnectError: E) {
46+
self.dataConnectError = dataConnectError
47+
}
48+
}
49+
50+
/// A type that represents an error domain with granular error codes.
51+
/// - SeeAlso: ``DataConnectError`` for the base error type.
52+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
53+
public protocol DataConnectDomainError: DataConnectError {
54+
associatedtype ErrorCode: DataConnectErrorCode
55+
var code: ErrorCode { get }
56+
}
57+
58+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
59+
public extension DataConnectDomainError {
60+
var debugDescription: String {
61+
return "{\(Self.self), code: \(code), message: \(message ?? "nil"), underlyingError: \(String(describing: underlyingError))}"
62+
}
63+
64+
var description: String {
65+
return debugDescription
66+
}
67+
}
68+
69+
/// A type that represents an error code within an error domain.
70+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
71+
public protocol DataConnectErrorCode: CustomStringConvertible, Equatable, Sendable, CaseIterable {}
72+
73+
// MARK: - Data Connect Initialization Errors
74+
75+
/// An error that occurs during the initialization of the Data Connect service.
76+
///
77+
/// This error can arise due to various reasons, such as missing configurations or
78+
/// issues with the underlying gRPC setup. It provides specific error codes
79+
/// to pinpoint the cause of the initialization failure.
80+
///
81+
/// - SeeAlso: ``DataConnectDomainError`` for the base error type.
82+
public struct DataConnectInitError: DataConnectDomainError {
83+
public struct Code: DataConnectErrorCode {
84+
private let code: String
85+
private init(_ code: String) { self.code = code }
86+
87+
public static let appNotConfigured = Code("appNotConfigured")
88+
public static let grpcNotConfigured = Code("grpcNotConfigured")
89+
90+
public static var allCases: [DataConnectInitError.Code] {
91+
return [appNotConfigured, grpcNotConfigured]
92+
}
93+
94+
public var description: String { return code }
95+
}
96+
97+
public let code: Code
98+
99+
public let message: String?
100+
101+
public let underlyingError: Error?
102+
103+
private init(code: Code, message: String? = nil, cause: Error? = nil) {
104+
self.code = code
105+
underlyingError = cause
106+
self.message = message
107+
}
108+
109+
static func appNotConfigured(message: String? = nil,
110+
cause: Error? = nil) -> DataConnectInitError {
111+
return DataConnectInitError(code: .appNotConfigured, message: message, cause: cause)
112+
}
113+
114+
static func grpcNotConfigured(message: String? = nil,
115+
cause: Error? = nil) -> DataConnectInitError {
116+
return DataConnectInitError(code: .grpcNotConfigured, message: message, cause: cause)
117+
}
118+
}
119+
120+
// MARK: - Data Codec Errors
121+
122+
/// An error that occurs during the encoding or decoding of data within the Data Connect service.
123+
///
124+
/// This error can arise due to various reasons, such as invalid data formats,
125+
/// incorrect UUIDs, or issues with timestamp/date formats. It provides specific error codes
126+
/// to pinpoint the cause of the encoding/decoding failure.
127+
///
128+
/// - SeeAlso: ``DataConnectDomainError`` for the base error type.
129+
public struct DataConnectCodecError: DataConnectDomainError {
130+
public struct Code: DataConnectErrorCode {
131+
private let code: String
132+
133+
private init(_ code: String) { self.code = code }
134+
135+
public static let encodingFailed = Code("encodingFailed")
136+
public static let decodingFailed = Code("decodingFailed")
137+
public static let invalidUUID = Code("invalidUUID")
138+
public static let invalidTimestampFormat = Code("invalidTimestampFormat")
139+
public static let invalidLocalDateFormat = Code("invalidLocalDateFormat")
140+
141+
public static var allCases: [DataConnectCodecError.Code] {
142+
return [
143+
encodingFailed,
144+
decodingFailed,
145+
invalidUUID,
146+
invalidTimestampFormat,
147+
invalidLocalDateFormat,
148+
]
149+
}
150+
151+
public var description: String { return code }
152+
}
153+
154+
public let code: Code
155+
156+
public let message: String?
157+
158+
public let underlyingError: (any Error)?
159+
160+
private init(code: Code, message: String? = nil, cause: Error? = nil) {
161+
self.code = code
162+
self.message = message
163+
underlyingError = cause
164+
}
165+
166+
static func encodingFailed(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
167+
return DataConnectCodecError(code: .encodingFailed, message: message, cause: cause)
168+
}
169+
170+
static func decodingFailed(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
171+
return DataConnectCodecError(code: .decodingFailed, message: message, cause: cause)
172+
}
173+
174+
static func invalidUUID(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
175+
return DataConnectCodecError(code: .invalidUUID, message: message, cause: cause)
176+
}
177+
178+
static func invalidTimestampFormat(message: String? = nil,
179+
cause: Error? = nil) -> DataConnectCodecError {
180+
return DataConnectCodecError(code: .invalidTimestampFormat, message: message, cause: cause)
181+
}
182+
183+
static func invalidLocalDateFormat(message: String? = nil,
184+
cause: Error? = nil) -> DataConnectCodecError {
185+
return DataConnectCodecError(code: .invalidLocalDateFormat, message: message, cause: cause)
186+
}
187+
}
188+
189+
// MARK: - Operation Execution Error including Partial Errors
190+
191+
/// An error that occurs during the execution of a Data Connect operation.
192+
///
193+
/// This error can arise due to various reasons, such as network issues, server-side errors,
194+
/// or problems with the operation itself. It may include an optional underlying error,
195+
/// a message describing the failure, and an optional response from the failed operation.
196+
///
197+
/// When available, the ``response`` will contain more error information and any partially decoded
198+
/// result
199+
///
200+
/// - SeeAlso: `DataConnectError` for the base error type.
201+
///
202+
203+
public struct DataConnectOperationError: DataConnectError {
204+
public let message: String?
205+
public let underlyingError: (any Error)?
206+
public let response: OperationFailureResponse?
207+
208+
private init(message: String? = nil, cause: Error? = nil,
209+
response: OperationFailureResponse? = nil) {
210+
self.response = response
211+
underlyingError = cause
212+
self.message = message
213+
}
214+
215+
static func executionFailed(message: String? = nil, cause: Error? = nil,
216+
response: OperationFailureResponse? = nil)
217+
-> DataConnectOperationError {
218+
return DataConnectOperationError(message: message, cause: cause, response: response)
219+
}
220+
}
22221

23-
/// failed to configure gRPC
24-
case grpcNotConfigured
222+
/// Contains the data and errors sent to us from the backend in its response.
223+
/// The ``OperationFailureResponse`` if present, is available as part of the
224+
/// ``DataConnectOperationError``
225+
public struct OperationFailureResponse: Sendable {
226+
/// JSON string whose value is the "data" property provided by the backend in
227+
/// its response payload; may be `nil` if the "data" property was not provided
228+
/// in the backend response and/or was `null` in the backend response.
229+
public let rawJsonData: String?
25230

26-
/// failed to decode results from server
27-
case decodeFailed
231+
/// The list of errors in the "error" property provided by the backend in
232+
/// its response payload; may be empty if the "errors" property was not
233+
/// provided in the backend response and/or was an empty list in the backend response.
234+
public let errors: [ErrorInfo]
28235

29-
/// Invalid uuid format during encoding / decoding of data
30-
case invalidUUID
236+
// (Partially) decoded data
237+
private let data: Sendable?
31238

32-
/// date components specified to initialize LocalDate are invalid
33-
case invalidLocalDateFormat
239+
/// Returns `rawJsonData` string decoded into the given type, if decoding was
240+
/// successful when the operation was executed.
241+
///
242+
/// - Parameter asType: The type to decode the `rawJsonData` into (defaults to the inferred
243+
/// generic parameter).
244+
/// - Returns: The decoded data of type `Data` (generic parameter), if decoding to the
245+
/// generic parameter was successful when the operation was executed, `nil` otherwise.
246+
public func data<Data: Decodable>(asType: Data.Type = Data.self) -> Data? {
247+
return data as? Data
248+
}
34249

35-
/// timestamp components specified to initialize Timestamp are invalid
36-
case invalidTimestampFormat
250+
init(rawJsonData: String? = nil,
251+
errors: [ErrorInfo],
252+
data: Sendable?) {
253+
self.rawJsonData = rawJsonData
254+
self.errors = errors
255+
self.data = data
256+
}
37257

38-
/// generic operation execution error
39-
case operationExecutionFailed(messages: String?)
258+
public struct ErrorInfo: Codable, Sendable {
259+
/// The error message (if available)
260+
public let message: String
261+
/// The path to the field to which this error applies.
262+
public let path: [DataConnectPathSegment]
263+
}
40264
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
16+
public enum DataConnectPathSegment: Codable, Equatable, Sendable {
17+
case field(String)
18+
case listIndex(Int)
19+
}
20+
21+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
22+
public extension DataConnectPathSegment {
23+
init(from decoder: any Decoder) throws {
24+
let container = try decoder.singleValueContainer()
25+
26+
do {
27+
let field = try container.decode(String.self)
28+
self = .field(field)
29+
} catch {
30+
let index = try container.decode(Int.self)
31+
self = .listIndex(index)
32+
}
33+
}
34+
35+
func encode(to encoder: any Encoder) throws {
36+
var container = encoder.singleValueContainer()
37+
switch self {
38+
case let .field(fieldVal):
39+
try container.encode(fieldVal)
40+
case let .listIndex(indexVal):
41+
try container.encode(indexVal)
42+
}
43+
}
44+
}

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.decodeFailed
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)