Skip to content

Commit

Permalink
fix(core): add Foundation HTTP client for watchOS / tvOS (#3230)
Browse files Browse the repository at this point in the history
  • Loading branch information
atierian authored Sep 21, 2023
1 parent 999a93e commit 6fa88d4
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import AWSCognitoIdentity
import AWSCognitoIdentityProvider
import AWSPluginsCore
import ClientRuntime
@_spi(FoundationClientEngine) import AWSPluginsCore

extension AWSCognitoAuthPlugin {

Expand Down Expand Up @@ -92,9 +93,23 @@ extension AWSCognitoAuthPlugin {
)

if var httpClientEngineProxy = httpClientEngineProxy {
let sdkEngine = configuration.httpClientEngine
httpClientEngineProxy.target = sdkEngine
let httpClientEngine: HttpClientEngine
#if os(iOS) || os(macOS)
// networking goes through CRT
httpClientEngine = configuration.httpClientEngine
#else
// networking goes through Foundation
httpClientEngine = FoundationClientEngine()
#endif
httpClientEngineProxy.target = httpClientEngine
configuration.httpClientEngine = httpClientEngineProxy
} else {
#if os(iOS) || os(macOS) // no-op
#else
// For any platform except iOS or macOS
// Use Foundation instead of CRT for networking.
configuration.httpClientEngine = FoundationClientEngine()
#endif
}

return CognitoIdentityProviderClient(config: configuration)
Expand All @@ -110,6 +125,14 @@ extension AWSCognitoAuthPlugin {
frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(),
region: identityPoolConfig.region
)

#if os(iOS) || os(macOS) // no-op
#else
// For any platform except iOS or macOS
// Use Foundation instead of CRT for networking.
configuration.httpClientEngine = FoundationClientEngine()
#endif

return CognitoIdentityClient(config: configuration)
default:
fatalError()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import ClientRuntime

extension Foundation.URLRequest {
init(sdkRequest: ClientRuntime.SdkHttpRequest) throws {
guard let url = sdkRequest.endpoint.url else {
throw FoundationClientEngineError.invalidRequestURL(sdkRequest: sdkRequest)
}
self.init(url: url)
httpMethod = sdkRequest.method.rawValue

for header in sdkRequest.headers.headers {
for value in header.value {
addValue(value, forHTTPHeaderField: header.name)
}
}

switch sdkRequest.body {
case .data(let data): httpBody = data
case .stream(let stream): httpBody = stream.toBytes().getData()
case .none: break
}
}
}

extension ClientRuntime.HttpResponse {
private static func headers(
from allHeaderFields: [AnyHashable: Any]
) -> ClientRuntime.Headers {
var headers = Headers()
for header in allHeaderFields {
switch (header.key, header.value) {
case let (key, value) as (String, String):
headers.add(name: key, value: value)
case let (key, values) as (String, [String]):
headers.add(name: key, values: values)
default: continue
}
}
return headers
}

convenience init(httpURLResponse: HTTPURLResponse, data: Data) throws {
let headers = Self.headers(from: httpURLResponse.allHeaderFields)
let body = HttpBody.stream(ByteStream.from(data: data))

guard let statusCode = HttpStatusCode(rawValue: httpURLResponse.statusCode) else {
// This shouldn't happen, but `HttpStatusCode` only exposes a failable
// `init`. The alternative here is force unwrapping, but we can't
// make the decision to crash here on behalf on consuming applications.
throw FoundationClientEngineError.unexpectedStatusCode(
statusCode: httpURLResponse.statusCode
)
}
self.init(headers: headers, body: body, statusCode: statusCode)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import ClientRuntime
import Amplify

@_spi(FoundationClientEngine)
public struct FoundationClientEngine: HttpClientEngine {
public func execute(request: ClientRuntime.SdkHttpRequest) async throws -> ClientRuntime.HttpResponse {
let urlRequest = try URLRequest(sdkRequest: request)

let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpURLResponse = response as? HTTPURLResponse else {
// This shouldn't be necessary because we're only making HTTP requests.
// `URLResponse` should always be a `HTTPURLResponse`.
// But to refrain from crashing consuming applications, we're throwing here.
throw FoundationClientEngineError.invalidURLResponse(urlRequest: response)
}

let httpResponse = try HttpResponse(
httpURLResponse: httpURLResponse,
data: data
)

return httpResponse
}

public init() {}

/// no-op
func close() async {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import Amplify
import ClientRuntime

struct FoundationClientEngineError: AmplifyError {
let errorDescription: ErrorDescription
let recoverySuggestion: RecoverySuggestion
let underlyingError: Error?

// protocol requirement
init(
errorDescription: ErrorDescription,
recoverySuggestion: RecoverySuggestion,
error: Error
) {
self.errorDescription = errorDescription
self.recoverySuggestion = recoverySuggestion
self.underlyingError = error
}
}

extension FoundationClientEngineError {
init(
errorDescription: ErrorDescription,
recoverySuggestion: RecoverySuggestion,
error: Error?
) {
self.errorDescription = errorDescription
self.recoverySuggestion = recoverySuggestion
self.underlyingError = error
}

static func invalidRequestURL(sdkRequest: ClientRuntime.SdkHttpRequest) -> Self {
.init(
errorDescription: """
The SdkHttpRequest generated by ClientRuntime doesn't include a valid URL
- \(sdkRequest)
""",
recoverySuggestion: """
Please open an issue at https://github.com/aws-amplify/amplify-swift
with the contents of this error message.
""",
error: nil
)
}

static func invalidURLResponse(urlRequest: URLResponse) -> Self {
.init(
errorDescription: """
The URLResponse received is not an HTTPURLResponse
- \(urlRequest)
""",
recoverySuggestion: """
Please open an issue at https://github.com/aws-amplify/amplify-swift
with the contents of this error message.
""",
error: nil
)
}

static func unexpectedStatusCode(statusCode: Int) -> Self {
.init(
errorDescription: """
The status code received isn't a valid `HttpStatusCode` value.
- status code: \(statusCode)
""",
recoverySuggestion: """
Please open an issue at https://github.com/aws-amplify/amplify-swift
with the contents of this error message.
""",
error: nil
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import Amplify
import AWSPluginsCore
import Foundation

@_spi(FoundationClientEngine) import AWSPluginsCore
import AWSLocation
import AWSClientRuntime

Expand All @@ -35,6 +35,13 @@ extension AWSLocationGeoPlugin {
frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(),
region: region)

#if os(iOS) || os(macOS) // no-op
#else
// For any platform except iOS or macOS
// Use Foundation instead of CRT for networking.
serviceConfiguration.httpClientEngine = FoundationClientEngine()
#endif

let location = LocationClient(config: serviceConfiguration)
let locationService = AWSLocationAdapter(location: location)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import AWSClientRuntime
import AWSPluginsCore
import AWSPinpoint
@_spi(FoundationClientEngine) import AWSPluginsCore

extension PinpointClient {
convenience init(region: String, credentialsProvider: CredentialsProvider) throws {
Expand All @@ -16,6 +17,12 @@ extension PinpointClient {
frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(),
region: region
)
#if os(iOS) || os(macOS) // no-op
#else
// For any platform except iOS or macOS
// Use Foundation instead of CRT for networking.
configuration.httpClientEngine = FoundationClientEngine()
#endif
PinpointRequestsRegistry.shared.setCustomHttpEngine(on: configuration)
self.init(config: configuration)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import ClientRuntime
}

nonisolated func setCustomHttpEngine(on configuration: PinpointClient.PinpointClientConfiguration) {
let oldHttpClientEngine = configuration.httpClientEngine
let baseHTTPClientEngine = configuration.httpClientEngine

configuration.httpClientEngine = CustomPinpointHttpClientEngine(
httpClientEngine: oldHttpClientEngine
httpClientEngine: baseHTTPClientEngine
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import AWSPluginsCore
@_spi(FoundationClientEngine) import AWSPluginsCore
import Amplify
import Combine
import Foundation
Expand Down Expand Up @@ -107,6 +108,14 @@ final class AWSCloudWatchLoggingSessionController {
frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(),
region: region
)

#if os(iOS) || os(macOS) // no-op
#else
// For any platform except iOS or macOS
// Use Foundation instead of CRT for networking.
configuration.httpClientEngine = FoundationClientEngine()
#endif

self.client = CloudWatchLogsClient(config: configuration)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import AWSTextract
import AWSComprehend
import AWSPolly
import AWSPluginsCore
@_spi(FoundationClientEngine) import AWSPluginsCore
import Foundation
import ClientRuntime
import AWSClientRuntime
Expand All @@ -37,30 +38,60 @@ class AWSPredictionsService {
credentialsProvider: credentialsProvider,
region: configuration.convert.region
)
#if os(iOS) || os(macOS) // no-op
#else
// For any platform except iOS or macOS
// Use Foundation instead of CRT for networking.
translateClientConfiguration.httpClientEngine = FoundationClientEngine()
#endif
let awsTranslateClient = TranslateClient(config: translateClientConfiguration)

let pollyClientConfiguration = try PollyClient.PollyClientConfiguration(
credentialsProvider: credentialsProvider,
region: configuration.convert.region
)
#if os(iOS) || os(macOS) // no-op
#else
// For any platform except iOS or macOS
// Use Foundation instead of CRT for networking.
pollyClientConfiguration.httpClientEngine = FoundationClientEngine()
#endif
let awsPollyClient = PollyClient(config: pollyClientConfiguration)

let comprehendClientConfiguration = try ComprehendClient.ComprehendClientConfiguration(
credentialsProvider: credentialsProvider,
region: configuration.convert.region
)
#if os(iOS) || os(macOS) // no-op
#else
// For any platform except iOS or macOS
// Use Foundation instead of CRT for networking.
comprehendClientConfiguration.httpClientEngine = FoundationClientEngine()
#endif
let awsComprehendClient = ComprehendClient(config: comprehendClientConfiguration)

let rekognitionClientConfiguration = try RekognitionClient.RekognitionClientConfiguration(
credentialsProvider: credentialsProvider,
region: configuration.identify.region
)
#if os(iOS) || os(macOS) // no-op
#else
// For any platform except iOS or macOS
// Use Foundation instead of CRT for networking.
rekognitionClientConfiguration.httpClientEngine = FoundationClientEngine()
#endif
let awsRekognitionClient = RekognitionClient(config: rekognitionClientConfiguration)

let textractClientConfiguration = try TextractClient.TextractClientConfiguration(
credentialsProvider: credentialsProvider,
region: configuration.identify.region
)
#if os(iOS) || os(macOS) // no-op
#else
// For any platform except iOS or macOS
// Use Foundation instead of CRT for networking.
textractClientConfiguration.httpClientEngine = FoundationClientEngine()
#endif
let awsTextractClient = TextractClient(config: textractClientConfiguration)

let awsTranscribeStreamingAdapter = AWSTranscribeStreamingAdapter(
Expand Down
Loading

0 comments on commit 6fa88d4

Please sign in to comment.