Skip to content

Commit

Permalink
feat: add interceptor client config
Browse files Browse the repository at this point in the history
Allows configuration of interceptors on a client level by adding interceptor
providers to client config, allowing Plugins to add interceptors.

The primary addition is `InterceptorProvider`, an interface that creates
generic interceptors which can operate on any transport - http or otherwise.
When an operation is executed, interceptor providers are called to create
new instances of service-level interceptors. Creating new instances also
means we don't need to synchronize on the shared interceptors. Transport
specific config, have their own methods for adding interceptor providers, so
you can add `HttpInterceptorProvider`s to http config. Operations know which
transport they operate on, and can choose which transport-specific interceptor
providers to use.

If/when we have operation-level configuration, it might make more sense to
allow plugins to configure more generic 'operation customizations' or
something, rather than just the interceptors. Operations would then call
the customizations before executing.

A few other minor changes were made to the client libraries:
- Added actual builder methods to RequestMessageBuilder, which we would need
eventually, so I could use them in testing
- Made SdkHttpRequestBuilder final

Codegen was also updated to generate the new config methods, and to call
interceptor providers in operations to add configured interceptors.
  • Loading branch information
milesziemer committed May 10, 2024
1 parent 1fbe84e commit 8e4ec7c
Show file tree
Hide file tree
Showing 22 changed files with 362 additions and 24 deletions.
5 changes: 5 additions & 0 deletions Sources/ClientRuntime/Config/DefaultClientConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,10 @@ public protocol DefaultClientConfiguration: ClientConfiguration {
/// If none is provided, only a default logger provider will be used.
var telemetryProvider: TelemetryProvider { get set }

/// Add an `InterceptorProvider` that will be used to provide interceptors for all operations.
///
/// - Parameter provider: The `InterceptorProvider` to add.
func addInterceptorProvider(_ provider: InterceptorProvider)

/// TODO(plugins): Add Checksum, etc.
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ public protocol DefaultHttpClientConfiguration: ClientConfiguration {
///
/// Defaults to a auth scheme resolver generated based on Smithy service model.
var authSchemeResolver: ClientRuntime.AuthSchemeResolver { get set }

/// Add an `HttpInterceptorProvider` that will be used to provide interceptors for all HTTP operations.
///
/// - Parameter provider: The `HttpInterceptorProvider` to add.
func addInterceptorProvider(_ provider: HttpInterceptorProvider)
}
17 changes: 17 additions & 0 deletions Sources/ClientRuntime/Interceptor/HttpInterceptorProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

/// Provides implementations of `HttpInterceptor`.
///
/// For the generic counterpart, see `InterceptorProvider`.
public protocol HttpInterceptorProvider {

/// Creates an instance of an `HttpInterceptor` implementation.
///
/// - Returns: The `HttpInterceptor` implementation.
func create<InputType, OutputType>() -> any HttpInterceptor<InputType, OutputType>
}
25 changes: 25 additions & 0 deletions Sources/ClientRuntime/Interceptor/InterceptorProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

/// Provides implementations of `Interceptor` for any Request, Response, and Attributes types.
///
/// This can be used to create `Interceptor`s that are generic on their Request/Response/Attributes
/// types, when you don't have access to the exact types until later.
public protocol InterceptorProvider {

/// Creates an instance of an `Interceptor` implementation, specialized on the given
/// `RequestType`, `ResponseType`, and `AttributesType`.
///
/// - Returns: The `Interceptor` implementation.
func create<
InputType,
OutputType,
RequestType: RequestMessage,
ResponseType: ResponseMessage,
AttributesType: HasAttributes
>() -> any Interceptor<InputType, OutputType, RequestType, ResponseType, AttributesType>
}
7 changes: 7 additions & 0 deletions Sources/ClientRuntime/Interceptor/Interceptors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ public struct Interceptors<
self.interceptors.append(interceptor.erase())
}

/// - Parameter interceptor: The Interceptor to add.
public mutating func add(
_ interceptor: some Interceptor<InputType, OutputType, RequestType, ResponseType, AttributesType>
) {
self.interceptors.append(interceptor.erase())
}

