Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(api): add collection type casting in swift 5.7 #3602

Merged
merged 3 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,31 @@ fileprivate func toAPIError<R: Decodable>(_ errors: [Error], type: R.Type) -> AP
(hasAuthorizationError ? ": \(APIError.UnauthorizedMessageString)" : "")
}

#if swift(<5.8)
if let errors = errors.cast(to: AppSyncRealTimeRequest.Error.self) {
let hasAuthorizationError = errors.contains(where: { $0 == .unauthorized})
return APIError.operationError(
errorDescription(hasAuthorizationError),
"",
errors.first
)
} else if let errors = errors.cast(to: GraphQLError.self) {
let hasAuthorizationError = errors.map(\.extensions)
.compactMap { $0.flatMap { $0["errorType"]?.stringValue } }
.contains(where: { AppSyncErrorType($0) == .unauthorized })
return APIError.operationError(
errorDescription(hasAuthorizationError),
"",
GraphQLResponseError<R>.error(errors)
)
} else {
return APIError.operationError(
errorDescription(),
"",
errors.first
)
}
#else
Comment on lines +381 to +405
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like you can probably reduce this once more

    let hasAuthorizationError: Bool
    let underlyingError: Error?
#if swift(<5.8)
    if let errors = errors.cast(to: AppSyncRealTimeRequest.Error.self) {
        hasAuthorizationError = errors.contains(where: { $0 == .unauthorized})
        underlyingError = errors.first
    } else if let errors = errors.cast(to: GraphQLError.self) {
        hasAuthorizationError = errors.map(\.extensions)
            .compactMap { $0.flatMap { $0["errorType"]?.stringValue } }
            .contains(where: { AppSyncErrorType($0) == .unauthorized })
        underlyingError = GraphQLResponseError<R>.error(errors)
    } else {
        hasAuthorizationError = false
        underlyingError = errors.first
    }
#else
    if let errors = errors as? [AppSyncRealTimeRequest.Error] {
        hasAuthorizationError = errors.contains(where: { $0 == .unauthorized})
        underlyingError = errors.first
    } else if let errors = errors as? [GraphQLError] {
        hasAuthorizationError = errors.map(\.extensions)
            .compactMap { $0.flatMap { $0["errorType"]?.stringValue } }
            .contains(where: { AppSyncErrorType($0) == .unauthorized })
        underlyingError = errors.first
    } else {
        hasAuthorizationError = false
        underlyingError = errors.first
    }
#endif
    return APIError.operationError(
        errorDescription(hasAuthorizationError),
        "",
        underlyingError
    )

Or even further, just use cast everywhere. This would avoid having to duplicate business logic while maintaining the Collection type casting in one place. Once we support 5.8 and up, then we can replace usages of cast with as

func cast<T>(to type: T.Type) -> [T]? {
        #if swift(<5.8)
        self.reduce([]) { partialResult, ele in
            if let partialResult, let ele = ele as? T {
                return partialResult + [ele]
            }
            return nil
        }
        #else
        return self as? [T]
        #endif
    }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of having the compiler directive in the cast function instead

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including the directive in the cast func will make unit testing harder. Once we upgrade the Swift tool version to 5.8, removing the code within the annotation will become straightforward.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it make unit testing harder? We don't need to unit-test that we support both 5.7 and 5.8

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted with @5d, the CI set up will cause cast to be unit tested against Swift 5.9 so it won't reach the code path. We'd have to refactor the unit test to test against something like:

func cast<T>(to type: T.Type) -> [T]? {
        #if swift(<5.8)
        return castSwift5_7() // update unit tests to test this function instead
        #else
        return self as? [T]
        #endif
    }

We to upgrade to min 5.8 soon so as Di mentioned it will be easier to remove the code later in the current PR

switch errors {
case let errors as [AppSyncRealTimeRequest.Error]:
let hasAuthorizationError = errors.contains(where: { $0 == .unauthorized})
Expand All @@ -402,5 +427,5 @@ fileprivate func toAPIError<R: Decodable>(_ errors: [Error], type: R.Type) -> AP
errors.first
)
}

#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//


import Foundation

@_spi(AmplifyAPI)
extension Array where Element == Error {
func cast<T>(to type: T.Type) -> [T]? {
self.reduce([]) { partialResult, ele in
if let partialResult, let ele = ele as? T {
return partialResult + [ele]
}
return nil
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//


import XCTest
@testable @_spi(AmplifyAPI) import AWSAPIPlugin

class ArrayWithErrorElementExtensionTests: XCTestCase {

/**
Given: errors with generic protocol type
When: cast to the correct underlying concrete type
Then: successfully casted to underlying concrete type
*/
func testCast_toCorrectErrorType_returnCastedErrorType() {
let errors: [Error] = [
Error1(), Error1(), Error1()
]

let error1s = errors.cast(to: Error1.self)
XCTAssertNotNil(error1s)
XCTAssertTrue(!error1s!.isEmpty)
XCTAssertEqual(errors.count, error1s!.count)
}

/**
Given: errors with generic protocol type
When: cast to the wong underlying concrete type
Then: return nil
*/
func testCast_toWrongErrorType_returnNil() {
let errors: [Error] = [
Error1(), Error1(), Error1()
]

let error2s = errors.cast(to: Error2.self)
XCTAssertNil(error2s)
}

/**
Given: errors with generic protocol type
When: some of the elements failed to cast to the underlying concrete type
Then: return nil
*/

func testCast_partiallyToWrongErrorType_returnNil() {
let errors: [Error] = [
Error2(), Error2(), Error1()
]

let error2s = errors.cast(to: Error2.self)
XCTAssertNil(error2s)
}

struct Error1: Error { }

struct Error2: Error { }
}
Loading