Unverified Commit f9d3f28c authored by John DiSanti's avatar John DiSanti Committed by GitHub
Browse files

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: default avatarRussell Cohen <rcoh@amazon.com>
parent 5c0e2941
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -20,7 +20,7 @@ val DECORATORS = listOf(
    SigV4SigningDecorator(),
    RetryPolicyDecorator(),
    IntegrationTestDecorator(),
    FluentClientDecorator(),
    AwsFluentClientDecorator(),
    CrateLicenseDecorator(),

    // Service specific decorators
+120 −0
Original line number Diff line number Diff line
@@ -5,51 +5,64 @@

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.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.FluentClientCore
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
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 {
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).render(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)))
        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")))
    }
@@ -70,49 +83,20 @@ class FluentClientDecorator : RustCodegenDecorator {
    }
}

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)

private class AwsFluentClientExtensions(private val types: Types) {
    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>") {
        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 }) }
                }

                pub fn conf(&self) -> &crate::Config {
                    &self.handle.conf
                }

                """,
                "aws_hyper" to hyperDep.asType()
                "aws_hyper" to types.awsHyper,
            )
        }
        writer.rustBlock("impl Client") {
        writer.rustBlock("impl Client<smithy_client::erase::DynConnector, aws_hyper::AwsMiddleware, smithy_client::retry::Standard>") {
            rustTemplate(
                """
                ##[cfg(any(feature = "rustls", feature = "native-tls"))]
@@ -128,100 +112,9 @@ class FluentClientGenerator(protocolConfig: ProtocolConfig) {
                    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()
                "aws_hyper" to types.awsHyper,
            )
                    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
                                """
                            )
                        }
                    }
                }
            }
        }
    }
}
+7 −2
Original line number Diff line number Diff line
@@ -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) {