From f9d3f28c65a67f5f0b0db230394f2cb93823eb1f Mon Sep 17 00:00:00 2001
From: John DiSanti <jdisanti@amazon.com>
Date: Fri, 27 Aug 2021 10:03:22 -0700
Subject: [PATCH] Consolidate fluent client builder implementations (#668)

* Consolidate fluent client builder implementations

* Rename FluentClientDecorator to AwsFluentClientDecorator

* Remove potentially confusing Smithy details from AWS fluent clients

* Fix AWS client generics issue

* fix missing template param

* remove problematic whitespace

* Fix Clone derivation issue

* Fix unintentional removal of `from_conf_conn`

* Change `Standard` to `retry`

Co-authored-by: Russell Cohen <rcoh@amazon.com>
---
 .../smithy/rustsdk/AwsCodegenDecorator.kt     |   2 +-
 .../rustsdk/AwsFluentClientDecorator.kt       | 120 ++++++
 .../smithy/rustsdk/FluentClientGenerator.kt   | 227 -----------
 .../generators/FluentClientDecorator.kt       | 371 ++++++++++--------
 .../generators/HttpProtocolGenerator.kt       |   9 +-
 5 files changed, 340 insertions(+), 389 deletions(-)
 create mode 100644 aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt
 delete mode 100644 aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/FluentClientGenerator.kt

diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt
index fc79c1e6b..a97fd8d3f 100644
--- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt
+++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt
@@ -20,7 +20,7 @@ val DECORATORS = listOf(
     SigV4SigningDecorator(),
     RetryPolicyDecorator(),
     IntegrationTestDecorator(),
-    FluentClientDecorator(),
+    AwsFluentClientDecorator(),
     CrateLicenseDecorator(),
 
     // Service specific decorators
diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt
new file mode 100644
index 000000000..f6b550457
--- /dev/null
+++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0.
+ */
+
+package software.amazon.smithy.rustsdk
+
+import software.amazon.smithy.rust.codegen.rustlang.Attribute
+import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
+import software.amazon.smithy.rust.codegen.rustlang.Feature
+import software.amazon.smithy.rust.codegen.rustlang.RustMetadata
+import software.amazon.smithy.rust.codegen.rustlang.RustModule
+import software.amazon.smithy.rust.codegen.rustlang.RustWriter
+import software.amazon.smithy.rust.codegen.rustlang.asType
+import software.amazon.smithy.rust.codegen.rustlang.rust
+import software.amazon.smithy.rust.codegen.rustlang.rustBlock
+import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
+import software.amazon.smithy.rust.codegen.rustlang.writable
+import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
+import software.amazon.smithy.rust.codegen.smithy.RuntimeType
+import software.amazon.smithy.rust.codegen.smithy.RustCrate
+import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
+import software.amazon.smithy.rust.codegen.smithy.generators.ClientGenerics
+import software.amazon.smithy.rust.codegen.smithy.generators.FluentClientGenerator
+import software.amazon.smithy.rust.codegen.smithy.generators.LibRsCustomization
+import software.amazon.smithy.rust.codegen.smithy.generators.LibRsSection
+import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig
+
+private class Types(runtimeConfig: RuntimeConfig) {
+    private val smithyClientDep = CargoDependency.SmithyClient(runtimeConfig).copy(optional = true)
+    private val awsHyperDep = runtimeConfig.awsRuntimeDependency("aws-hyper").copy(optional = true)
+
+    val awsHyper = awsHyperDep.asType()
+    val smithyClientRetry = RuntimeType("retry", smithyClientDep, "smithy_client")
+
+    val AwsMiddleware = RuntimeType("AwsMiddleware", awsHyperDep, "aws_hyper")
+    val DynConnector = RuntimeType("DynConnector", smithyClientDep, "smithy_client::erase")
+}
+
+class AwsFluentClientDecorator : RustCodegenDecorator {
+    override val name: String = "FluentClient"
+    override val order: Byte = 0
+
+    override fun extras(protocolConfig: ProtocolConfig, rustCrate: RustCrate) {
+        val types = Types(protocolConfig.runtimeConfig)
+        val module = RustMetadata(additionalAttributes = listOf(Attribute.Cfg.feature("client")), public = true)
+        rustCrate.withModule(RustModule("client", module)) { writer ->
+            FluentClientGenerator(
+                protocolConfig,
+                includeSmithyGenericClientDocs = false,
+                generics = ClientGenerics(
+                    connectorDefault = "#{AwsFluentClient_DynConnector}",
+                    middlewareDefault = "#{AwsFluentClient_AwsMiddleware}",
+                    retryDefault = "#{AwsFluentClient_retry}::Standard",
+                    codegenScope = listOf(
+                        "AwsFluentClient_AwsMiddleware" to types.AwsMiddleware,
+                        "AwsFluentClient_DynConnector" to types.DynConnector,
+                        "AwsFluentClient_retry" to types.smithyClientRetry,
+                    )
+                ),
+            ).render(writer)
+            AwsFluentClientExtensions(types).render(writer)
+        }
+        val awsHyper = "aws-hyper"
+        rustCrate.addFeature(Feature("client", true, listOf(awsHyper, "smithy-client")))
+        rustCrate.addFeature(Feature("rustls", default = true, listOf("$awsHyper/rustls")))
+        rustCrate.addFeature(Feature("native-tls", default = false, listOf("$awsHyper/native-tls")))
+    }
+
+    override fun libRsCustomizations(
+        protocolConfig: ProtocolConfig,
+        baseCustomizations: List<LibRsCustomization>
+    ): List<LibRsCustomization> {
+        return baseCustomizations + object : LibRsCustomization() {
+            override fun section(section: LibRsSection) = when (section) {
+                is LibRsSection.Body -> writable {
+                    Attribute.Cfg.feature("client").render(this)
+                    rust("pub use client::Client;")
+                }
+                else -> emptySection
+            }
+        }
+    }
+}
+
+private class AwsFluentClientExtensions(private val types: Types) {
+    fun render(writer: RustWriter) {
+        writer.rustBlock("impl<C> Client<C, aws_hyper::AwsMiddleware, smithy_client::retry::Standard>") {
+            rustTemplate(
+                """
+                pub fn from_conf_conn(conf: crate::Config, conn: C) -> Self {
+                    let client = #{aws_hyper}::Client::new(conn);
+                    Self { handle: std::sync::Arc::new(Handle { client, conf }) }
+                }
+                """,
+                "aws_hyper" to types.awsHyper,
+            )
+        }
+        writer.rustBlock("impl Client<smithy_client::erase::DynConnector, aws_hyper::AwsMiddleware, smithy_client::retry::Standard>") {
+            rustTemplate(
+                """
+                ##[cfg(any(feature = "rustls", feature = "native-tls"))]
+                pub async fn from_env() -> Self {
+                    // backwards compatibility shim
+                    use aws_types::region::ProvideRegion;
+                    let region = aws_types::region::default_provider().region().await;
+                    Self::from_conf(crate::Config::builder().region(region).build())
+                }
+
+                ##[cfg(any(feature = "rustls", feature = "native-tls"))]
+                pub fn from_conf(conf: crate::Config) -> Self {
+                    let client = #{aws_hyper}::Client::https();
+                    Self { handle: std::sync::Arc::new(Handle { client, conf }) }
+                }
+                """,
+                "aws_hyper" to types.awsHyper,
+            )
+        }
+    }
+}
diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/FluentClientGenerator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/FluentClientGenerator.kt
deleted file mode 100644
index e3913d1a8..000000000
--- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/FluentClientGenerator.kt
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0.
- */
-
-package software.amazon.smithy.rustsdk
-
-import software.amazon.smithy.model.knowledge.TopDownIndex
-import software.amazon.smithy.model.shapes.MemberShape
-import software.amazon.smithy.rust.codegen.rustlang.Attribute
-import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
-import software.amazon.smithy.rust.codegen.rustlang.Feature
-import software.amazon.smithy.rust.codegen.rustlang.RustMetadata
-import software.amazon.smithy.rust.codegen.rustlang.RustModule
-import software.amazon.smithy.rust.codegen.rustlang.RustReservedWords
-import software.amazon.smithy.rust.codegen.rustlang.RustType
-import software.amazon.smithy.rust.codegen.rustlang.RustWriter
-import software.amazon.smithy.rust.codegen.rustlang.asOptional
-import software.amazon.smithy.rust.codegen.rustlang.asType
-import software.amazon.smithy.rust.codegen.rustlang.documentShape
-import software.amazon.smithy.rust.codegen.rustlang.render
-import software.amazon.smithy.rust.codegen.rustlang.rust
-import software.amazon.smithy.rust.codegen.rustlang.rustBlock
-import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate
-import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
-import software.amazon.smithy.rust.codegen.rustlang.stripOuter
-import software.amazon.smithy.rust.codegen.rustlang.writable
-import software.amazon.smithy.rust.codegen.smithy.RustCrate
-import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
-import software.amazon.smithy.rust.codegen.smithy.generators.FluentClientCore
-import software.amazon.smithy.rust.codegen.smithy.generators.LibRsCustomization
-import software.amazon.smithy.rust.codegen.smithy.generators.LibRsSection
-import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig
-import software.amazon.smithy.rust.codegen.smithy.generators.builderSymbol
-import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol
-import software.amazon.smithy.rust.codegen.smithy.generators.setterName
-import software.amazon.smithy.rust.codegen.smithy.rustType
-import software.amazon.smithy.rust.codegen.util.inputShape
-import software.amazon.smithy.rust.codegen.util.outputShape
-import software.amazon.smithy.rust.codegen.util.toSnakeCase
-
-class FluentClientDecorator : RustCodegenDecorator {
-    override val name: String = "FluentClient"
-    override val order: Byte = 0
-
-    override fun extras(protocolConfig: ProtocolConfig, rustCrate: RustCrate) {
-        val module = RustMetadata(additionalAttributes = listOf(Attribute.Cfg.feature("client")), public = true)
-        rustCrate.withModule(RustModule("client", module)) { writer ->
-            FluentClientGenerator(protocolConfig).render(writer)
-        }
-        val awsHyper = "aws-hyper"
-        rustCrate.addFeature(Feature("client", true, listOf(awsHyper)))
-        rustCrate.addFeature(Feature("rustls", default = true, listOf("$awsHyper/rustls")))
-        rustCrate.addFeature(Feature("native-tls", default = false, listOf("$awsHyper/native-tls")))
-    }
-
-    override fun libRsCustomizations(
-        protocolConfig: ProtocolConfig,
-        baseCustomizations: List<LibRsCustomization>
-    ): List<LibRsCustomization> {
-        return baseCustomizations + object : LibRsCustomization() {
-            override fun section(section: LibRsSection) = when (section) {
-                is LibRsSection.Body -> writable {
-                    Attribute.Cfg.feature("client").render(this)
-                    rust("pub use client::Client;")
-                }
-                else -> emptySection
-            }
-        }
-    }
-}
-
-class FluentClientGenerator(protocolConfig: ProtocolConfig) {
-    private val serviceShape = protocolConfig.serviceShape
-    private val operations =
-        TopDownIndex.of(protocolConfig.model).getContainedOperations(serviceShape).sortedBy { it.id }
-    private val symbolProvider = protocolConfig.symbolProvider
-    private val model = protocolConfig.model
-    private val runtimeConfig = protocolConfig.runtimeConfig
-    private val hyperDep = runtimeConfig.awsRuntimeDependency("aws-hyper").copy(optional = true)
-    private val core = FluentClientCore(model)
-
-    fun render(writer: RustWriter) {
-        writer.rustTemplate(
-            """
-            ##[derive(std::fmt::Debug)]
-            pub(crate) struct Handle<C = #{aws_hyper}::DynConnector> {
-                client: #{aws_hyper}::Client<C>,
-                conf: crate::Config
-            }
-
-            ##[derive(Clone, std::fmt::Debug)]
-            pub struct Client<C = #{aws_hyper}::DynConnector> {
-                handle: std::sync::Arc<Handle<C>>
-            }
-        """,
-            "aws_hyper" to hyperDep.asType()
-        )
-        writer.rustBlock("impl<C> Client<C>") {
-            rustTemplate(
-                """
-                pub fn from_conf_conn(conf: crate::Config, conn: C) -> Self {
-                    let client = #{aws_hyper}::Client::new(conn);
-                    Self { handle: std::sync::Arc::new(Handle { client, conf })}
-                }
-
-                pub fn conf(&self) -> &crate::Config {
-                    &self.handle.conf
-                }
-
-            """,
-                "aws_hyper" to hyperDep.asType()
-            )
-        }
-        writer.rustBlock("impl Client") {
-            rustTemplate(
-                """
-                ##[cfg(any(feature = "rustls", feature = "native-tls"))]
-                pub async fn from_env() -> Self {
-                    // backwards compatibility shim
-                    use aws_types::region::ProvideRegion;
-                    let region = aws_types::region::default_provider().region().await;
-                    Self::from_conf(crate::Config::builder().region(region).build())
-                }
-
-                ##[cfg(any(feature = "rustls", feature = "native-tls"))]
-                pub fn from_conf(conf: crate::Config) -> Self {
-                    let client = #{aws_hyper}::Client::https();
-                    Self { handle: std::sync::Arc::new(Handle { client, conf })}
-                }
-
-            """,
-                "aws_hyper" to hyperDep.asType()
-            )
-        }
-        writer.rustBlockTemplate(
-            """
-            impl<C> Client<C>
-                where C: #{aws_hyper}::SmithyConnector,
-            """,
-            "aws_hyper" to hyperDep.asType()
-        ) {
-            operations.forEach { operation ->
-                val name = symbolProvider.toSymbol(operation).name
-                rust(
-                    """
-                    pub fn ${RustReservedWords.escapeIfNeeded(name.toSnakeCase())}(&self) -> fluent_builders::$name<C> {
-                        fluent_builders::$name::new(self.handle.clone())
-                    }"""
-                )
-            }
-        }
-        writer.withModule("fluent_builders") {
-            operations.forEach { operation ->
-                val name = symbolProvider.toSymbol(operation).name
-                val input = operation.inputShape(model)
-                val members: List<MemberShape> = input.allMembers.values.toList()
-
-                rustTemplate(
-                    """
-                    ##[derive(std::fmt::Debug)]
-                    pub struct $name<C = #{aws_hyper}::DynConnector> {
-                        handle: std::sync::Arc<super::Handle<C>>,
-                        inner: #{ty}
-                    }""",
-                    "ty" to input.builderSymbol(symbolProvider),
-                    "aws_hyper" to hyperDep.asType()
-                )
-
-                rustBlock("impl<C> $name<C>") {
-                    rustTemplate(
-                        """
-                        pub(crate) fn new(handle: std::sync::Arc<super::Handle<C>>) -> Self {
-                            Self { handle, inner: Default::default() }
-                        }
-
-                        pub async fn send(self) -> std::result::Result<#{ok}, #{sdk_err}<#{operation_err}>>
-                          where C: #{aws_hyper}::SmithyConnector,
-                        {
-                            let input = self.inner.build().map_err(|err|#{sdk_err}::ConstructionFailure(err.into()))?;
-                            let op = input.make_operation(&self.handle.conf)
-                                .map_err(|err|#{sdk_err}::ConstructionFailure(err.into()))?;
-                            self.handle.client.call(op).await
-                        }
-                        """,
-                        "ok" to symbolProvider.toSymbol(operation.outputShape(model)),
-                        "operation_err" to operation.errorSymbol(symbolProvider),
-                        "sdk_err" to CargoDependency.SmithyHttp(runtimeConfig).asType().copy(name = "result::SdkError"),
-                        "aws_hyper" to hyperDep.asType()
-                    )
-                    members.forEach { member ->
-                        val memberName = symbolProvider.toMemberName(member)
-                        // All fields in the builder are optional
-                        val memberSymbol = symbolProvider.toSymbol(member)
-                        val outerType = memberSymbol.rustType()
-                        when (val coreType = outerType.stripOuter<RustType.Option>()) {
-                            is RustType.Vec -> with(core) { renderVecHelper(member, memberName, coreType) }
-                            is RustType.HashMap -> with(core) { renderMapHelper(member, memberName, coreType) }
-                            else -> {
-                                val signature = when (coreType) {
-                                    is RustType.String,
-                                    is RustType.Box -> "(mut self, input: impl Into<${coreType.render(true)}>) -> Self"
-                                    else -> "(mut self, input: ${coreType.render(true)}) -> Self"
-                                }
-                                documentShape(member, model)
-                                rustBlock("pub fn $memberName$signature") {
-                                    write("self.inner = self.inner.$memberName(input);")
-                                    write("self")
-                                }
-                            }
-                        }
-                        // pure setter
-                        val inputType = outerType.asOptional()
-                        rustBlock("pub fn ${member.setterName()}(mut self, input: ${inputType.render(true)}) -> Self") {
-                            rust(
-                                """
-                                self.inner = self.inner.${member.setterName()}(input);
-                                self
-                                """
-                            )
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/FluentClientDecorator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/FluentClientDecorator.kt
index 0b905bc66..934bcbded 100644
--- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/FluentClientDecorator.kt
+++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/FluentClientDecorator.kt
@@ -17,7 +17,6 @@ import software.amazon.smithy.rust.codegen.rustlang.RustType
 import software.amazon.smithy.rust.codegen.rustlang.RustWriter
 import software.amazon.smithy.rust.codegen.rustlang.asOptional
 import software.amazon.smithy.rust.codegen.rustlang.asType
-import software.amazon.smithy.rust.codegen.rustlang.contains
 import software.amazon.smithy.rust.codegen.rustlang.documentShape
 import software.amazon.smithy.rust.codegen.rustlang.render
 import software.amazon.smithy.rust.codegen.rustlang.rust
@@ -48,7 +47,7 @@ class FluentClientDecorator : RustCodegenDecorator {
 
         val module = RustMetadata(additionalAttributes = listOf(Attribute.Cfg.feature("client")), public = true)
         rustCrate.withModule(RustModule("client", module)) { writer ->
-            FluentClientGenerator(protocolConfig).render(writer)
+            FluentClientGenerator(protocolConfig, includeSmithyGenericClientDocs = true).render(writer)
         }
         val smithyClient = CargoDependency.SmithyClient(protocolConfig.runtimeConfig)
         rustCrate.addFeature(Feature("client", true, listOf(smithyClient.name)))
@@ -76,7 +75,42 @@ class FluentClientDecorator : RustCodegenDecorator {
     }
 }
 
-class FluentClientGenerator(protocolConfig: ProtocolConfig) {
+data class ClientGenerics(
+    val connectorDefault: String? = null,
+    val middlewareDefault: String? = null,
+    val retryDefault: String? = "#{client}::retry::Standard",
+    val codegenScope: List<Pair<String, Any>> = emptyList(),
+) {
+    /** Declaration with defaults set */
+    val decl: String by lazy {
+        "<C${fmtDefault(connectorDefault)}, M${fmtDefault(middlewareDefault)}, R${fmtDefault(retryDefault)}>"
+    }
+
+    /** Instantiation */
+    val inst: String = "<C, M, R>"
+
+    /** Trait bounds */
+    val bounds: String = """
+        C: #{client}::bounds::SmithyConnector,
+        M: #{client}::bounds::SmithyMiddleware<C>,
+        R: #{client}::retry::NewRequestPolicy,
+    """.trimIndent()
+
+    private fun fmtDefault(default: String?): String {
+        return when (default) {
+            null -> ""
+            else -> "= $default"
+        }
+    }
+}
+
+class FluentClientGenerator(
+    protocolConfig: ProtocolConfig,
+    // Whether to include Client construction details that are relevant to generic Smithy generated clients,
+    // but not necessarily relevant to customized clients, such as the ones with the AWS SDK.
+    private val includeSmithyGenericClientDocs: Boolean,
+    private val generics: ClientGenerics = ClientGenerics()
+) {
     private val serviceShape = protocolConfig.serviceShape
     private val operations =
         TopDownIndex.of(protocolConfig.model).getContainedOperations(serviceShape).sortedBy { it.id }
@@ -93,136 +127,34 @@ class FluentClientGenerator(protocolConfig: ProtocolConfig) {
         writer.rustTemplate(
             """
             ##[derive(Debug)]
-            pub(crate) struct Handle<C, M, R> {
-                client: #{client}::Client<C, M, R>,
+            pub(crate) struct Handle${generics.decl} {
+                client: #{client}::Client${generics.inst},
                 conf: crate::Config,
             }
 
-            /// An ergonomic service client for `$humanName`.
-            ///
-            /// This client allows ergonomic access to a `$humanName`-shaped service.
-            /// Each method corresponds to an endpoint defined in the service's Smithy model,
-            /// and the request and response shapes are auto-generated from that same model.
-            ///
-            /// ## Constructing a Client
-            ///
-            /// To construct a client, you need a few different things:
-            ///
-            /// - A [`Config`](crate::Config) that specifies additional configuration
-            ///   required by the service.
-            /// - A connector (`C`) that specifies how HTTP requests are translated
-            ///   into HTTP responses. This will typically be an HTTP client (like
-            ///   `hyper`), though you can also substitute in your own, like a mock
-            ///   mock connector for testing.
-            /// - A "middleware" (`M`) that modifies requests prior to them being
-            ///   sent to the request. Most commonly, middleware will decide what
-            ///   endpoint the requests should be sent to, as well as perform
-            ///   authentcation and authorization of requests (such as SigV4).
-            ///   You can also have middleware that performs request/response
-            ///   tracing, throttling, or other middleware-like tasks.
-            /// - A retry policy (`R`) that dictates the behavior for requests that
-            ///   fail and should (potentially) be retried. The default type is
-            ///   generally what you want, as it implements a well-vetted retry
-            ///   policy described in TODO.
-            ///
-            /// To construct a client, you will generally want to call
-            /// [`Client::with_config`], which takes a [`#{client}::Client`] (a
-            /// Smithy client that isn't specialized to a particular service),
-            /// and a [`Config`](crate::Config). Both of these are constructed using
-            /// the [builder pattern] where you first construct a `Builder` type,
-            /// then configure it with the necessary parameters, and then call
-            /// `build` to construct the finalized output type. The
-            /// [`#{client}::Client`] builder is re-exported in this crate as
-            /// [`Builder`] for convenience.
-            ///
-            /// In _most_ circumstances, you will want to use the following pattern
-            /// to construct a client:
-            ///
-            /// ```
-            /// use $moduleUseName::{Builder, Client, Config};
-            /// let raw_client =
-            ///     Builder::new()
-            ///       .https()
-            /// ##     /*
-            ///       .middleware(/* discussed below */)
-            /// ##     */
-            /// ##     .middleware_fn(|r| r)
-            ///       .build();
-            /// let config = Config::builder().build();
-            /// let client = Client::with_config(raw_client, config);
-            /// ```
-            ///
-            /// For the middleware, you'll want to use whatever matches the
-            /// routing, authentication and authorization required by the target
-            /// service. For example, for the standard AWS SDK which uses
-            /// [SigV4-signed requests], the middleware looks like this:
-            ///
-            // Ignored as otherwise we'd need to pull in all these dev-dependencies.
-            /// ```rust,ignore
-            /// use aws_endpoint::AwsEndpointStage;
-            /// use aws_http::user_agent::UserAgentStage;
-            /// use aws_sig_auth::middleware::SigV4SigningStage;
-            /// use aws_sig_auth::signer::SigV4Signer;
-            /// use smithy_http_tower::map_request::MapRequestLayer;
-            /// use tower::layer::util::Stack;
-            /// use tower::ServiceBuilder;
-            ///
-            /// type AwsMiddlewareStack =
-            ///     Stack<MapRequestLayer<SigV4SigningStage>,
-            ///         Stack<MapRequestLayer<UserAgentStage>,
-            ///             MapRequestLayer<AwsEndpointStage>>>,
-            ///
-            /// ##[derive(Debug, Default)]
-            /// pub struct AwsMiddleware;
-            /// impl<S> tower::Layer<S> for AwsMiddleware {
-            ///     type Service = <AwsMiddlewareStack as tower::Layer<S>>::Service;
-            ///
-            ///     fn layer(&self, inner: S) -> Self::Service {
-            ///         let signer = MapRequestLayer::for_mapper(SigV4SigningStage::new(SigV4Signer::new())); _signer: MapRequestLaye
-            ///         let endpoint_resolver = MapRequestLayer::for_mapper(AwsEndpointStage); _endpoint_resolver: MapRequestLayer<Aw
-            ///         let user_agent = MapRequestLayer::for_mapper(UserAgentStage::new()); _user_agent: MapRequestLayer<UserAgentSt
-            ///         // These layers can be considered as occuring in order, that is:
-            ///         // 1. Resolve an endpoint
-            ///         // 2. Add a user agent
-            ///         // 3. Sign
-            ///         // (4. Dispatch over the wire)
-            ///         ServiceBuilder::new() _ServiceBuilder<Identity>
-            ///             .layer(endpoint_resolver) _ServiceBuilder<Stack<MapRequestLayer<_>, _>>
-            ///             .layer(user_agent) _ServiceBuilder<Stack<MapRequestLayer<_>, _>>
-            ///             .layer(signer) _ServiceBuilder<Stack<MapRequestLayer<_>, _>>
-            ///             .service(inner)
-            ///     }
-            /// }
-            /// ```
-            ///
-            /// ## Using a Client
-            ///
-            /// Once you have a client set up, you can access the service's endpoints
-            /// by calling the appropriate method on [`Client`]. Each such method
-            /// returns a request builder for that endpoint, with methods for setting
-            /// the various fields of the request. Once your request is complete, use
-            /// the `send` method to send the request. `send` returns a future, which
-            /// you then have to `.await` to get the service's response.
-            ///
-            /// [builder pattern]: https://rust-lang.github.io/api-guidelines/type-safety.html##c-builder
-            /// [SigV4-signed requests]: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
-            ##[derive(Clone, std::fmt::Debug)]
-            pub struct Client<C, M, R = #{client}::retry::Standard> {
-                // TODO: Why Arc<>?
-                handle: std::sync::Arc<Handle<C, M, R>>
+            ${clientDocComments()}
+            ##[derive(std::fmt::Debug)]
+            pub struct Client${generics.decl} {
+                handle: std::sync::Arc<Handle${generics.inst}>
+            }
+
+            impl${generics.inst} std::clone::Clone for Client${generics.inst} {
+                fn clone(&self) -> Self {
+                    Self { handle: self.handle.clone() }
+                }
             }
 
             ##[doc(inline)]
             pub use #{client}::Builder;
 
-            impl<C, M, R> From<#{client}::Client<C, M, R>> for Client<C, M, R> {
-                fn from(client: #{client}::Client<C, M, R>) -> Self {
+            impl${generics.inst} From<#{client}::Client${generics.inst}> for Client${generics.inst} {
+                fn from(client: #{client}::Client${generics.inst}) -> Self {
                     Self::with_config(client, crate::Config::builder().build())
                 }
             }
 
-            impl<C, M, R> Client<C, M, R> {
-                pub fn with_config(client: #{client}::Client<C, M, R>, conf: crate::Config) -> Self {
+            impl${generics.inst} Client${generics.inst} {
+                pub fn with_config(client: #{client}::Client${generics.inst}, conf: crate::Config) -> Self {
                     Self {
                         handle: std::sync::Arc::new(Handle {
                             client,
@@ -235,26 +167,22 @@ class FluentClientGenerator(protocolConfig: ProtocolConfig) {
                     &self.handle.conf
                 }
             }
-        """,
-            "client" to clientDep.asType()
+            """,
+            "client" to clientDep.asType(),
+            *generics.codegenScope.toTypedArray()
         )
         writer.rustBlockTemplate(
-            """
-            impl<C, M, R> Client<C, M, R>
-              where
-                C: #{client}::bounds::SmithyConnector,
-                M: #{client}::bounds::SmithyMiddleware<C>,
-                R: #{client}::retry::NewRequestPolicy,
-            """,
+            "impl${generics.inst} Client${generics.inst} where ${generics.bounds}",
             "client" to clientDep.asType(),
         ) {
             operations.forEach { operation ->
                 val name = symbolProvider.toSymbol(operation).name
                 rust(
                     """
-                    pub fn ${RustReservedWords.escapeIfNeeded(name.toSnakeCase())}(&self) -> fluent_builders::$name<C, M, R> {
+                    pub fn ${RustReservedWords.escapeIfNeeded(name.toSnakeCase())}(&self) -> fluent_builders::$name${generics.inst} {
                         fluent_builders::$name::new(self.handle.clone())
-                    }"""
+                    }
+                    """
                 )
             }
         }
@@ -264,41 +192,41 @@ class FluentClientGenerator(protocolConfig: ProtocolConfig) {
                 val input = operation.inputShape(model)
                 val members: List<MemberShape> = input.allMembers.values.toList()
 
-                rust(
+                rustTemplate(
                     """
-                ##[derive(std::fmt::Debug)]
-                pub struct $name<C, M, R> {
-                    handle: std::sync::Arc<super::Handle<C, M, R>>,
-                    inner: #T
-                }""",
-                    input.builderSymbol(symbolProvider)
+                    ##[derive(std::fmt::Debug)]
+                    pub struct $name${generics.decl} {
+                        handle: std::sync::Arc<super::Handle${generics.inst}>,
+                        inner: #{Inner}
+                    }
+                    """,
+                    "Inner" to input.builderSymbol(symbolProvider),
+                    *generics.codegenScope.toTypedArray(),
+                    "client" to clientDep.asType(),
                 )
 
                 rustBlockTemplate(
-                    """
-                    impl<C, M, R> $name<C, M, R>
-                      where
-                        C: #{client}::bounds::SmithyConnector,
-                        M: #{client}::bounds::SmithyMiddleware<C>,
-                        R: #{client}::retry::NewRequestPolicy,
-                    """,
+                    "impl${generics.inst} $name${generics.inst} where ${generics.bounds}",
                     "client" to clientDep.asType(),
                 ) {
                     rustTemplate(
                         """
-                    pub(crate) fn new(handle: std::sync::Arc<super::Handle<C, M, R>>) -> Self {
-                        Self { handle, inner: Default::default() }
-                    }
-
-                    pub async fn send(self) -> std::result::Result<#{ok}, #{sdk_err}<#{operation_err}>> where
-                        R::Policy: #{client}::bounds::SmithyRetryPolicy<#{input}OperationOutputAlias, #{ok}, #{operation_err}, #{input}OperationRetryAlias>,
-                    {
-                        let input = self.inner.build().map_err(|err|#{sdk_err}::ConstructionFailure(err.into()))?;
-                        let op = input.make_operation(&self.handle.conf)
-                            .map_err(|err|#{sdk_err}::ConstructionFailure(err.into()))?;
-                        self.handle.client.call(op).await
-                    }
-                    """,
+                        pub(crate) fn new(handle: std::sync::Arc<super::Handle${generics.inst}>) -> Self {
+                            Self { handle, inner: Default::default() }
+                        }
+                        pub async fn send(self) -> std::result::Result<#{ok}, #{sdk_err}<#{operation_err}>>
+                        where
+                            R::Policy: #{client}::bounds::SmithyRetryPolicy<#{input}OperationOutputAlias,
+                            #{ok},
+                            #{operation_err},
+                            #{input}OperationRetryAlias>,
+                        {
+                            let input = self.inner.build().map_err(|err|#{sdk_err}::ConstructionFailure(err.into()))?;
+                            let op = input.make_operation(&self.handle.conf)
+                                .map_err(|err|#{sdk_err}::ConstructionFailure(err.into()))?;
+                            self.handle.client.call(op).await
+                        }
+                        """,
                         "input" to symbolProvider.toSymbol(operation.inputShape(model)),
                         "ok" to symbolProvider.toSymbol(operation.outputShape(model)),
                         "operation_err" to operation.errorSymbol(symbolProvider),
@@ -341,4 +269,129 @@ class FluentClientGenerator(protocolConfig: ProtocolConfig) {
             }
         }
     }
+
+    fun clientDocComments(): String {
+        val docs = StringBuilder()
+
+        docs.append(
+            """
+            /// An ergonomic service client for `$humanName`.
+            ///
+            /// This client allows ergonomic access to a `$humanName`-shaped service.
+            /// Each method corresponds to an endpoint defined in the service's Smithy model,
+            /// and the request and response shapes are auto-generated from that same model.
+            ///"""
+        )
+        if (includeSmithyGenericClientDocs) {
+            docs.append(
+                """
+                /// ## Constructing a Client
+                ///
+                /// To construct a client, you need a few different things:
+                ///
+                /// - A [`Config`](crate::Config) that specifies additional configuration
+                ///   required by the service.
+                /// - A connector (`C`) that specifies how HTTP requests are translated
+                ///   into HTTP responses. This will typically be an HTTP client (like
+                ///   `hyper`), though you can also substitute in your own, like a mock
+                ///   mock connector for testing.
+                /// - A "middleware" (`M`) that modifies requests prior to them being
+                ///   sent to the request. Most commonly, middleware will decide what
+                ///   endpoint the requests should be sent to, as well as perform
+                ///   authentcation and authorization of requests (such as SigV4).
+                ///   You can also have middleware that performs request/response
+                ///   tracing, throttling, or other middleware-like tasks.
+                /// - A retry policy (`R`) that dictates the behavior for requests that
+                ///   fail and should (potentially) be retried. The default type is
+                ///   generally what you want, as it implements a well-vetted retry
+                ///   policy described in TODO.
+                ///
+                /// To construct a client, you will generally want to call
+                /// [`Client::with_config`], which takes a [`#{client}::Client`] (a
+                /// Smithy client that isn't specialized to a particular service),
+                /// and a [`Config`](crate::Config). Both of these are constructed using
+                /// the [builder pattern] where you first construct a `Builder` type,
+                /// then configure it with the necessary parameters, and then call
+                /// `build` to construct the finalized output type. The
+                /// [`#{client}::Client`] builder is re-exported in this crate as
+                /// [`Builder`] for convenience.
+                ///
+                /// In _most_ circumstances, you will want to use the following pattern
+                /// to construct a client:
+                ///
+                /// ```
+                /// use $moduleUseName::{Builder, Client, Config};
+                /// let raw_client =
+                ///     Builder::new()
+                ///       .https()
+                /// ##     /*
+                ///       .middleware(/* discussed below */)
+                /// ##     */
+                /// ##     .middleware_fn(|r| r)
+                ///       .build();
+                /// let config = Config::builder().build();
+                /// let client = Client::with_config(raw_client, config);
+                /// ```
+                ///
+                /// For the middleware, you'll want to use whatever matches the
+                /// routing, authentication and authorization required by the target
+                /// service. For example, for the standard AWS SDK which uses
+                /// [SigV4-signed requests], the middleware looks like this:
+                ///
+                // Ignored as otherwise we'd need to pull in all these dev-dependencies.
+                /// ```rust,ignore
+                /// use aws_endpoint::AwsEndpointStage;
+                /// use aws_http::user_agent::UserAgentStage;
+                /// use aws_sig_auth::middleware::SigV4SigningStage;
+                /// use aws_sig_auth::signer::SigV4Signer;
+                /// use smithy_http_tower::map_request::MapRequestLayer;
+                /// use tower::layer::util::Stack;
+                /// use tower::ServiceBuilder;
+                ///
+                /// type AwsMiddlewareStack =
+                ///     Stack<MapRequestLayer<SigV4SigningStage>,
+                ///         Stack<MapRequestLayer<UserAgentStage>,
+                ///             MapRequestLayer<AwsEndpointStage>>>,
+                ///
+                /// ##[derive(Debug, Default)]
+                /// pub struct AwsMiddleware;
+                /// impl<S> tower::Layer<S> for AwsMiddleware {
+                ///     type Service = <AwsMiddlewareStack as tower::Layer<S>>::Service;
+                ///
+                ///     fn layer(&self, inner: S) -> Self::Service {
+                ///         let signer = MapRequestLayer::for_mapper(SigV4SigningStage::new(SigV4Signer::new())); _signer: MapRequestLaye
+                ///         let endpoint_resolver = MapRequestLayer::for_mapper(AwsEndpointStage); _endpoint_resolver: MapRequestLayer<Aw
+                ///         let user_agent = MapRequestLayer::for_mapper(UserAgentStage::new()); _user_agent: MapRequestLayer<UserAgentSt
+                ///         // These layers can be considered as occuring in order, that is:
+                ///         // 1. Resolve an endpoint
+                ///         // 2. Add a user agent
+                ///         // 3. Sign
+                ///         // (4. Dispatch over the wire)
+                ///         ServiceBuilder::new() _ServiceBuilder<Identity>
+                ///             .layer(endpoint_resolver) _ServiceBuilder<Stack<MapRequestLayer<_>, _>>
+                ///             .layer(user_agent) _ServiceBuilder<Stack<MapRequestLayer<_>, _>>
+                ///             .layer(signer) _ServiceBuilder<Stack<MapRequestLayer<_>, _>>
+                ///             .service(inner)
+                ///     }
+                /// }
+                /// ```
+                ///"""
+            )
+        }
+        docs.append(
+            """
+            /// ## Using a Client
+            ///
+            /// Once you have a client set up, you can access the service's endpoints
+            /// by calling the appropriate method on [`Client`]. Each such method
+            /// returns a request builder for that endpoint, with methods for setting
+            /// the various fields of the request. Once your request is complete, use
+            /// the `send` method to send the request. `send` returns a future, which
+            /// you then have to `.await` to get the service's response.
+            ///
+            /// [builder pattern]: https://rust-lang.github.io/api-guidelines/type-safety.html##c-builder
+            /// [SigV4-signed requests]: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html"""
+        )
+        return docs.toString()
+    }
 }
diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt
index ce8bc7baa..ce69bd27e 100644
--- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt
+++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt
@@ -77,8 +77,13 @@ abstract class HttpProtocolGenerator(
         // on these aliases.
         val operationTypeOutput = buildOperationTypeOutput(inputWriter, operationShape)
         val operationTypeRetry = buildOperationTypeRetry(inputWriter, customizations)
-        inputWriter.rust("##[doc(hidden)] pub type ${inputShape.id.name}OperationOutputAlias= $operationTypeOutput;")
-        inputWriter.rust("##[doc(hidden)] pub type ${inputShape.id.name}OperationRetryAlias = $operationTypeRetry;")
+        val inputPrefix = symbolProvider.toSymbol(inputShape).name
+        inputWriter.rust(
+            """
+            ##[doc(hidden)] pub type ${inputPrefix}OperationOutputAlias = $operationTypeOutput;
+            ##[doc(hidden)] pub type ${inputPrefix}OperationRetryAlias = $operationTypeRetry;
+            """
+        )
 
         // impl OperationInputShape { ... }
         inputWriter.implBlock(inputShape, symbolProvider) {
-- 
GitLab