|
1 | | -// Copyright 2024 Google LLC |
| 1 | +// Copyright 2025 Google LLC |
2 | 2 | // |
3 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | 4 | // you may not use this file except in compliance with the License. |
|
14 | 14 |
|
15 | 15 | import Foundation |
16 | 16 |
|
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 | + |
18 | 26 | @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 | +} |
22 | 36 |
|
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 { |
25 | 40 |
|
26 | | - /// Invalid uuid format during encoding / decoding of data |
27 | | - case invalidUUID |
| 41 | + private let dataConnectError: DataConnectError |
28 | 42 |
|
29 | | - /// date components specified to initialize LocalDate are invalid |
30 | | - case invalidLocalDateFormat |
| 43 | + init<E: DataConnectError>(dataConnectError: E) { |
| 44 | + self.dataConnectError = dataConnectError |
| 45 | + } |
31 | 46 |
|
32 | | - /// timestamp components specified to initialize Timestamp are invalid |
33 | | - case invalidTimestampFormat |
| 47 | + public var message: String? { |
| 48 | + return dataConnectError.message |
| 49 | + } |
34 | 50 |
|
35 | | - /// generic operation execution error |
36 | | - case operationExecutionFailed(messages: String?, response: OperationFailureResponse) |
| 51 | + public var cause: (any Error)? { |
| 52 | + return dataConnectError.cause |
| 53 | + } |
37 | 54 | } |
38 | 55 |
|
39 | | -// The data and errors sent to us from the backend in its response. |
40 | 56 | @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 | + |
60 | 207 | } |
61 | 208 |
|
62 | | -struct OperationFailureResponseImpl : OperationFailureResponse { |
63 | | - public let jsonData: String? |
64 | 209 |
|
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? |
66 | 226 |
|
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 | + } |
69 | 260 | } |
| 261 | + |
70 | 262 | } |
71 | 263 |
|
72 | 264 | // Information about an error provided by the backend in its response. |
73 | 265 | @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 |
77 | 266 |
|
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() |
80 | 271 |
|
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 | + } |
85 | 279 | } |
86 | | -} |
87 | 280 |
|
| 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 | +} |
0 commit comments