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 fc79c1e6b48e2065be8a5b3e8a2c5e113c73f2ab..a97fd8d3f710c2e4b0a848551732d25608f783fd 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 0000000000000000000000000000000000000000..f6b550457c2404525ac82098790e296c86d63103 --- /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 + ): List { + 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 Client") { + 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") { + 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 e3913d1a8d289e05c2a6b9317c77ec9a9c178928..0000000000000000000000000000000000000000 --- 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 - ): List { - 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 { - client: #{aws_hyper}::Client, - conf: crate::Config - } - - ##[derive(Clone, std::fmt::Debug)] - pub struct Client { - handle: std::sync::Arc> - } - """, - "aws_hyper" to hyperDep.asType() - ) - writer.rustBlock("impl Client") { - 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 Client - 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 { - 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 = input.allMembers.values.toList() - - rustTemplate( - """ - ##[derive(std::fmt::Debug)] - pub struct $name { - handle: std::sync::Arc>, - inner: #{ty} - }""", - "ty" to input.builderSymbol(symbolProvider), - "aws_hyper" to hyperDep.asType() - ) - - rustBlock("impl $name") { - rustTemplate( - """ - pub(crate) fn new(handle: std::sync::Arc>) -> 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()) { - 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 0b905bc6644224d5a410900d6995f3a56f2962b1..934bcbdede2ecede50ff72e9a95b002f4d998de9 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> = emptyList(), +) { + /** Declaration with defaults set */ + val decl: String by lazy { + "" + } + + /** Instantiation */ + val inst: String = "" + + /** Trait bounds */ + val bounds: String = """ + C: #{client}::bounds::SmithyConnector, + M: #{client}::bounds::SmithyMiddleware, + 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 { - client: #{client}::Client, + 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, - /// Stack, - /// MapRequestLayer>>, - /// - /// ##[derive(Debug, Default)] - /// pub struct AwsMiddleware; - /// impl tower::Layer for AwsMiddleware { - /// type Service = >::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 - /// .layer(endpoint_resolver) _ServiceBuilder, _>> - /// .layer(user_agent) _ServiceBuilder, _>> - /// .layer(signer) _ServiceBuilder, _>> - /// .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 { - // TODO: Why Arc<>? - handle: std::sync::Arc> + ${clientDocComments()} + ##[derive(std::fmt::Debug)] + pub struct Client${generics.decl} { + handle: std::sync::Arc + } + + 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 From<#{client}::Client> for Client { - fn from(client: #{client}::Client) -> 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 Client { - pub fn with_config(client: #{client}::Client, 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 Client - where - C: #{client}::bounds::SmithyConnector, - M: #{client}::bounds::SmithyMiddleware, - 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 { + 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 = input.allMembers.values.toList() - rust( + rustTemplate( """ - ##[derive(std::fmt::Debug)] - pub struct $name { - handle: std::sync::Arc>, - inner: #T - }""", - input.builderSymbol(symbolProvider) + ##[derive(std::fmt::Debug)] + pub struct $name${generics.decl} { + handle: std::sync::Arc, + inner: #{Inner} + } + """, + "Inner" to input.builderSymbol(symbolProvider), + *generics.codegenScope.toTypedArray(), + "client" to clientDep.asType(), ) rustBlockTemplate( - """ - impl $name - where - C: #{client}::bounds::SmithyConnector, - M: #{client}::bounds::SmithyMiddleware, - 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>) -> 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) -> 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, + /// Stack, + /// MapRequestLayer>>, + /// + /// ##[derive(Debug, Default)] + /// pub struct AwsMiddleware; + /// impl tower::Layer for AwsMiddleware { + /// type Service = >::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 + /// .layer(endpoint_resolver) _ServiceBuilder, _>> + /// .layer(user_agent) _ServiceBuilder, _>> + /// .layer(signer) _ServiceBuilder, _>> + /// .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 ce8bc7baa6b959f2da20b0c156f851aa915fc5fc..ce69bd27e7ad434450fab2140be1d65158ef891d 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) {