Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
080185c
Sources/DataConnectError.swift: add classes to support partial errors.
dconeybe Feb 27, 2025
d5b1f19
Update DataConnectError.swift
aashishpatil-g Feb 28, 2025
3f277ce
DataConnectError.swift: minor tweaks from offline discussion
dconeybe Mar 3, 2025
c3cef5a
DataConnectError.swift: remove message string parameter
dconeybe Mar 3, 2025
3a03ef9
DataConnectError.swift: move OperationFailureResponse.ErrorInfo to to…
dconeybe Mar 3, 2025
23d76fb
Merge remote-tracking branch 'origin/main' into PartialErrors
dconeybe Mar 14, 2025
ad7cc64
DataConnectError.swift: update to match api proposal
dconeybe Mar 14, 2025
477e464
get it to build
dconeybe Mar 14, 2025
7ab5f83
PartialErrorsDemo.swift added
dconeybe Mar 14, 2025
f55305a
PartialErrorsDemo.swift: add usage of decodedData() to demo its use.
dconeybe Mar 14, 2025
98faec4
Updates matching API review and Refactored Errors
aashishpatil-g Mar 21, 2025
e29553c
Style Fixes
aashishpatil-g Mar 21, 2025
c763b2d
Remove whitespace from gql file
aashishpatil-g Mar 21, 2025
2a52e58
Re-add copyright notice to generated code
aashishpatil-g Mar 24, 2025
255d8dd
Unnest PathSegment definition
aashishpatil-g Mar 24, 2025
a763f9c
Rename PathSegment to DataConnectPathSegment to match other platforms
aashishpatil-g Mar 24, 2025
cd2cc5a
Cleanup post refactor (removed old `kind` var)
aashishpatil-g Mar 25, 2025
0e717a9
Convert vars to lets
aashishpatil-g Mar 25, 2025
8ddbe53
Change `cause` to `underlyingError`
aashishpatil-g Mar 25, 2025
72c1ac1
Make boxed error accessible
aashishpatil-g Mar 26, 2025
4d78594
Add Unit test to confirm domain error carries through the boxed type
aashishpatil-g Mar 26, 2025
95fc74e
Style checks
aashishpatil-g Mar 26, 2025
8bc19e7
Remove DataConnectError conformance
aashishpatil-g Mar 26, 2025
20d34b1
Nick feedback
aashishpatil-g Mar 27, 2025
92c1aec
fix copyright year
aashishpatil-g Mar 27, 2025
0fc0b63
Comment feedback fix
aashishpatil-g Mar 27, 2025
1c41f41
Documentation fixes.
aashishpatil-g Mar 27, 2025
0aafe10
More documentation fixes
aashishpatil-g Mar 27, 2025
c294d70
Yet more doc fixes
aashishpatil-g Mar 27, 2025
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
233 changes: 216 additions & 17 deletions Sources/DataConnectError.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 Google LLC
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -14,27 +14,226 @@

import Foundation

/// Represents an error returned by the DataConnect service
// MARK: Base Error Definitions

