diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift index d39a0b3375..bf5c8529fc 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift @@ -20,21 +20,27 @@ public extension AWSAPIPlugin { /// - Throws: /// - PluginError.pluginConfigurationError: If one of the required configuration values is invalid or empty func configure(using configuration: Any?) throws { - - guard let jsonValue = configuration as? JSONValue else { - throw PluginError.pluginConfigurationError( - "Could not cast incoming configuration to JSONValue", - """ - The specified configuration is either nil, or not convertible to a JSONValue. Review the configuration \ - and ensure it contains the expected values, and does not use any types that aren't convertible to a \ - corresponding JSONValue: - \(String(describing: configuration)) - """ - ) - } - - let dependencies = try ConfigurationDependencies(configurationValues: jsonValue, + + let dependencies: ConfigurationDependencies + if let configuration = self.configuration { + dependencies = try ConfigurationDependencies(configuration: configuration, apiAuthProviderFactory: authProviderFactory) + } else { + guard let jsonValue = configuration as? JSONValue else { + throw PluginError.pluginConfigurationError( + "Could not cast incoming configuration to JSONValue", + """ + The specified configuration is either nil, or not convertible to a JSONValue. Review the configuration \ + and ensure it contains the expected values, and does not use any types that aren't convertible to a \ + corresponding JSONValue: + \(String(describing: configuration)) + """ + ) + } + dependencies = try ConfigurationDependencies(configurationValues: jsonValue, + apiAuthProviderFactory: authProviderFactory) + + } configure(using: dependencies) // Initialize SwiftSDK's CRT dependency for SigV4 signing functionality @@ -56,6 +62,33 @@ extension AWSAPIPlugin { let subscriptionConnectionFactory: SubscriptionConnectionFactory let logLevel: Amplify.LogLevel + init(configuration: AWSAPIPluginConfiguration, + apiAuthProviderFactory: APIAuthProviderFactory, + authService: AWSAuthServiceBehavior? = nil, + subscriptionConnectionFactory: SubscriptionConnectionFactory? = nil, + logLevel: Amplify.LogLevel? = nil) throws { + let authService = authService + ?? AWSAuthService() + + let pluginConfig = try AWSAPICategoryPluginConfiguration( + configuration: configuration, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService + ) + + let subscriptionConnectionFactory = subscriptionConnectionFactory + ?? AWSSubscriptionConnectionFactory() + + let logLevel = logLevel ?? Amplify.Logging.logLevel + + self.init( + pluginConfig: pluginConfig, + authService: authService, + subscriptionConnectionFactory: subscriptionConnectionFactory, + logLevel: logLevel + ) + } + init( configurationValues: JSONValue, apiAuthProviderFactory: APIAuthProviderFactory, diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift index 5fe085b42e..ae6a9dfef4 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift @@ -15,6 +15,10 @@ final public class AWSAPIPlugin: NSObject, APICategoryPlugin, APICategoryGraphQL return "awsAPIPlugin" } + /// API configuration, takes precedent over configuration in `pluginConfig` + /// Passed in to the initializer of AWSAPIPlugin. + var configuration: AWSAPIPluginConfiguration? + /// A holder for API configurations. This will be populated during the /// configuration phase, and is clearable by `reset()`. var pluginConfig: AWSAPICategoryPluginConfiguration! @@ -46,10 +50,12 @@ final public class AWSAPIPlugin: NSObject, APICategoryPlugin, APICategoryGraphQL let reachabilityMapLock: NSLock public init( + configuration: AWSAPIPluginConfiguration? = nil, modelRegistration: AmplifyModelRegistration? = nil, sessionFactory: URLSessionBehaviorFactory? = nil, apiAuthProviderFactory: APIAuthProviderFactory? = nil ) { + self.configuration = configuration self.mapper = OperationTaskMapper() self.queue = OperationQueue() self.authProviderFactory = apiAuthProviderFactory ?? APIAuthProviderFactory() diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration+EndpointConfig.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration+EndpointConfig.swift index a5fda2fe29..866bff5368 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration+EndpointConfig.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration+EndpointConfig.swift @@ -23,10 +23,36 @@ public extension AWSAPICategoryPluginConfiguration { // default authorization configuration let authorizationConfiguration: AWSAuthorizationConfiguration - let endpointType: AWSAPICategoryPluginEndpointType + let endpointType: AWSAPIPluginEndpointType var apiKey: String? + public init(configuration: AWSAPIPluginConfiguration.API, + apiAuthProviderFactory: APIAuthProviderFactory, + authService: AWSAuthServiceBehavior? = nil) throws { + + guard let baseURL = URL(string: configuration.endpoint) else { + throw PluginError.pluginConfigurationError( + "Could not convert `\(configuration.endpoint)` to a URL", + """ + The "endpoint" value in the specified configuration cannot be converted to a URL. Review the \ + configuration and ensure it contains the expected values: + \(configuration.endpoint) + """ + ) + } + + try self.init(name: configuration.apiName, + baseURL: baseURL, + region: configuration.region, + authorizationType: configuration.authorizationType, + endpointType: configuration.endpointType, + apiKey: configuration.apiKey, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService) + + } + public init(name: String, jsonValue: JSONValue, apiAuthProviderFactory: APIAuthProviderFactory, @@ -63,7 +89,7 @@ public extension AWSAPICategoryPluginConfiguration { baseURL: URL, region: AWSRegionType?, authorizationType: AWSAuthorizationType, - endpointType: AWSAPICategoryPluginEndpointType, + endpointType: AWSAPIPluginEndpointType, apiKey: String? = nil, apiAuthProviderFactory: APIAuthProviderFactory, authService: AWSAuthServiceBehavior? = nil) throws { @@ -113,7 +139,7 @@ public extension AWSAPICategoryPluginConfiguration { } private static func getEndpointType(from endpointJSON: [String: JSONValue]) throws -> - AWSAPICategoryPluginEndpointType { + AWSAPIPluginEndpointType { guard case .string(let endpointTypeValue) = endpointJSON["endpointType"] else { throw PluginError.pluginConfigurationError( @@ -126,7 +152,7 @@ public extension AWSAPICategoryPluginConfiguration { ) } - let endpointTypeOptional = AWSAPICategoryPluginEndpointType(rawValue: endpointTypeValue) + let endpointTypeOptional = AWSAPIPluginEndpointType(rawValue: endpointTypeValue) guard let endpointType = endpointTypeOptional else { throw PluginError.pluginConfigurationError( @@ -201,7 +227,7 @@ extension Dictionary where Key == String, Value == AWSAPICategoryPluginConfigura /// 3. If nothing is specified, return the endpoint only if there is a single one, with GraphQL taking precedent /// over REST. func getConfig(for apiName: String? = nil, - endpointType: AWSAPICategoryPluginEndpointType? = nil) throws -> + endpointType: AWSAPIPluginEndpointType? = nil) throws -> AWSAPICategoryPluginConfiguration.EndpointConfig { if let apiName = apiName { return try getConfig(for: apiName) @@ -231,7 +257,7 @@ extension Dictionary where Key == String, Value == AWSAPICategoryPluginConfigura } /// Retrieve the endpoint configuration when there is only one endpoint of the specified `endpointType` - private func getConfig(for endpointType: AWSAPICategoryPluginEndpointType) throws -> + private func getConfig(for endpointType: AWSAPIPluginEndpointType) throws -> AWSAPICategoryPluginConfiguration.EndpointConfig { let apiForEndpointType = filter { (_, endpointConfig) -> Bool in return endpointConfig.endpointType == endpointType diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift index 200a813e96..fad9d3598b 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift @@ -19,6 +19,29 @@ public struct AWSAPICategoryPluginConfiguration { private var apiAuthProviderFactory: APIAuthProviderFactory? private var authService: AWSAuthServiceBehavior? + init(configuration: AWSAPIPluginConfiguration, + apiAuthProviderFactory: APIAuthProviderFactory, + authService: AWSAuthServiceBehavior) throws { + + var endpoints: [APIEndpointName: EndpointConfig] = [:] + + try configuration.apis.forEach { config in + let endpointConfig = try EndpointConfig(configuration: config, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService) + endpoints.updateValue(endpointConfig, forKey: config.apiName) + } + + let interceptors = try AWSAPICategoryPluginConfiguration.makeInterceptors(forEndpoints: endpoints, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService) + + self.init(endpoints: endpoints, + interceptors: interceptors, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService) + } + init(jsonValue: JSONValue, apiAuthProviderFactory: APIAuthProviderFactory, authService: AWSAuthServiceBehavior) throws { diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginEndpointType.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginEndpointType.swift index 49b673d3e3..f58aa19b6f 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginEndpointType.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginEndpointType.swift @@ -7,7 +7,7 @@ import Foundation -enum AWSAPICategoryPluginEndpointType: String { +public enum AWSAPIPluginEndpointType: String, Codable { case rest = "REST" case graphQL = "GraphQL" } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift index 9be9d3c05b..aa11b9541e 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift @@ -45,7 +45,7 @@ struct AWSAPIEndpointInterceptors { } /// Initialize authorization interceptors - mutating func addAuthInterceptorsToEndpoint(endpointType: AWSAPICategoryPluginEndpointType, + mutating func addAuthInterceptorsToEndpoint(endpointType: AWSAPIPluginEndpointType, authConfiguration: AWSAuthorizationConfiguration) throws { switch authConfiguration { case .none: diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIPluginConfiguration.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIPluginConfiguration.swift new file mode 100644 index 0000000000..c71d840d2a --- /dev/null +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIPluginConfiguration.swift @@ -0,0 +1,33 @@ +import Foundation +import AWSPluginsCore + +public struct AWSAPIPluginConfiguration: Codable { + + public struct API: Codable { + var apiName: String + var endpoint: String + var endpointType: AWSAPIPluginEndpointType + var region: String + var authorizationType: AWSAuthorizationType + var apiKey: String? + } + + public let apis: [API] + + public init(apiName: String = UUID().uuidString, + endpoint: String, + endpointType: AWSAPIPluginEndpointType, + region: String, + authorizationType: AWSAuthorizationType, + apiKey: String? = nil) { + self.apis = [.init(apiName: apiName, + endpoint: endpoint, + endpointType: endpointType, + region: region, + authorizationType: authorizationType)] + } + + public init(_ apis: API...) { + self.apis = apis + } +} diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift index d012985a63..f271967183 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift @@ -15,12 +15,12 @@ typealias AWSRegionType = String struct IAMURLRequestInterceptor: URLRequestInterceptor { let iamCredentialsProvider: IAMCredentialsProvider let region: AWSRegionType - let endpointType: AWSAPICategoryPluginEndpointType + let endpointType: AWSAPIPluginEndpointType private let userAgent = AmplifyAWSServiceConfiguration.userAgentLib init(iamCredentialsProvider: IAMCredentialsProvider, region: AWSRegionType, - endpointType: AWSAPICategoryPluginEndpointType) { + endpointType: AWSAPIPluginEndpointType) { self.iamCredentialsProvider = iamCredentialsProvider self.region = region self.endpointType = endpointType diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ConfigureTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ConfigureTests.swift index 3609fe61f2..c408760126 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ConfigureTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ConfigureTests.swift @@ -16,6 +16,29 @@ class AWSAPICategoryPluginConfigureTests: AWSAPICategoryPluginTestBase { XCTAssertEqual(apiPlugin.key, "awsAPIPlugin") } + func testExplicitConfiguration() throws { + let configuration = AWSAPIPluginConfiguration( + .init(apiName: "Test", + endpoint: "http://www.example.com", + endpointType: .rest, + region: "us-east-1", + authorizationType: .apiKey, + apiKey: "key"), + .init(apiName: "TEST2", + endpoint: "http://www.example.com", + endpointType: .graphQL, + region: "us-east-2", + authorizationType: .amazonCognitoUserPools)) + + let apiPlugin = AWSAPIPlugin(configuration: configuration) + + do { + try apiPlugin.configure(using: nil) + } catch { + XCTFail("Failed to configure api plugin: \(error)") + } + } + func testConfigureSuccess() throws { let apiPlugin = AWSAPIPlugin() let apiPluginConfig: JSONValue = [ diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ReachabilityTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ReachabilityTests.swift index 5f5f38ff2f..a8e6b9f578 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ReachabilityTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ReachabilityTests.swift @@ -120,7 +120,7 @@ class AWSAPICategoryPluginReachabilityTests: XCTestCase { // MARK: - Helpers - func getEndpointConfig(apiName: String, endpointType: AWSAPICategoryPluginEndpointType) throws -> + func getEndpointConfig(apiName: String, endpointType: AWSAPIPluginEndpointType) throws -> AWSAPICategoryPluginConfiguration.EndpointConfig { try AWSAPICategoryPluginConfiguration.EndpointConfig( name: apiName, diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationEndpointConfigTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationEndpointConfigTests.swift index be200d23a2..baf36a4f43 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationEndpointConfigTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationEndpointConfigTests.swift @@ -145,7 +145,7 @@ class AWSAPICategoryPluginConfigurationEndpointConfigTests: XCTestCase { // MARK: - Helpers - func getEndpointConfig(apiName: String, endpointType: AWSAPICategoryPluginEndpointType) throws -> + func getEndpointConfig(apiName: String, endpointType: AWSAPIPluginEndpointType) throws -> AWSAPICategoryPluginConfiguration.EndpointConfig { try AWSAPICategoryPluginConfiguration.EndpointConfig( name: apiName, diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift index a2f7503f01..589a172d81 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift @@ -109,7 +109,7 @@ class AWSAPICategoryPluginConfigurationTests: XCTestCase { // MARK: - Helpers - func getEndpointConfig(apiName: String, endpointType: AWSAPICategoryPluginEndpointType) throws -> + func getEndpointConfig(apiName: String, endpointType: AWSAPIPluginEndpointType) throws -> AWSAPICategoryPluginConfiguration.EndpointConfig { try AWSAPICategoryPluginConfiguration.EndpointConfig( name: apiName, diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/OperationTestBase.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/OperationTestBase.swift index 6c5496d13e..446d91cfea 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/OperationTestBase.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/OperationTestBase.swift @@ -25,7 +25,7 @@ class OperationTestBase: XCTestCase { func setUpPlugin( sessionFactory: URLSessionBehaviorFactory? = nil, subscriptionConnectionFactory: SubscriptionConnectionFactory? = nil, - endpointType: AWSAPICategoryPluginEndpointType + endpointType: AWSAPIPluginEndpointType ) throws { apiPlugin = AWSAPIPlugin(sessionFactory: sessionFactory) @@ -50,7 +50,7 @@ class OperationTestBase: XCTestCase { func setUpPluginForSingleResponse( sending data: Data, - for endpointType: AWSAPICategoryPluginEndpointType + for endpointType: AWSAPIPluginEndpointType ) throws { let task = try makeSingleValueSuccessMockTask(sending: data) let mockSession = MockURLSession(onTaskForRequest: { _ in task }) @@ -58,7 +58,7 @@ class OperationTestBase: XCTestCase { try setUpPlugin(sessionFactory: sessionFactory, endpointType: endpointType) } - func setUpPluginForSingleError(for endpointType: AWSAPICategoryPluginEndpointType) throws { + func setUpPluginForSingleError(for endpointType: AWSAPIPluginEndpointType) throws { let task = try makeSingleValueErrorMockTask() let mockSession = MockURLSession(onTaskForRequest: { _ in task }) let sessionFactory = MockSessionFactory(returning: mockSession) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthorizationType.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthorizationType.swift index 0530183e3c..b71cc0f64a 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthorizationType.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthorizationType.swift @@ -13,7 +13,7 @@ import Foundation /// GraphQL backend, or an Amazon API Gateway endpoint. /// /// - SeeAlso: [https://docs.aws.amazon.com/appsync/latest/devguide/security.html](AppSync Security) -public enum AWSAuthorizationType: String { +public enum AWSAuthorizationType: String, Codable { /// For public APIs case none = "NONE"