/// - Parameter interceptorFn: The closure to use as the Interceptor hook.
public mutating func addReadBeforeExecution(
_ interceptorFn: @escaping (any BeforeSerialization<InputType, AttributesType>) async throws -> Void
Expand Down
4 changes: 4 additions & 0 deletions Sources/ClientRuntime/Message/RequestMessageBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public protocol RequestMessageBuilder<RequestType>: AnyObject {

init()

func withHost(_ host: String) -> Self

func withBody(_ body: ByteStream) -> Self

/// - Returns: The built request.
func build() -> RequestType
}
2 changes: 1 addition & 1 deletion Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ extension SdkHttpRequestBuilder {
}
}

public class SdkHttpRequestBuilder: RequestMessageBuilder {
public final class SdkHttpRequestBuilder: RequestMessageBuilder {

required public init() {}

Expand Down
22 changes: 18 additions & 4 deletions Sources/WeatherSDK/WeatherClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,13 @@ extension WeatherClient {

public var authSchemeResolver: ClientRuntime.AuthSchemeResolver

public private(set) var interceptorProviders: [ClientRuntime.InterceptorProvider]

public private(set) var httpInterceptorProviders: [ClientRuntime.HttpInterceptorProvider]

internal let logger: ClientRuntime.LogAgent

private init(_ telemetryProvider: ClientRuntime.TelemetryProvider, _ retryStrategyOptions: ClientRuntime.RetryStrategyOptions, _ clientLogMode: ClientRuntime.ClientLogMode, _ endpoint: Swift.String?, _ idempotencyTokenGenerator: ClientRuntime.IdempotencyTokenGenerator, _ httpClientEngine: ClientRuntime.HTTPClient, _ httpClientConfiguration: ClientRuntime.HttpClientConfiguration, _ authSchemes: [ClientRuntime.AuthScheme]?, _ authSchemeResolver: ClientRuntime.AuthSchemeResolver) {
private init(_ telemetryProvider: ClientRuntime.TelemetryProvider, _ retryStrategyOptions: ClientRuntime.RetryStrategyOptions, _ clientLogMode: ClientRuntime.ClientLogMode, _ endpoint: Swift.String?, _ idempotencyTokenGenerator: ClientRuntime.IdempotencyTokenGenerator, _ httpClientEngine: ClientRuntime.HTTPClient, _ httpClientConfiguration: ClientRuntime.HttpClientConfiguration, _ authSchemes: [ClientRuntime.AuthScheme]?, _ authSchemeResolver: ClientRuntime.AuthSchemeResolver, _ interceptorProviders: [ClientRuntime.InterceptorProvider], _ httpInterceptorProviders: [ClientRuntime.HttpInterceptorProvider]) {
self.telemetryProvider = telemetryProvider
self.retryStrategyOptions = retryStrategyOptions
self.clientLogMode = clientLogMode
Expand All @@ -56,20 +60,30 @@ extension WeatherClient {
self.httpClientConfiguration = httpClientConfiguration
self.authSchemes = authSchemes
self.authSchemeResolver = authSchemeResolver
self.interceptorProviders = interceptorProviders
self.httpInterceptorProviders = httpInterceptorProviders
self.logger = telemetryProvider.loggerProvider.getLogger(name: WeatherClient.clientName)
}

public convenience init(telemetryProvider: ClientRuntime.TelemetryProvider? = nil, retryStrategyOptions: ClientRuntime.RetryStrategyOptions? = nil, clientLogMode: ClientRuntime.ClientLogMode? = nil, endpoint: Swift.String? = nil, idempotencyTokenGenerator: ClientRuntime.IdempotencyTokenGenerator? = nil, httpClientEngine: ClientRuntime.HTTPClient? = nil, httpClientConfiguration: ClientRuntime.HttpClientConfiguration? = nil, authSchemes: [ClientRuntime.AuthScheme]? = nil, authSchemeResolver: ClientRuntime.AuthSchemeResolver? = nil) throws {
self.init(telemetryProvider ?? ClientRuntime.DefaultTelemetry.provider, retryStrategyOptions ?? ClientConfigurationDefaults.defaultRetryStrategyOptions, clientLogMode ?? ClientConfigurationDefaults.defaultClientLogMode, endpoint, idempotencyTokenGenerator ?? ClientConfigurationDefaults.defaultIdempotencyTokenGenerator, httpClientEngine ?? ClientConfigurationDefaults.makeClient(httpClientConfiguration: httpClientConfiguration ?? ClientConfigurationDefaults.defaultHttpClientConfiguration), httpClientConfiguration ?? ClientConfigurationDefaults.defaultHttpClientConfiguration, authSchemes, authSchemeResolver ?? ClientConfigurationDefaults.defaultAuthSchemeResolver)
public convenience init(telemetryProvider: ClientRuntime.TelemetryProvider? = nil, retryStrategyOptions: ClientRuntime.RetryStrategyOptions? = nil, clientLogMode: ClientRuntime.ClientLogMode? = nil, endpoint: Swift.String? = nil, idempotencyTokenGenerator: ClientRuntime.IdempotencyTokenGenerator? = nil, httpClientEngine: ClientRuntime.HTTPClient? = nil, httpClientConfiguration: ClientRuntime.HttpClientConfiguration? = nil, authSchemes: [ClientRuntime.AuthScheme]? = nil, authSchemeResolver: ClientRuntime.AuthSchemeResolver? = nil, interceptorProviders: [ClientRuntime.InterceptorProvider]? = nil, httpInterceptorProviders: [ClientRuntime.HttpInterceptorProvider]? = nil) throws {
self.init(telemetryProvider ?? ClientRuntime.DefaultTelemetry.provider, retryStrategyOptions ?? ClientConfigurationDefaults.defaultRetryStrategyOptions, clientLogMode ?? ClientConfigurationDefaults.defaultClientLogMode, endpoint, idempotencyTokenGenerator ?? ClientConfigurationDefaults.defaultIdempotencyTokenGenerator, httpClientEngine ?? ClientConfigurationDefaults.makeClient(httpClientConfiguration: httpClientConfiguration ?? ClientConfigurationDefaults.defaultHttpClientConfiguration), httpClientConfiguration ?? ClientConfigurationDefaults.defaultHttpClientConfiguration, authSchemes, authSchemeResolver ?? ClientConfigurationDefaults.defaultAuthSchemeResolver, interceptorProviders ?? [], httpInterceptorProviders ?? [])
}

public convenience required init() async throws {
try await self.init(telemetryProvider: nil, retryStrategyOptions: nil, clientLogMode: nil, endpoint: nil, idempotencyTokenGenerator: nil, httpClientEngine: nil, httpClientConfiguration: nil, authSchemes: nil, authSchemeResolver: nil)
try await self.init(telemetryProvider: nil, retryStrategyOptions: nil, clientLogMode: nil, endpoint: nil, idempotencyTokenGenerator: nil, httpClientEngine: nil, httpClientConfiguration: nil, authSchemes: nil, authSchemeResolver: nil, interceptorProviders: nil, httpInterceptorProviders: nil)
}

public var partitionID: String? {
return ""
}
public func addInterceptorProvider(_ provider: ClientRuntime.InterceptorProvider) {
self.interceptorProviders.append(provider)
}

public func addInterceptorProvider(_ provider: ClientRuntime.HttpInterceptorProvider) {
self.httpInterceptorProviders.append(provider)
}

}

public static func builder() -> ClientBuilder<WeatherClient> {
Expand Down
38 changes: 36 additions & 2 deletions Tests/ClientRuntimeTests/InterceptorTests/InterceptorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ class InterceptorTests: XCTestCase {
}
}

struct ModifyMultipleInterceptor<OutputType>: HttpInterceptor {
struct ModifyMultipleInterceptor: HttpInterceptor {
public typealias InputType = TestInput
public typealias OutputType = TestOutput

private let newInputValue: Int

Expand Down Expand Up @@ -95,7 +96,7 @@ class InterceptorTests: XCTestCase {
let addAttributeInterceptor = AddAttributeInterceptor<String, TestInput, TestOutput, SdkHttpRequest, HttpResponse, HttpContext>(key: AttributeKey(name: "foo"), value: "bar")
let modifyInputInterceptor = ModifyInputInterceptor<TestInput, TestOutput, SdkHttpRequest, HttpResponse, HttpContext>(keyPath: \.property, value: "bar")
let addHeaderInterceptor = AddHeaderInterceptor<TestInput, TestOutput>(headerName: "foo", headerValue: "bar")
let modifyMultipleInterceptor = ModifyMultipleInterceptor<TestOutput>(newInputValue: 1)
let modifyMultipleInterceptor = ModifyMultipleInterceptor(newInputValue: 1)

let interceptors: [AnyInterceptor<TestInput, TestOutput, SdkHttpRequest, HttpResponse, HttpContext>] = [
addAttributeInterceptor.erase(),
Expand All @@ -118,4 +119,37 @@ class InterceptorTests: XCTestCase {
XCTAssertEqual(interceptorContext.getRequest().headers.value(for: "foo"), "bar")
XCTAssertEqual(interceptorContext.getRequest().headers.value(for: "otherProperty"), "1")
}

struct ModifyHostInterceptor<InputType, OutputType, RequestType: RequestMessage, ResponseType: ResponseMessage, AttributesType: HasAttributes>: Interceptor {
func modifyBeforeRetryLoop(context: some MutableRequest<Self.InputType, Self.RequestType, Self.AttributesType>) async throws {
context.updateRequest(updated: context.getRequest().toBuilder().withHost("foo").build())
}
}

struct ModifyHostInterceptorProvider: InterceptorProvider {
func create<InputType, OutputType, RequestType: RequestMessage, ResponseType: ResponseMessage, AttributesType: HasAttributes>() -> any Interceptor<InputType, OutputType, RequestType, ResponseType, AttributesType> {
ModifyHostInterceptor()
}
}

func test_providers() async throws {
let provider1 = ModifyHostInterceptorProvider()
var interceptors = Interceptors<TestInput, TestOutput, SdkHttpRequest, HttpResponse, HttpContext>()

interceptors.add(provider1.create())

let httpContext = HttpContext(attributes: Attributes())
let input = TestInput()

let context = DefaultInterceptorContext<TestInput, TestOutput, SdkHttpRequest, HttpResponse, HttpContext>(input: input, attributes: httpContext)
context.updateRequest(updated: SdkHttpRequestBuilder().build())

try await interceptors.modifyBeforeSerialization(context: context)
try await interceptors.modifyBeforeRetryLoop(context: context)
try await interceptors.modifyBeforeTransmit(context: context)

let resultRequest = try XCTUnwrap(context.getRequest())

XCTAssertEqual(resultRequest.host, "foo")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ object ClientRuntimeTypes {
val AuthSchemeResolverParams = runtimeSymbol("AuthSchemeResolverParameters")
}

object Interceptor {
val Provider = runtimeSymbol("InterceptorProvider")
val Providers = runtimeSymbolWithoutNamespace("[ClientRuntime.InterceptorProvider]")
val HttpProvider = runtimeSymbolWithoutNamespace("ClientRuntime.HttpInterceptorProvider")
val HttpProviders = runtimeSymbolWithoutNamespace("[ClientRuntime.HttpInterceptorProvider]")
}

object Operation {
val Orchestrator = runtimeSymbol("Orchestrator")
val OrchestratorBuilder = runtimeSymbol("OrchestratorBuilder")
}

object Core {
val Endpoint = runtimeSymbol("Endpoint")
val Date = runtimeSymbol("Date")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package software.amazon.smithy.swift.codegen.config
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.swift.codegen.Dependency
import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator
import software.amazon.smithy.swift.codegen.lang.Function
import software.amazon.smithy.swift.codegen.model.buildSymbol

/**
Expand All @@ -21,6 +22,11 @@ interface ClientConfiguration {

fun getProperties(ctx: ProtocolGenerator.GenerationContext): Set<ConfigProperty>

/**
* The methods to render in the generated client configuration
*/
fun getMethods(ctx: ProtocolGenerator.GenerationContext): Set<Function> = setOf()

companion object {
fun runtimeSymbol(
name: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,31 @@
package software.amazon.smithy.swift.codegen.config

import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.swift.codegen.SwiftWriter
import software.amazon.smithy.swift.codegen.lang.AccessModifier
import software.amazon.smithy.swift.codegen.model.isOptional

data class ConfigProperty(
val name: String,
val type: Symbol,
val default: DefaultProvider? = null
val default: DefaultProvider? = null,
val accessModifier: AccessModifier = AccessModifier.Public,
) {
constructor(
name: String,
type: Symbol,
default: String,
isThrowable: Boolean = false,
isAsync: Boolean = false
) : this(name, type, DefaultProvider(default, isThrowable, isAsync))
isAsync: Boolean = false,
accessModifier: AccessModifier = AccessModifier.Public
) : this(name, type, DefaultProvider(default, isThrowable, isAsync), accessModifier)

init {
if (!type.isOptional() && default == null)
throw RuntimeException("Non-optional client config property must have a default value")
}

fun render(writer: SwiftWriter) {
writer.write("${accessModifier.renderedRightPad()}var \$L: \$N", name, type)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import software.amazon.smithy.swift.codegen.SwiftDependency
import software.amazon.smithy.swift.codegen.SwiftTypes
import software.amazon.smithy.swift.codegen.config.ClientConfiguration.Companion.runtimeSymbol
import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator
import software.amazon.smithy.swift.codegen.lang.AccessModifier
import software.amazon.smithy.swift.codegen.lang.Function
import software.amazon.smithy.swift.codegen.lang.FunctionParameter
import software.amazon.smithy.swift.codegen.model.toOptional

class DefaultClientConfiguration : ClientConfiguration {
Expand Down Expand Up @@ -39,5 +42,21 @@ class DefaultClientConfiguration : ClientConfiguration {
ClientRuntimeTypes.Core.IdempotencyTokenGenerator,
"ClientConfigurationDefaults.defaultIdempotencyTokenGenerator"
),
ConfigProperty(
"interceptorProviders",
ClientRuntimeTypes.Interceptor.Providers,
"[]",
accessModifier = AccessModifier.PublicPrivateSet
),
)

override fun getMethods(ctx: ProtocolGenerator.GenerationContext): Set<Function> = setOf(
Function(
name = "addInterceptorProvider",
renderBody = { writer -> writer.write("self.interceptorProviders.append(provider)") },
parameters = listOf(
FunctionParameter.NoLabel("provider", ClientRuntimeTypes.Interceptor.Provider)
),
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import software.amazon.smithy.swift.codegen.ClientRuntimeTypes
import software.amazon.smithy.swift.codegen.SwiftDependency
import software.amazon.smithy.swift.codegen.config.ClientConfiguration.Companion.runtimeSymbol
import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator
import software.amazon.smithy.swift.codegen.lang.AccessModifier
import software.amazon.smithy.swift.codegen.lang.Function
import software.amazon.smithy.swift.codegen.lang.FunctionParameter
import software.amazon.smithy.swift.codegen.model.toOptional

class DefaultHttpClientConfiguration : ClientConfiguration {
Expand All @@ -34,6 +37,22 @@ class DefaultHttpClientConfiguration : ClientConfiguration {
"authSchemeResolver",
ClientRuntimeTypes.Auth.AuthSchemeResolver,
"ClientConfigurationDefaults.defaultAuthSchemeResolver"
),
ConfigProperty(
"httpInterceptorProviders",
ClientRuntimeTypes.Interceptor.HttpProviders,
"[]",
accessModifier = AccessModifier.PublicPrivateSet
),
)

override fun getMethods(ctx: ProtocolGenerator.GenerationContext): Set<Function> = setOf(
Function(
name = "addInterceptorProvider",
renderBody = { writer -> writer.write("self.httpInterceptorProviders.append(provider)") },
parameters = listOf(
FunctionParameter.NoLabel("provider", ClientRuntimeTypes.Interceptor.HttpProvider)
),
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@ open class HttpProtocolServiceClient(
serviceConfig.clientName.toUpperCamelCase(),
clientConfigurationProtocols
) {
val properties: List<ConfigProperty> = ctx.integrations
.flatMap { it.clientConfigurations(ctx).flatMap { it.getProperties(ctx) } }
val clientConfigs = ctx.integrations.flatMap { it.clientConfigurations(ctx) }
val properties: List<ConfigProperty> = clientConfigs
.flatMap { it.getProperties(ctx) }
.let { overrideConfigProperties(it) }
.sortedBy { it.accessModifier }

renderConfigClassVariables(serviceSymbol, properties)

Expand All @@ -132,6 +134,14 @@ open class HttpProtocolServiceClient(
renderCustomConfigInitializer(properties)

renderPartitionID()

clientConfigs
.flatMap { it.getMethods(ctx) }
.sortedBy { it.accessModifier }
.forEach {
it.render(writer)
writer.write("")
}
}
writer.write("")
}
Expand Down Expand Up @@ -185,11 +195,10 @@ open class HttpProtocolServiceClient(
* Declare class variables in client configuration class
*/
private fun renderConfigClassVariables(serviceSymbol: Symbol, properties: List<ConfigProperty>) {
properties
.forEach {
writer.write("public var \$L: \$N", it.name, it.type)
writer.write("")
}
properties.forEach {
it.render(writer)
writer.write("")
}
writer.injectSection(ConfigClassVariablesCustomization(serviceSymbol))
writer.write("")
}
Expand Down
Loading

0 comments on commit 8e4ec7c

Please sign in to comment.