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!: Make config object immutable after client creation #903

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions Sources/ClientRuntime/Client/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
//
// SPDX-License-Identifier: Apache-2.0
//

public protocol Client {
associatedtype Config: ClientConfiguration

init(config: Config)
}
27 changes: 17 additions & 10 deletions Sources/ClientRuntime/Client/ClientBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,36 @@
//
// SPDX-License-Identifier: Apache-2.0
//

public class ClientBuilder<ClientType: Client> {

private var plugins: [Plugin]
private struct PluginContainer: Plugin {
let plugin: any Plugin<ClientType.Config>

public init(defaultPlugins: [Plugin] = []) {
self.plugins = defaultPlugins
func configureClient(clientConfiguration: inout ClientType.Config) async throws {
try await plugin.configureClient(clientConfiguration: &clientConfiguration)
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The PluginContainer class above is used to type-erase the actual plugin so that multiple plugins may be stored in a collection together.


public func withPlugin(_ plugin: any Plugin) -> ClientBuilder<ClientType> {
self.plugins.append(plugin)
private var plugins = [PluginContainer]()

public init() {}

public func withPlugin<P: Plugin>(_ plugin: P) -> ClientBuilder<ClientType> where P.Config == ClientType.Config {
self.plugins.append(PluginContainer(plugin: plugin))
return self
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Plugins are added with a .withPlugin() method so that each can be placed in a type-erased container for storage in an array.


public func build() async throws -> ClientType {
let configuration = try await resolve(plugins: self.plugins)
let configuration = try await resolve()
return ClientType(config: configuration)
}

func resolve(plugins: [any Plugin]) async throws -> ClientType.Config {
let clientConfiguration = try await ClientType.Config()
private func resolve() async throws -> ClientType.Config {
var config = try await ClientType.Config()
for plugin in plugins {
try await plugin.configureClient(clientConfiguration: clientConfiguration)
try await plugin.configureClient(clientConfiguration: &config)
}
return clientConfiguration
return config
Copy link
Contributor Author

Choose a reason for hiding this comment

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

An empty config is created, it is modified by each plugin, then the final config is returned to the caller.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public protocol DefaultClientConfiguration: ClientConfiguration {
/// Adds an `InterceptorProvider` that will be used to provide interceptors for all operations.
///
/// - Parameter provider: The `InterceptorProvider` to add.
func addInterceptorProvider(_ provider: InterceptorProvider)
mutating func addInterceptorProvider(_ provider: InterceptorProvider)

/// TODO(plugins): Add Checksum, etc.
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ public protocol DefaultHttpClientConfiguration: ClientConfiguration {
/// Adds a `HttpInterceptorProvider` that will be used to provide interceptors for all HTTP operations.
///
/// - Parameter provider: The `HttpInterceptorProvider` to add.
func addInterceptorProvider(_ provider: HttpInterceptorProvider)
mutating func addInterceptorProvider(_ provider: HttpInterceptorProvider)
}
16 changes: 7 additions & 9 deletions Sources/ClientRuntime/Plugins/AuthSchemePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import SmithyHTTPAuthAPI

public class AuthSchemePlugin: Plugin {
public class AuthSchemePlugin<Config: DefaultHttpClientConfiguration>: Plugin {

private var authSchemes: [AuthScheme]?

Expand All @@ -21,14 +21,12 @@ public class AuthSchemePlugin: Plugin {
self.authSchemes = authSchemes
}

public func configureClient(clientConfiguration: ClientConfiguration) {
if var config = clientConfiguration as? DefaultHttpClientConfiguration {
if self.authSchemes != nil {
config.authSchemes = self.authSchemes!
}
if self.authSchemeResolver != nil {
config.authSchemeResolver = self.authSchemeResolver!
}
public func configureClient(clientConfiguration: inout Config) {
if let authSchemes {
clientConfiguration.authSchemes = authSchemes
}
if let authSchemeResolver {
clientConfiguration.authSchemeResolver = authSchemeResolver
}
}
}
26 changes: 10 additions & 16 deletions Sources/ClientRuntime/Plugins/DefaultClientPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,17 @@

import struct SmithyRetries.DefaultRetryStrategy

public class DefaultClientPlugin: Plugin {
public class DefaultClientPlugin<Config: DefaultClientConfiguration & DefaultHttpClientConfiguration>: Plugin {
typealias DefaultRuntimeConfig = DefaultSDKRuntimeConfiguration<DefaultRetryStrategy, DefaultRetryErrorInfoProvider>

public init() {}
public func configureClient(clientConfiguration: ClientConfiguration) {
if var config = clientConfiguration as? DefaultClientConfiguration {
config.retryStrategyOptions =
DefaultSDKRuntimeConfiguration<DefaultRetryStrategy, DefaultRetryErrorInfoProvider>
.defaultRetryStrategyOptions
}

if var config = clientConfiguration as? DefaultHttpClientConfiguration {
let httpClientConfiguration =
DefaultSDKRuntimeConfiguration<DefaultRetryStrategy, DefaultRetryErrorInfoProvider>
.defaultHttpClientConfiguration
config.httpClientConfiguration = httpClientConfiguration
config.httpClientEngine =
DefaultSDKRuntimeConfiguration<DefaultRetryStrategy, DefaultRetryErrorInfoProvider>
.makeClient(httpClientConfiguration: httpClientConfiguration)
}
public func configureClient(clientConfiguration: inout Config) async throws {
clientConfiguration.retryStrategyOptions = DefaultRuntimeConfig.defaultRetryStrategyOptions
let httpClientConfiguration = DefaultRuntimeConfig .defaultHttpClientConfiguration
clientConfiguration.httpClientConfiguration = httpClientConfiguration
clientConfiguration.httpClientEngine = DefaultRuntimeConfig.makeClient(
httpClientConfiguration: httpClientConfiguration
)
}
}
12 changes: 4 additions & 8 deletions Sources/ClientRuntime/Plugins/HttpClientPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
import protocol SmithyHTTPAPI.HTTPClient
import struct SmithyRetries.DefaultRetryStrategy

public class DefaultHttpClientPlugin: Plugin {

public class DefaultHttpClientPlugin<Config: DefaultHttpClientConfiguration>: Plugin {
var httpClientConfiguration: HttpClientConfiguration

var httpClient: HTTPClient

public init(httpClient: HTTPClient, httpClientConfiguration: HttpClientConfiguration) {
Expand All @@ -27,10 +25,8 @@ public class DefaultHttpClientPlugin: Plugin {
)
}

public func configureClient(clientConfiguration: ClientConfiguration) {
if var config = clientConfiguration as? DefaultHttpClientConfiguration {
config.httpClientConfiguration = self.httpClientConfiguration
config.httpClientEngine = self.httpClient
}
public func configureClient(clientConfiguration: inout Config) {
clientConfiguration.httpClientConfiguration = self.httpClientConfiguration
clientConfiguration.httpClientEngine = self.httpClient
}
}
6 changes: 4 additions & 2 deletions Sources/ClientRuntime/Plugins/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// SPDX-License-Identifier: Apache-2.0
//

public protocol Plugin {
func configureClient(clientConfiguration: ClientConfiguration) async throws
public protocol Plugin<Config> {
associatedtype Config: ClientConfiguration

func configureClient(clientConfiguration: inout Config) async throws
Copy link
Contributor Author

@jbelkins jbelkins Feb 6, 2025

Choose a reason for hiding this comment

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

Plugin now takes an inout Config so that it may make permanent changes to the passed config.

The type of the config is now generic to the plugin so that additional casting is not needed on the config in order to access its properties.

}
8 changes: 3 additions & 5 deletions Sources/ClientRuntime/Plugins/RetryPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@

import struct SmithyRetriesAPI.RetryStrategyOptions

public class RetryPlugin: Plugin {
public class RetryPlugin<Config: DefaultClientConfiguration>: Plugin {

private var retryStrategyOptions: RetryStrategyOptions

public init(retryStrategyOptions: RetryStrategyOptions) {
self.retryStrategyOptions = retryStrategyOptions
}

public func configureClient(clientConfiguration: ClientConfiguration) {
if var config = clientConfiguration as? DefaultClientConfiguration {
config.retryStrategyOptions = self.retryStrategyOptions
}
public func configureClient(clientConfiguration: inout Config) {
clientConfiguration.retryStrategyOptions = self.retryStrategyOptions
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note there is no need to cast to DefaultClientConfiguration any more, because that constraint is built into the type of the plugin

}
}
9 changes: 4 additions & 5 deletions Sources/ClientRuntime/Plugins/TelemetryPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
// SPDX-License-Identifier: Apache-2.0
//

public class TelemetryPlugin: Plugin {
public class TelemetryPlugin<Config: DefaultClientConfiguration>: Plugin {

private let telemetryProvider: TelemetryProvider

public init(telemetryProvider: TelemetryProvider) {
Expand All @@ -26,10 +27,8 @@ public class TelemetryPlugin: Plugin {
)
}

public func configureClient(clientConfiguration: ClientConfiguration) {
if var config = clientConfiguration as? DefaultClientConfiguration {
config.telemetryProvider = self.telemetryProvider
}
public func configureClient(clientConfiguration: inout Config) {
clientConfiguration.telemetryProvider = self.telemetryProvider
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class DefaultClientConfiguration : ClientConfiguration {
listOf(
FunctionParameter.NoLabel("provider", ClientRuntimeTypes.Core.InterceptorProvider),
),
isMutating = true,
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class DefaultHttpClientConfiguration : ClientConfiguration {
listOf(
FunctionParameter.NoLabel("provider", ClientRuntimeTypes.Core.HttpInterceptorProvider),
),
isMutating = true,
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,30 +75,25 @@ open class HttpProtocolServiceClient(
ClientRuntimeTypes.Core.ClientBuilder,
serviceSymbol.name,
) {
writer.openBlock(
"return \$N<\$L>(defaultPlugins: [",
"])",
writer.write(
"return \$N<\$L>()",
ClientRuntimeTypes.Core.ClientBuilder,
serviceSymbol.name,
) {
val defaultPlugins: MutableList<Plugin> = mutableListOf(DefaultClientPlugin())
)
writer.indent()
val defaultPlugins: MutableList<Plugin> = mutableListOf(DefaultClientPlugin())

ctx.integrations
.flatMap { it.plugins(serviceConfig) }
.filter { it.isDefault }
.onEach { defaultPlugins.add(it) }
ctx.integrations
.flatMap { it.plugins(serviceConfig) }
.filter { it.isDefault }
.onEach { defaultPlugins.add(it) }

val pluginsIterator = defaultPlugins.iterator()
val pluginsIterator = defaultPlugins.iterator()

while (pluginsIterator.hasNext()) {
pluginsIterator.next().customInitialization(writer)
if (pluginsIterator.hasNext()) {
writer.write(",")
}
}

writer.unwrite(",\n").write("")
while (pluginsIterator.hasNext()) {
writer.write(".withPlugin(\$L)", pluginsIterator.next().customInitialization(writer))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Rather than render the plugins as array elements, they are added with a builder method since Swift will reject a literal array for not having plugins of the same type.

}
writer.dedent()
}
}
writer.write("")
Expand All @@ -113,7 +108,7 @@ open class HttpProtocolServiceClient(
.joinToString(" & ")

writer.openBlock(
"public class \$LConfiguration: \$L {",
"public struct \$LConfiguration: \$L {",
"}",
serviceConfig.clientName.toUpperCamelCase(),
clientConfigurationProtocols,
Expand Down Expand Up @@ -156,7 +151,7 @@ open class HttpProtocolServiceClient(
open fun overrideConfigProperties(properties: List<ConfigProperty>): List<ConfigProperty> = properties

private fun renderEmptyAsynchronousConfigInitializer(properties: List<ConfigProperty>) {
writer.openBlock("public convenience required init() async throws {", "}") {
writer.openBlock("public init() async throws {", "}") {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In several places convenience and required are removed from config initializers since these modifiers are not used for value types.

writer.openBlock("try await self.init(", ")") {
properties.forEach { property ->
writer.write("\$L: nil,", property.name)
Expand Down Expand Up @@ -219,7 +214,7 @@ open class HttpProtocolServiceClient(
}

private fun renderSynchronousConfigInitializer(properties: List<ConfigProperty>) {
writer.openBlock("public convenience init(", ") throws {") {
writer.openBlock("public init(", ") throws {") {
properties.forEach { property ->
writer.write("\$L: \$N = nil,", property.name, property.type.toOptional())
}
Expand All @@ -246,7 +241,7 @@ open class HttpProtocolServiceClient(
private fun renderAsynchronousConfigInitializer(properties: List<ConfigProperty>) {
if (properties.none { it.default?.isAsync == true }) return

writer.openBlock("public convenience init(", ") async throws {") {
writer.openBlock("public init(", ") async throws {") {
properties.forEach { property ->
writer.write("\$L: \$L = nil,", property.name, property.type.toOptional().renderSwiftType(writer))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ interface Plugin {
val isDefault: Boolean
get() = false

fun customInitialization(writer: SwiftWriter) {
writer.writeInline("\$N()", className)
}
fun customInitialization(writer: SwiftWriter): String = writer.format("\$N()", className)

fun render(
ctx: ProtocolGenerator.GenerationContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,25 @@ data class Function(
val accessModifier: AccessModifier = AccessModifier.Public,
val isAsync: Boolean = false,
val isThrowing: Boolean = false,
val isMutating: Boolean = false,
) {
/**
* Render this function using the given writer.
*/
fun render(writer: SwiftWriter) {
val renderedMutating = "mutating ".takeIf { isMutating } ?: ""
val renderedParameters = parameters.joinToString(", ") { it.rendered(writer) }
val renderedAsync = if (isAsync) "async " else ""
val renderedThrows = if (isThrowing) "throws " else ""
val renderedReturnType = returnType?.let { writer.format("-> \$N ", it) } ?: ""
writer.openBlock(
"${accessModifier.renderedRightPad()}func $name($renderedParameters) $renderedAsync$renderedThrows$renderedReturnType{",
"\$L\$Lfunc \$L(\$L) \$L$renderedThrows$renderedReturnType{",
"}",
accessModifier.renderedRightPad(),
renderedMutating,
name,
renderedParameters,
renderedAsync,
) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the optional mutating modifier here & did a lot of code cleanup.

(renderedThrows and renderedReturnType remain interpolated because openBlock takes a max number of arguments.)

renderBody.accept(writer)
}
Expand Down
Loading
Loading