/// Protocol representing an error returned by the DataConnect service
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public protocol DataConnectError: Error, CustomDebugStringConvertible, CustomStringConvertible {
var message: String? { get }
var underlyingError: Error? { get }
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public extension DataConnectError {
var debugDescription: String {
return "{\(Self.self), message: \(message ?? "nil"), cause: \(String(describing: underlyingError))}"
}

var description: String {
return debugDescription
}
}

/// Type erased DataConnectError
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public struct AnyDataConnectError: Error {
public let dataConnectError: DataConnectError

init<E: DataConnectError>(dataConnectError: E) {
self.dataConnectError = dataConnectError
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
/// Represents an error domain which can have more granular error codes
public protocol DataConnectDomainError: DataConnectError {
associatedtype ErrorCode: DataConnectErrorCode
var code: ErrorCode { get }
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public enum DataConnectError: Error {
/// no firebase app specified. configure not complete
case appNotConfigured
public extension DataConnectDomainError {
var debugDescription: String {
return "{\(Self.self), code: \(code), message: \(message ?? "nil"), cause: \(String(describing: underlyingError))}"
}

var description: String {
return debugDescription
}
}

/// Error code within an error domain
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public protocol DataConnectErrorCode: CustomStringConvertible, Equatable, Sendable, CaseIterable {}

// MARK: Data Connect Initialization Errors

/// Error initializing Data Connect
public struct DataConnectInitError: DataConnectDomainError {
public struct Code: DataConnectErrorCode {
private let code: String
private init(_ code: String) { self.code = code }

public static let appNotConfigured = Code("appNotConfigured")
public static let grpcNotConfigured = Code("grpcNotConfigured")

public static var allCases: [DataConnectInitError.Code] {
return [appNotConfigured, grpcNotConfigured]
}

public var description: String { return code }
}

public let code: Code

public let message: String?

public let underlyingError: Error?

private init(code: Code, message: String? = nil, cause: Error? = nil) {
self.code = code
underlyingError = cause
self.message = message
}

static func appNotConfigured(message: String? = nil,
cause: Error? = nil) -> DataConnectInitError {
return DataConnectInitError(code: .appNotConfigured, message: message, cause: cause)
}

static func grpcNotConfigured(message: String? = nil,
cause: Error? = nil) -> DataConnectInitError {
return DataConnectInitError(code: .grpcNotConfigured, message: message, cause: cause)
}
}

// MARK: Data Codec Errors

/// Data Encoding / Decoding Error
public struct DataConnectCodecError: DataConnectDomainError {
public struct Code: DataConnectErrorCode {
private let code: String

private init(_ code: String) { self.code = code }

public static let encodingFailed = Code("encodingFailed")
public static let decodingFailed = Code("decodingFailed")
public static let invalidUUID = Code("invalidUUID")
public static let invalidTimestampFormat = Code("invalidTimestampFormat")
public static let invalidLocalDateFormat = Code("invalidLocalDateFormat")

public static var allCases: [DataConnectCodecError.Code] {
return [
encodingFailed,
decodingFailed,
invalidUUID,
invalidTimestampFormat,
invalidLocalDateFormat,
]
}

public var description: String { return code }
}

public let code: Code

public let message: String?

public let underlyingError: (any Error)?

private init(code: Code, message: String? = nil, cause: Error? = nil) {
self.code = code
self.message = message
underlyingError = cause
}

static func encodingFailed(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
return DataConnectCodecError(code: .encodingFailed, message: message, cause: cause)
}

static func decodingFailed(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
return DataConnectCodecError(code: .decodingFailed, message: message, cause: cause)
}

static func invalidUUID(message: String? = nil, cause: Error? = nil) -> DataConnectCodecError {
return DataConnectCodecError(code: .invalidUUID, message: message, cause: cause)
}

static func invalidTimestampFormat(message: String? = nil,
cause: Error? = nil) -> DataConnectCodecError {
return DataConnectCodecError(code: .invalidTimestampFormat, message: message, cause: cause)
}

static func invalidLocalDateFormat(message: String? = nil,
cause: Error? = nil) -> DataConnectCodecError {
return DataConnectCodecError(code: .invalidLocalDateFormat, message: message, cause: cause)
}
}

// MARK: Operation Execution Error including Partial Errors

/// Data Connect Operation Failed
public struct DataConnectOperationError: DataConnectError {
public let message: String?
public let underlyingError: (any Error)?
public let response: OperationFailureResponse?

private init(message: String? = nil, cause: Error? = nil,
response: OperationFailureResponse? = nil) {
self.response = response
underlyingError = cause
self.message = message
}

static func executionFailed(message: String? = nil, cause: Error? = nil,
response: OperationFailureResponse? = nil)
-> DataConnectOperationError {
return DataConnectOperationError(message: message, cause: cause, response: response)
}
}

/// failed to configure gRPC
case grpcNotConfigured
// The data and errors sent to us from the backend in its response.
// New struct, that contains the data and errors sent to us
// from the backend in its response.
public struct OperationFailureResponse: Sendable {
// JSON string whose value is the "data" property provided by the backend in
// its response payload; may be `nil` if the "data" property was not provided
// in the backend response and/or was `null` in the backend response.
public let rawJsonData: String?

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

/// Invalid uuid format during encoding / decoding of data
case invalidUUID
// (Partially) decoded data
private let data: Sendable?

/// date components specified to initialize LocalDate are invalid
case invalidLocalDateFormat
// Returns `jsonData` string decoded into the given type, if decoding was
// successful when the operation was executed. Returns `nil` if `jsonData`
// is `nil`, if `jsonData` was _not_ able to be decoded when the operation
// was executed, or if the given type is _not_ equal to the `Data` type that
// was used when the operation was executed.
//
// This function does _not_ do the decoding itself, but simply returns
// the decoded data, if any, that was decoded at the time of the
// operation's execution.
func data<Data: Decodable>(asType: Data.Type = Data.self) -> Data? {
return data as? Data
}

/// timestamp components specified to initialize Timestamp are invalid
case invalidTimestampFormat
init(rawJsonData: String? = nil,
errors: [ErrorInfo],
data: Sendable?) {
self.rawJsonData = rawJsonData
self.errors = errors
self.data = data
}

/// generic operation execution error
case operationExecutionFailed(messages: String?)
public struct ErrorInfo: Codable, Sendable {
// The error message.
public let message: String
// The path to the field to which this error applies.
public let path: [DataConnectPathSegment]
}
}
43 changes: 43 additions & 0 deletions Sources/DataConnectPathSegment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2025 Google LLC
//
// 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.

public enum DataConnectPathSegment: Codable, Equatable, Sendable {
case field(String)
case listIndex(Int)
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public extension DataConnectPathSegment {
init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()

do {
let field = try container.decode(String.self)
self = .field(field)
} catch {
let index = try container.decode(Int.self)
self = .listIndex(index)
}
}

func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case let .field(fieldVal):
try container.encode(fieldVal)
case let .listIndex(indexVal):
try container.encode(indexVal)
}
}
}
4 changes: 2 additions & 2 deletions Sources/Internal/CodableHelpers.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 Google LLC
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -40,7 +40,7 @@ class Int64CodableConverter: CodableConverter {
}

guard let int64Value = Int64(input) else {
throw DataConnectError.decodeFailed
throw DataConnectInitError.appNotConfigured()
}
return int64Value
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Internal/CodableTimestamp.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 Google LLC
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -55,7 +55,7 @@
DataConnectLogger.error(
"Timestamp string format \(timestampString) is not supported."
)
throw DataConnectError.invalidTimestampFormat
throw DataConnectCodecError.invalidTimestampFormat()
}

let buf: Google_Protobuf_Timestamp =
Expand All @@ -75,7 +75,7 @@

/** Extends Timestamp to conform to Codable. */
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension Timestamp: CodableTimestamp {}

Check warning on line 78 in Sources/Internal/CodableTimestamp.swift

View workflow job for this annotation

GitHub Actions / spm (macos-15, iOS, Xcode_16.2)

extension declares a conformance of imported type 'Timestamp' to imported protocols 'Encodable', 'Decodable'; this will not behave correctly if the owners of 'FirebaseCore' introduce this conformance in the future

Check warning on line 78 in Sources/Internal/CodableTimestamp.swift

View workflow job for this annotation

GitHub Actions / spm (macos-15, iOS, Xcode_16.2)

extension declares a conformance of imported type 'Timestamp' to imported protocols 'Encodable', 'Decodable'; this will not behave correctly if the owners of 'FirebaseCore' introduce this conformance in the future

Check warning on line 78 in Sources/Internal/CodableTimestamp.swift

View workflow job for this annotation

GitHub Actions / spm (macos-15, macOS, Xcode_16.2)

extension declares a conformance of imported type 'Timestamp' to imported protocols 'Encodable', 'Decodable'; this will not behave correctly if the owners of 'FirebaseCore' introduce this conformance in the future

Check warning on line 78 in Sources/Internal/CodableTimestamp.swift

View workflow job for this annotation

GitHub Actions / spm (macos-15, macOS, Xcode_16.2)

extension declares a conformance of imported type 'Timestamp' to imported protocols 'Encodable', 'Decodable'; this will not behave correctly if the owners of 'FirebaseCore' introduce this conformance in the future

Check warning on line 78 in Sources/Internal/CodableTimestamp.swift

View workflow job for this annotation

GitHub Actions / spm (macos-15, tvOS, Xcode_16.2)

extension declares a conformance of imported type 'Timestamp' to imported protocols 'Encodable', 'Decodable'; this will not behave correctly if the owners of 'FirebaseCore' introduce this conformance in the future

Check warning on line 78 in Sources/Internal/CodableTimestamp.swift

View workflow job for this annotation

GitHub Actions / spm (macos-15, tvOS, Xcode_16.2)

extension declares a conformance of imported type 'Timestamp' to imported protocols 'Encodable', 'Decodable'; this will not behave correctly if the owners of 'FirebaseCore' introduce this conformance in the future

Check warning on line 78 in Sources/Internal/CodableTimestamp.swift

View workflow job for this annotation

GitHub Actions / spm (macos-15, catalyst, Xcode_16.2)

extension declares a conformance of imported type 'Timestamp' to imported protocols 'Encodable', 'Decodable'; this will not behave correctly if the owners of 'FirebaseCore' introduce this conformance in the future

Check warning on line 78 in Sources/Internal/CodableTimestamp.swift

View workflow job for this annotation

GitHub Actions / spm (macos-15, catalyst, Xcode_16.2)

extension declares a conformance of imported type 'Timestamp' to imported protocols 'Encodable', 'Decodable'; this will not behave correctly if the owners of 'FirebaseCore' introduce this conformance in the future

class CodableTimestampHelper {
static let regex =
Expand Down
2 changes: 1 addition & 1 deletion Sources/Internal/Codec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Codec {
}

// Decode Protos to Codable
func decode<T: Decodable>(result: Google_Protobuf_Struct, asType: T.Type) throws -> T? {
func decode<T: Decodable>(result: Google_Protobuf_Struct, asType: T.Type) throws -> T {
do {
let jsonData = try result.jsonUTF8Data()
let jsonDecoder = JSONDecoder()
Expand Down
Loading
Loading