diff --git a/aws/rust-runtime/aws-endpoint/src/lib.rs b/aws/rust-runtime/aws-endpoint/src/lib.rs index 37934a96eb..769501571e 100644 --- a/aws/rust-runtime/aws-endpoint/src/lib.rs +++ b/aws/rust-runtime/aws-endpoint/src/lib.rs @@ -161,7 +161,7 @@ pub fn set_endpoint_resolver(config: &mut PropertyBag, provider: AwsEndpointReso /// 3. Apply the endpoint to the URI in the request /// 4. Set the `SigningRegion` and `SigningService` in the property bag to drive downstream /// signing middleware. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct AwsEndpointStage; #[derive(Debug)] diff --git a/aws/rust-runtime/aws-http/src/user_agent.rs b/aws/rust-runtime/aws-http/src/user_agent.rs index cc5efddccc..ac811a95bb 100644 --- a/aws/rust-runtime/aws-http/src/user_agent.rs +++ b/aws/rust-runtime/aws-http/src/user_agent.rs @@ -216,7 +216,7 @@ impl Display for ExecEnvMetadata { } #[non_exhaustive] -#[derive(Default, Clone)] +#[derive(Default, Clone, Debug)] pub struct UserAgentStage; impl UserAgentStage { diff --git a/aws/rust-runtime/aws-hyper/Cargo.toml b/aws/rust-runtime/aws-hyper/Cargo.toml index f982623235..5ee7f8609b 100644 --- a/aws/rust-runtime/aws-hyper/Cargo.toml +++ b/aws/rust-runtime/aws-hyper/Cargo.toml @@ -10,8 +10,8 @@ license = "Apache-2.0" [features] test-util = ["protocol-test-helpers"] default = ["test-util"] -native-tls = ["hyper-tls"] -rustls = ["hyper-rustls"] +native-tls = ["hyper-tls", "smithy-client/native-tls"] +rustls = ["hyper-rustls", "smithy-client/rustls"] [dependencies] hyper = { version = "0.14.2", features = ["client", "http1", "http2", "tcp", "runtime"] } @@ -28,6 +28,7 @@ http-body = "0.4.0" smithy-http = { path = "../../../rust-runtime/smithy-http" } smithy-types = { path = "../../../rust-runtime/smithy-types" } smithy-http-tower = { path = "../../../rust-runtime/smithy-http-tower" } +smithy-client = { path = "../../../rust-runtime/smithy-client" } fastrand = "1.4.0" tokio = { version = "1", features = ["time"] } diff --git a/aws/rust-runtime/aws-hyper/src/conn.rs b/aws/rust-runtime/aws-hyper/src/conn.rs deleted file mode 100644 index bb5219d695..0000000000 --- a/aws/rust-runtime/aws-hyper/src/conn.rs +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use crate::BoxError; -use http::Request; -use hyper::client::ResponseFuture; -use hyper::Response; -use smithy_http::body::SdkBody; -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; -use tower::Service; - -#[derive(Clone)] -pub struct Standard(Connector); - -impl Standard { - /// An https connection - /// - /// If the `rustls` feature is enabled, this will use `rustls`. - /// If the ONLY the `native-tls` feature is enabled, this will use `native-tls`. - /// If both features are enabled, this will use `rustls` - #[cfg(any(feature = "native-tls", feature = "rustls"))] - pub fn https() -> Self { - #[cfg(feature = "rustls")] - { - Self::rustls() - } - - // If we are compiling this function & rustls is not enabled, then native-tls MUST be enabled - #[cfg(not(feature = "rustls"))] - { - Self::native_tls() - } - } - - #[cfg(feature = "rustls")] - pub fn rustls() -> Self { - let https = hyper_rustls::HttpsConnector::with_native_roots(); - let client = hyper::Client::builder().build::<_, SdkBody>(https); - Self(Connector::RustlsHttps(client)) - } - - #[cfg(feature = "native-tls")] - pub fn native_tls() -> Self { - let https = hyper_tls::HttpsConnector::new(); - let client = hyper::Client::builder().build::<_, SdkBody>(https); - Self(Connector::NativeHttps(client)) - } - - /// A connection based on the provided `impl HttpService` - /// - /// Generally, [`Standard::https()`](Standard::https) should be used. This constructor is intended to support - /// using things like [`TestConnection`](crate::test_connection::TestConnection) or alternative - /// http implementations. - pub fn new(connector: impl HttpService + 'static) -> Self { - Self(Connector::Dyn(Box::new(connector))) - } -} - -/// An Http connection type for most use cases -/// -/// This supports three options: -/// 1. HTTPS -/// 2. A `TestConnection` -/// 3. Any implementation of the `HttpService` trait -/// -/// This is designed to be used with [`aws_hyper::Client`](crate::Client) as a connector. -#[derive(Clone)] -enum Connector { - /// An Https Connection - /// - /// This is the correct connection for use cases talking to real AWS services. - #[cfg(feature = "native-tls")] - NativeHttps(hyper::Client, SdkBody>), - - /// An Https Connection - /// - /// This is the correct connection for use cases talking to real AWS services. - #[cfg(feature = "rustls")] - RustlsHttps(hyper::Client, SdkBody>), - - /// A generic escape hatch - /// - /// This enables using any implementation of the HttpService trait. This allows using a totally - /// separate HTTP stack or your own custom `TestConnection`. - Dyn(Box), -} - -impl Clone for Box { - fn clone(&self) -> Self { - self.clone_box() - } -} - -pub trait HttpService: Send + Sync { - /// Return whether this service is ready to accept a request - /// - /// See [`Service::poll_ready`](tower::Service::poll_ready) - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll>; - - /// Call this service and return a response - /// - /// See [`Service::call`](tower::Service::call) - fn call( - &mut self, - req: http::Request, - ) -> Pin, BoxError>> + Send>>; - - /// Return a Boxed-clone of this service - /// - /// `aws_hyper::Client` will clone the inner service for each request so this should be a cheap - /// clone operation. - fn clone_box(&self) -> Box; -} - -/// Reverse implementation: If you have a correctly shaped tower service, it _is_ an `HttpService` -/// -/// This is to facilitate ease of use for people using `Standard::Dyn` -impl HttpService for S -where - S: Service, Response = http::Response> - + Send - + Sync - + Clone - + 'static, - S::Error: Into + Send + Sync + 'static, - S::Future: Send + 'static, -{ - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - Service::poll_ready(self, cx).map_err(|err| err.into()) - } - - fn call( - &mut self, - req: Request, - ) -> Pin, BoxError>> + Send>> { - let fut = Service::call(self, req); - let fut = async move { - fut.await - .map(|res| res.map(SdkBody::from)) - .map_err(|err| err.into()) - }; - Box::pin(fut) - } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } -} - -impl tower::Service> for Standard { - type Response = http::Response; - type Error = BoxError; - type Future = StandardFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - match &mut self.0 { - #[cfg(feature = "native-tls")] - Connector::NativeHttps(https) => { - Service::poll_ready(https, cx).map_err(|err| err.into()) - } - #[cfg(feature = "rustls")] - Connector::RustlsHttps(https) => { - Service::poll_ready(https, cx).map_err(|err| err.into()) - } - Connector::Dyn(conn) => conn.poll_ready(cx), - } - } - - fn call(&mut self, req: http::Request) -> Self::Future { - match &mut self.0 { - #[cfg(feature = "native-tls")] - Connector::NativeHttps(https) => StandardFuture::Https(Service::call(https, req)), - #[cfg(feature = "rustls")] - Connector::RustlsHttps(https) => StandardFuture::Https(Service::call(https, req)), - Connector::Dyn(conn) => StandardFuture::Dyn(conn.call(req)), - } - } -} - -/// Future returned by `Standard` when used as a tower::Service -#[pin_project::pin_project(project = FutProj)] -pub enum StandardFuture { - Https(#[pin] ResponseFuture), - Dyn(#[pin] Pin, BoxError>> + Send>>), -} - -impl Future for StandardFuture { - type Output = Result, BoxError>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match self.project() { - FutProj::Https(fut) => fut - .poll(cx) - .map(|resp| resp.map(|res| res.map(SdkBody::from))) - .map_err(|err| err.into()), - FutProj::Dyn(dyn_fut) => dyn_fut.poll(cx), - } - } -} diff --git a/aws/rust-runtime/aws-hyper/src/lib.rs b/aws/rust-runtime/aws-hyper/src/lib.rs index ee2676bb7e..e25ff9fde1 100644 --- a/aws/rust-runtime/aws-hyper/src/lib.rs +++ b/aws/rust-runtime/aws-hyper/src/lib.rs @@ -3,41 +3,54 @@ * SPDX-License-Identifier: Apache-2.0. */ -pub mod conn; -mod retry; -#[cfg(feature = "test-util")] -pub mod test_connection; +#[doc(inline)] +pub use smithy_client::test_connection; -pub use retry::RetryConfig; +pub use smithy_client::retry::Config as RetryConfig; -use crate::conn::Standard; -use crate::retry::RetryHandlerFactory; 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::body::SdkBody; -use smithy_http::operation::Operation; -use smithy_http::response::ParseHttpResponse; pub use smithy_http::result::{SdkError, SdkSuccess}; -use smithy_http::retry::ClassifyResponse; -use smithy_http_tower::dispatch::DispatchLayer; use smithy_http_tower::map_request::MapRequestLayer; -use smithy_http_tower::parse_response::ParseResponseLayer; -use smithy_types::retry::ProvideErrorKind; -use std::error::Error; -use std::fmt; -use std::fmt::{Debug, Formatter}; -use tower::{Service, ServiceBuilder, ServiceExt}; +use std::fmt::Debug; +use tower::layer::util::Stack; +use tower::ServiceBuilder; -type BoxError = Box; -pub type StandardClient = Client; +type AwsMiddlewareStack = Stack< + MapRequestLayer, + Stack, MapRequestLayer>, +>; + +#[derive(Debug, Default)] +#[non_exhaustive] +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())); + let endpoint_resolver = MapRequestLayer::for_mapper(AwsEndpointStage); + let user_agent = MapRequestLayer::for_mapper(UserAgentStage::new()); + // 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() + .layer(endpoint_resolver) + .layer(user_agent) + .layer(signer) + .service(inner) + } +} /// AWS Service Client /// /// Hyper-based AWS Service Client. Most customers will want to construct a client with -/// [`Client::https()`](Client::https). For testing & other more advanced use cases, a custom -/// connector may be used via [`Client::new(connector)`](Client::new). +/// [`Client::https`](smithy_client::Client::https). For testing & other more advanced use cases, a +/// custom connector may be used via [`Client::new(connector)`](smithy_client::Client::new). /// /// The internal connector must implement the following trait bound to be used to dispatch requests: /// ```rust,ignore @@ -48,116 +61,55 @@ pub type StandardClient = Client; /// S::Error: Into + Send + Sync + 'static, /// S::Future: Send + 'static, /// ``` -pub struct Client { - inner: S, - retry_handler: RetryHandlerFactory, -} +pub type Client = smithy_client::Client; -impl Debug for Client { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut formatter = f.debug_struct("Client"); - formatter.field("retry_handler", &self.retry_handler); - formatter.finish() - } -} +#[doc(inline)] +pub use smithy_client::erase::DynConnector; +pub type StandardClient = Client; -impl Client { - /// Construct a new `Client` with a custom connector - pub fn new(connector: S) -> Self { - Client { - inner: connector, - retry_handler: RetryHandlerFactory::new(RetryConfig::default()), - } - } +#[doc(inline)] +pub use smithy_client::bounds::SmithyConnector; - pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self { - self.retry_handler.with_config(retry_config); - self - } -} - -impl Client { - /// Construct an `https` based client - #[cfg(any(feature = "native-tls", feature = "rustls"))] - pub fn https() -> StandardClient { - Client { - inner: Standard::https(), - retry_handler: RetryHandlerFactory::new(RetryConfig::default()), - } - } -} +/// AWS Service Client builder. +/// +/// See [`smithy_client::Builder`] for details. +pub type Builder = smithy_client::Builder; -impl Client -where - S: Service, Response = http::Response> + Send + Clone + 'static, - S::Error: Into + Send + Sync + 'static, - S::Future: Send + 'static, -{ - /// Dispatch this request to the network - /// - /// For ergonomics, this does not include the raw response for successful responses. To - /// access the raw response use `call_raw`. - pub async fn call(&self, input: Operation) -> Result> - where - O: ParseHttpResponse> + Send + Sync + Clone + 'static, - E: Error + ProvideErrorKind, - Retry: ClassifyResponse, SdkError>, - { - self.call_raw(input).await.map(|res| res.parsed) - } +/// Construct an `https` based client +/// +/// If the `rustls` feature is enabled, this will use `rustls`. +/// If the ONLY the `native-tls` feature is enabled, this will use `native-tls`. +/// If both features are enabled, this will use `rustls` +#[cfg(any(feature = "native-tls", feature = "rustls"))] +pub fn https() -> StandardClient { + #[cfg(feature = "rustls")] + let with_https = |b: Builder<_>| b.rustls(); + // If we are compiling this function & rustls is not enabled, then native-tls MUST be enabled + #[cfg(not(feature = "rustls"))] + let with_https = |b: Builder<_>| b.native_tls(); - /// Dispatch this request to the network - /// - /// The returned result contains the raw HTTP response which can be useful for debugging or implementing - /// unsupported features. - pub async fn call_raw( - &self, - input: Operation, - ) -> Result, SdkError> - where - O: ParseHttpResponse> + Send + Sync + Clone + 'static, - E: Error + ProvideErrorKind, - Retry: ClassifyResponse, SdkError>, - { - let signer = MapRequestLayer::for_mapper(SigV4SigningStage::new(SigV4Signer::new())); - let endpoint_resolver = MapRequestLayer::for_mapper(AwsEndpointStage); - let user_agent = MapRequestLayer::for_mapper(UserAgentStage::new()); - let inner = self.inner.clone(); - let mut svc = ServiceBuilder::new() - // Create a new request-scoped policy - .retry(self.retry_handler.new_handler()) - .layer(ParseResponseLayer::::new()) - // 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 - .layer(endpoint_resolver) - .layer(user_agent) - .layer(signer) - .layer(DispatchLayer::new()) - .service(inner); - svc.ready().await?.call(input).await - } + with_https(smithy_client::Builder::new()) + .build() + .into_dyn_connector() } -#[cfg(test)] -mod tests { - +mod static_tests { #[cfg(any(feature = "rustls", feature = "native-tls"))] - #[test] + #[allow(dead_code)] fn construct_default_client() { let c = crate::Client::https(); fn is_send_sync(_c: T) {} is_send_sync(c); } +} +#[cfg(test)] +mod tests { #[cfg(any(feature = "rustls", feature = "native-tls"))] #[test] fn client_debug_includes_retry_info() { let client = crate::Client::https(); let s = format!("{:?}", client); - assert!(s.contains("RetryConfig")); assert!(s.contains("quota_available")); } } diff --git a/aws/rust-runtime/aws-sig-auth/src/middleware.rs b/aws/rust-runtime/aws-sig-auth/src/middleware.rs index 7021813037..0283511e1d 100644 --- a/aws/rust-runtime/aws-sig-auth/src/middleware.rs +++ b/aws/rust-runtime/aws-sig-auth/src/middleware.rs @@ -26,7 +26,7 @@ use std::time::SystemTime; /// The following fields MAY be present in the property bag: /// - [`SystemTime`](SystemTime): The timestamp to use when signing the request. If this field is not present /// [`SystemTime::now`](SystemTime::now) will be used. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct SigV4SigningStage { signer: SigV4Signer, } diff --git a/aws/rust-runtime/aws-sig-auth/src/signer.rs b/aws/rust-runtime/aws-sig-auth/src/signer.rs index 022fb91050..4a34d3d5e9 100644 --- a/aws/rust-runtime/aws-sig-auth/src/signer.rs +++ b/aws/rust-runtime/aws-sig-auth/src/signer.rs @@ -10,6 +10,7 @@ use aws_types::SigningService; use http::header::HeaderName; use smithy_http::body::SdkBody; use std::error::Error; +use std::fmt; use std::time::SystemTime; #[derive(Eq, PartialEq, Clone, Copy)] @@ -86,6 +87,13 @@ pub struct SigV4Signer { _private: (), } +impl fmt::Debug for SigV4Signer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut formatter = f.debug_struct("SigV4Signer"); + formatter.finish() + } +} + pub type SigningError = Box; impl SigV4Signer { 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 index c6d41b2b49..b88a3b60b7 100644 --- 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 @@ -20,6 +20,7 @@ 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 @@ -80,48 +81,64 @@ class FluentClientGenerator(protocolConfig: ProtocolConfig) { writer.rustTemplate( """ ##[derive(std::fmt::Debug)] - pub(crate) struct Handle { - client: #{aws_hyper}::Client<#{aws_hyper}::conn::Standard>, + pub(crate) struct Handle { + client: #{aws_hyper}::Client, conf: crate::Config } ##[derive(Clone, std::fmt::Debug)] - pub struct Client { - handle: std::sync::Arc + 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 fn from_env() -> Self { - Self::from_conf_conn(crate::Config::builder().build(), #{aws_hyper}::conn::Standard::https()) + Self::from_conf(crate::Config::builder().build()) } ##[cfg(any(feature = "rustls", feature = "native-tls"))] pub fn from_conf(conf: crate::Config) -> Self { - Self::from_conf_conn(conf, #{aws_hyper}::conn::Standard::https()) - } - - pub fn from_conf_conn(conf: crate::Config, conn: #{aws_hyper}::conn::Standard) -> Self { - let client = #{aws_hyper}::Client::new(conn); + let client = #{aws_hyper}::Client::https(); Self { handle: std::sync::Arc::new(Handle { client, conf })} } - pub fn conf(&self) -> &crate::Config { - &self.handle.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 ${name.toSnakeCase()}(&self) -> fluent_builders::$name { + pub fn ${name.toSnakeCase()}(&self) -> fluent_builders::$name { fluent_builders::$name::new(self.handle.clone()) }""" ) @@ -133,24 +150,27 @@ 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 + pub struct $name { + handle: std::sync::Arc>, + inner: #{ty} }""", - input.builderSymbol(symbolProvider) + "ty" to input.builderSymbol(symbolProvider), + "aws_hyper" to hyperDep.asType() ) - rustBlock("impl $name") { + rustBlock("impl $name") { rustTemplate( """ - pub(crate) fn new(handle: std::sync::Arc) -> Self { + pub(crate) fn new(handle: std::sync::Arc>) -> Self { Self { handle, inner: Default::default() } } - pub async fn send(self) -> Result<#{ok}, #{sdk_err}<#{operation_err}>> { + pub async fn send(self) -> 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()))?; @@ -159,7 +179,8 @@ class FluentClientGenerator(protocolConfig: ProtocolConfig) { """, "ok" to symbolProvider.toSymbol(operation.outputShape(model)), "operation_err" to operation.errorSymbol(symbolProvider), - "sdk_err" to CargoDependency.SmithyHttp(runtimeConfig).asType().copy(name = "result::SdkError") + "sdk_err" to CargoDependency.SmithyHttp(runtimeConfig).asType().copy(name = "result::SdkError"), + "aws_hyper" to hyperDep.asType() ) members.forEach { member -> val memberName = symbolProvider.toMemberName(member) diff --git a/aws/sdk/build.gradle.kts b/aws/sdk/build.gradle.kts index 6b20283387..af9da338f9 100644 --- a/aws/sdk/build.gradle.kts +++ b/aws/sdk/build.gradle.kts @@ -26,6 +26,7 @@ val runtimeModules = listOf( "smithy-xml", "smithy-http", "smithy-http-tower", + "smithy-client", "protocol-test-helpers" ) val awsModules = listOf("aws-auth", "aws-endpoint", "aws-types", "aws-hyper", "aws-sig-auth", "aws-http") @@ -103,6 +104,9 @@ fun generateSmithyBuild(tests: List): String { "runtimeConfig": { "relativePath": "../" }, + "codegen": { + "includeFluentClient": false + }, "service": "${it.service}", "module": "aws-sdk-${it.module}", "moduleVersion": "0.0.7-alpha", diff --git a/aws/sdk/examples/dynamo-movies/src/main.rs b/aws/sdk/examples/dynamo-movies/src/main.rs index bff652adb6..2a62df5d1b 100644 --- a/aws/sdk/examples/dynamo-movies/src/main.rs +++ b/aws/sdk/examples/dynamo-movies/src/main.rs @@ -35,8 +35,7 @@ async fn main() { let conf = dynamodb::Config::builder() .region(Region::new("us-east-1")) .build(); - let conn = aws_hyper::conn::Standard::https(); - let client = dynamodb::Client::from_conf_conn(conf, conn); + let client = dynamodb::Client::from_conf(conf); let raw_client = aws_hyper::Client::https(); let table_exists = client diff --git a/aws/sdk/integration-tests/kms/tests/integration.rs b/aws/sdk/integration-tests/kms/tests/integration.rs index 26a58d630d..434dae8af5 100644 --- a/aws/sdk/integration-tests/kms/tests/integration.rs +++ b/aws/sdk/integration-tests/kms/tests/integration.rs @@ -41,7 +41,7 @@ async fn generate_random_cn() { .region(Region::new("cn-north-1")) .credentials_provider(creds) .build(); - let client = kms::Client::from_conf_conn(conf, aws_hyper::conn::Standard::new(conn.clone())); + let client = kms::Client::from_conf_conn(conf, conn.clone()); let _ = client .generate_random() .number_of_bytes(64) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt index 1f18236843..3600612ddb 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt @@ -185,9 +185,12 @@ data class CargoDependency( companion object { val FastRand = CargoDependency("fastrand", CratesIo("1")) val Http: CargoDependency = CargoDependency("http", CratesIo("0.2")) + val Tower: CargoDependency = CargoDependency("tower", CratesIo("0.4"), optional = true) fun SmithyTypes(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("types") fun SmithyHttp(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http") + fun SmithyHttpTower(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http-tower") + fun SmithyClient(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("client", true) fun ProtocolTestHelpers(runtimeConfig: RuntimeConfig) = CargoDependency( "protocol-test-helpers", runtimeConfig.runtimeCrateLocation.crateLocation(), scope = DependencyScope.Dev diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt index ed4ebccd58..f5f055cf7e 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt @@ -52,8 +52,8 @@ data class RuntimeConfig( } } - fun runtimeCrate(runtimeCrateName: String): CargoDependency = - CargoDependency("$cratePrefix-$runtimeCrateName", runtimeCrateLocation.crateLocation()) + fun runtimeCrate(runtimeCrateName: String, optional: Boolean = false): CargoDependency = + CargoDependency("$cratePrefix-$runtimeCrateName", runtimeCrateLocation.crateLocation(), optional = optional) } data class RuntimeType(val name: String?, val dependency: RustDependency?, val namespace: String) { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustSettings.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustSettings.kt index ad1429bb6e..e2121ca7c9 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustSettings.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustSettings.kt @@ -28,12 +28,13 @@ private const val RUNTIME_CONFIG = "runtimeConfig" private const val CODEGEN_SETTINGS = "codegen" private const val LICENSE = "license" -data class CodegenConfig(val renameExceptions: Boolean = true) { +data class CodegenConfig(val renameExceptions: Boolean = true, val includeFluentClient: Boolean = true) { companion object { fun fromNode(node: Optional): CodegenConfig { return if (node.isPresent) { CodegenConfig( - node.get().getBooleanMemberOrDefault("renameErrors", true) + node.get().getBooleanMemberOrDefault("renameErrors", true), + node.get().getBooleanMemberOrDefault("includeFluentClient", true) ) } else { CodegenConfig() diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt index 98c2cdc837..fd6ea1e25c 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt @@ -12,6 +12,7 @@ import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.rust.codegen.smithy.RustCrate import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.smithy.generators.FluentClientDecorator import software.amazon.smithy.rust.codegen.smithy.generators.LibRsCustomization import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig import software.amazon.smithy.rust.codegen.smithy.generators.config.ConfigCustomization @@ -133,7 +134,7 @@ open class CombinedCodegenDecorator(decorators: List) : Ru .onEach { logger.info("Adding Codegen Decorator: ${it.javaClass.name}") }.toList() - return CombinedCodegenDecorator(decorators + RequiredCustomizations()) + return CombinedCodegenDecorator(decorators + RequiredCustomizations() + FluentClientDecorator()) } } } 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 new file mode 100644 index 0000000000..a26de275bb --- /dev/null +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/FluentClientDecorator.kt @@ -0,0 +1,367 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy.generators + +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.RustType +import software.amazon.smithy.rust.codegen.rustlang.RustWriter +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 +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.error.errorSymbol +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 + + private fun applies(protocolConfig: ProtocolConfig): Boolean = protocolConfig.symbolProvider.config().codegenConfig.includeFluentClient + + override fun extras(protocolConfig: ProtocolConfig, rustCrate: RustCrate) { + if (!applies(protocolConfig)) { + return + } + + val module = RustMetadata(additionalAttributes = listOf(Attribute.Cfg.feature("client")), public = true) + rustCrate.withModule(RustModule("client", module)) { writer -> + FluentClientGenerator(protocolConfig).render(writer) + } + val smithyClient = CargoDependency.SmithyClient(protocolConfig.runtimeConfig) + rustCrate.addFeature(Feature("client", true, listOf(smithyClient.name))) + rustCrate.addFeature(Feature("rustls", default = true, listOf("smithy-client/rustls"))) + rustCrate.addFeature(Feature("native-tls", default = false, listOf("smithy-client/native-tls"))) + } + + override fun libRsCustomizations( + protocolConfig: ProtocolConfig, + baseCustomizations: List + ): List { + if (!applies(protocolConfig)) { + return baseCustomizations + } + + 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, Builder};") + } + 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 clientDep = CargoDependency.SmithyClient(protocolConfig.runtimeConfig).copy(optional = true) + private val runtimeConfig = protocolConfig.runtimeConfig + private val moduleName = protocolConfig.moduleName + private val moduleUseName = moduleName.replace("-", "_") + private val humanName = serviceShape.id.name + + fun render(writer: RustWriter) { + writer.rustTemplate( + """ + ##[derive(Debug)] + pub(crate) struct Handle { + client: #{client}::Client, + 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> + } + + ##[doc(inline)] + pub use #{client}::Builder; + + impl From<#{client}::Client> for Client { + fn from(client: #{client}::Client) -> Self { + Self::with_config(client, crate::Config::builder().build()) + } + } + + impl Client { + pub fn with_config(client: #{client}::Client, conf: crate::Config) -> Self { + Self { + handle: std::sync::Arc::new(Handle { + client, + conf, + }) + } + } + + pub fn conf(&self) -> &crate::Config { + &self.handle.conf + } + } + """, + "client" to clientDep.asType() + ) + writer.rustBlockTemplate( + """ + impl Client + where + C: #{client}::bounds::SmithyConnector, + M: #{client}::bounds::SmithyMiddleware, + R: #{client}::retry::NewRequestPolicy, + """, + "client" to clientDep.asType(), + ) { + operations.forEach { operation -> + val name = symbolProvider.toSymbol(operation).name + rust( + """ + pub fn ${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() + + rust( + """ + ##[derive(std::fmt::Debug)] + pub struct $name { + handle: std::sync::Arc>, + inner: #T + }""", + input.builderSymbol(symbolProvider) + ) + + rustBlockTemplate( + """ + impl $name + where + C: #{client}::bounds::SmithyConnector, + M: #{client}::bounds::SmithyMiddleware, + R: #{client}::retry::NewRequestPolicy, + """, + "client" to CargoDependency.SmithyClient(runtimeConfig).asType(), + ) { + rustTemplate( + """ + pub(crate) fn new(handle: std::sync::Arc>) -> Self { + Self { handle, inner: Default::default() } + } + + pub async fn send(self) -> 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), + "sdk_err" to CargoDependency.SmithyHttp(runtimeConfig).asType().copy(name = "result::SdkError"), + "client" to CargoDependency.SmithyClient(runtimeConfig).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() + val coreType = outerType.stripOuter() + when (coreType) { + is RustType.Vec -> renderVecHelper(member, memberName, coreType) + is RustType.HashMap -> renderMapHelper(member, memberName, coreType) + else -> { + val signature = when (coreType) { + is RustType.String, + is RustType.Box -> "(mut self, inp: impl Into<${coreType.render(true)}>) -> Self" + else -> "(mut self, inp: ${coreType.render(true)}) -> Self" + } + documentShape(member, model) + rustBlock("pub fn $memberName$signature") { + write("self.inner = self.inner.$memberName(inp);") + write("self") + } + } + } + // pure setter + rustBlock("pub fn ${member.setterName()}(mut self, inp: ${outerType.render(true)}) -> Self") { + rust( + """ + self.inner = self.inner.${member.setterName()}(inp); + self + """ + ) + } + } + } + } + } + } + + private fun RustWriter.renderMapHelper(member: MemberShape, memberName: String, coreType: RustType.HashMap) { + documentShape(member, model) + val k = coreType.key + val v = coreType.member + + rustBlock("pub fn $memberName(mut self, k: impl Into<${k.render()}>, v: impl Into<${v.render()}>) -> Self") { + rust( + """ + self.inner = self.inner.$memberName(k, v); + self + """ + ) + } + } + + private fun RustWriter.renderVecHelper(member: MemberShape, memberName: String, coreType: RustType.Vec) { + documentShape(member, model) + rustBlock("pub fn $memberName(mut self, inp: impl Into<${coreType.member.render(true)}>) -> Self") { + rust( + """ + self.inner = self.inner.$memberName(inp); + self + """ + ) + } + } +} 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 ce4a3d91c2..091503ab55 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 @@ -72,6 +72,14 @@ abstract class HttpProtocolGenerator( val builderGenerator = BuilderGenerator(model, symbolProvider, operationShape.inputShape(model)) builderGenerator.render(inputWriter) + // TODO: One day, it should be possible for callers to invoke + // buildOperationType* directly to get the type rather than depending + // 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;") + // impl OperationInputShape { ... } inputWriter.implBlock(inputShape, symbolProvider) { buildOperation(this, operationShape, customizations, sdkId) @@ -130,6 +138,36 @@ abstract class HttpProtocolGenerator( abstract fun RustWriter.body(self: String, operationShape: OperationShape): BodyMetadata + private fun buildOperationType( + writer: RustWriter, + shape: OperationShape, + features: List, + ): String { + val runtimeConfig = protocolConfig.runtimeConfig + val operationT = RuntimeType.operation(runtimeConfig) + val output = buildOperationTypeOutput(writer, shape) + val retry = buildOperationTypeRetry(writer, features) + + return with(writer) { "${format(operationT)}<$output, $retry>" } + } + + private fun buildOperationTypeOutput( + writer: RustWriter, + shape: OperationShape, + ): String { + val outputSymbol = symbolProvider.toSymbol(shape) + return with(writer) { "${format(outputSymbol)}" } + } + + private fun buildOperationTypeRetry( + writer: RustWriter, + features: List, + ): String { + val retryType = features.mapNotNull { it.retryType() }.firstOrNull()?.let { writer.format(it) } ?: "()" + + return with(writer) { "$retryType" } + } + private fun buildOperation( implBlockWriter: RustWriter, shape: OperationShape, @@ -138,12 +176,10 @@ abstract class HttpProtocolGenerator( ) { val runtimeConfig = protocolConfig.runtimeConfig val outputSymbol = symbolProvider.toSymbol(shape) - val operationT = RuntimeType.operation(runtimeConfig) val operationModule = RuntimeType.operationModule(runtimeConfig) val sdkBody = RuntimeType.sdkBody(runtimeConfig) - val retryType = features.mapNotNull { it.retryType() }.firstOrNull()?.let { implBlockWriter.format(it) } ?: "()" - val baseReturnType = with(implBlockWriter) { "${format(operationT)}<${format(outputSymbol)}, $retryType>" } + val baseReturnType = buildOperationType(implBlockWriter, shape, features) val returnType = "Result<$baseReturnType, ${implBlockWriter.format(runtimeConfig.operationBuildError())}>" implBlockWriter.docs("Consumes the builder and constructs an Operation<#D>", outputSymbol) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/TopLevelErrorGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/TopLevelErrorGeneratorTest.kt index af63fc4555..f3aa04c1e9 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/TopLevelErrorGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/TopLevelErrorGeneratorTest.kt @@ -17,7 +17,7 @@ import kotlin.io.path.writeText internal class TopLevelErrorGeneratorTest { @ExperimentalPathApi @Test - fun `top level errors are sync + sync`() { + fun `top level errors are send + sync`() { val model = """ namespace com.example diff --git a/rust-runtime/smithy-client/Cargo.toml b/rust-runtime/smithy-client/Cargo.toml new file mode 100644 index 0000000000..6686169f3e --- /dev/null +++ b/rust-runtime/smithy-client/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "smithy-client" +version = "0.1.0" +authors = ["Russell Cohen "] +edition = "2018" +license = "Apache-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +test-util = ["protocol-test-helpers"] +default = ["test-util", "hyper", "rustls"] +native-tls = ["hyper", "hyper-tls"] +rustls = ["hyper", "hyper-rustls"] + +[dependencies] +hyper = { version = "0.14.2", features = ["client", "http2"], optional = true } +tower = { version = "0.4.6", features = ["util", "retry"] } +hyper-tls = { version ="0.5.0", optional = true } +hyper-rustls = { version = "0.22.1", optional = true, features = ["rustls-native-certs"] } +http = "0.2.3" +bytes = "1" +http-body = "0.4.0" +smithy-http = { path = "../smithy-http" } +smithy-types = { path = "../smithy-types" } +smithy-http-tower = { path = "../smithy-http-tower" } +fastrand = "1.4.0" +tokio = { version = "1", features = ["time"] } + +pin-project = "1" +tracing = "0.1.25" + +protocol-test-helpers = { path = "../protocol-test-helpers", optional = true } + +[dev-dependencies] +tokio = { version = "1", features = ["full", "test-util"] } +tower-test = "0.4.0" diff --git a/rust-runtime/smithy-client/LICENSE b/rust-runtime/smithy-client/LICENSE new file mode 100644 index 0000000000..67db858821 --- /dev/null +++ b/rust-runtime/smithy-client/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/rust-runtime/smithy-client/src/bounds.rs b/rust-runtime/smithy-client/src/bounds.rs new file mode 100644 index 0000000000..71026e090d --- /dev/null +++ b/rust-runtime/smithy-client/src/bounds.rs @@ -0,0 +1,144 @@ +//! This module holds convenient short-hands for the otherwise fairly extensive trait bounds +//! required for `call` and friends. +//! +//! The short-hands will one day be true [trait aliases], but for now they are traits with blanket +//! implementations. Also, due to [compiler limitations], the bounds repeat a nubmer of associated +//! types with bounds so that those bounds [do not need to be repeated] at the call site. It's a +//! bit of a mess to define, but _should_ be invisible to callers. +//! +//! [trait aliases]: https://rust-lang.github.io/rfcs/1733-trait-alias.html +//! [compiler limitations]: https://github.com/rust-lang/rust/issues/20671 +//! [do not need to be repeated]: https://github.com/rust-lang/rust/issues/20671#issuecomment-529752828 + +use crate::*; + +/// A service that has parsed a raw Smithy response. +pub type Parsed = smithy_http_tower::parse_response::ParseResponseService; + +/// A low-level Smithy connector that maps from [`http::Request`] to [`http::Response`]. +/// +/// This trait has a blanket implementation for all compatible types, and should never be +/// implemented. +pub trait SmithyConnector: + Service< + http::Request, + Response = http::Response, + Error = ::Error, + Future = ::Future, + > + Send + + Sync + + Clone + + 'static +{ + /// Forwarding type to `::Error` for bound inference. + /// + /// See module-level docs for details. + type Error: Into + Send + Sync + 'static; + + /// Forwarding type to `::Future` for bound inference. + /// + /// See module-level docs for details. + type Future: Send + 'static; +} + +impl SmithyConnector for T +where + T: Service, Response = http::Response> + + Send + + Sync + + Clone + + 'static, + T::Error: Into + Send + Sync + 'static, + T::Future: Send + 'static, +{ + type Error = T::Error; + type Future = T::Future; +} + +/// A Smithy middleware service that adjusts [`smithy_http::operation::Request`]s. +/// +/// This trait has a blanket implementation for all compatible types, and should never be +/// implemented. +pub trait SmithyMiddlewareService: + Service< + smithy_http::operation::Request, + Response = http::Response, + Error = smithy_http_tower::SendOperationError, + Future = ::Future, +> +{ + /// Forwarding type to `::Future` for bound inference. + /// + /// See module-level docs for details. + type Future: Send + 'static; +} + +impl SmithyMiddlewareService for T +where + T: Service< + smithy_http::operation::Request, + Response = http::Response, + Error = smithy_http_tower::SendOperationError, + >, + T::Future: Send + 'static, +{ + type Future = T::Future; +} + +/// A Smithy middleware layer (i.e., factory). +/// +/// This trait has a blanket implementation for all compatible types, and should never be +/// implemented. +pub trait SmithyMiddleware: + Layer< + smithy_http_tower::dispatch::DispatchService, + Service = >::Service, +> +{ + /// Forwarding type to `::Service` for bound inference. + /// + /// See module-level docs for details. + type Service: SmithyMiddlewareService + Send + Sync + Clone + 'static; +} + +impl SmithyMiddleware for T +where + T: Layer>, + T::Service: SmithyMiddlewareService + Send + Sync + Clone + 'static, +{ + type Service = T::Service; +} + +/// A Smithy retry policy. +/// +/// This trait has a blanket implementation for all compatible types, and should never be +/// implemented. +pub trait SmithyRetryPolicy: + tower::retry::Policy, SdkSuccess, SdkError> + Clone +{ + /// Forwarding type to `O` for bound inference. + /// + /// See module-level docs for details. + type O: ParseHttpResponse> + Send + Sync + Clone + 'static; + /// Forwarding type to `E` for bound inference. + /// + /// See module-level docs for details. + type E: Error + ProvideErrorKind; + + /// Forwarding type to `Retry` for bound inference. + /// + /// See module-level docs for details. + type Retry: ClassifyResponse, SdkError>; +} + +impl SmithyRetryPolicy for R +where + R: tower::retry::Policy, SdkSuccess, SdkError> + Clone, + O: ParseHttpResponse> + Send + Sync + Clone + 'static, + E: Error + ProvideErrorKind, + Retry: ClassifyResponse, SdkError>, +{ + type O = O; + type E = E; + type Retry = Retry; +} diff --git a/rust-runtime/smithy-client/src/builder.rs b/rust-runtime/smithy-client/src/builder.rs new file mode 100644 index 0000000000..0281f0c1e3 --- /dev/null +++ b/rust-runtime/smithy-client/src/builder.rs @@ -0,0 +1,229 @@ +use crate::{bounds, erase, retry, BoxError, Client}; +use smithy_http::body::SdkBody; + +/// A builder that provides more customization options when constructing a [`Client`]. +/// +/// To start, call [`Builder::new`]. Then, chain the method calls to configure the `Builder`. +/// When configured to your liking, call [`Builder::build`]. The individual methods have additional +/// documentation. +#[derive(Clone, Debug, Default)] +pub struct Builder { + connector: C, + middleware: M, + retry_policy: R, +} + +// It'd be nice to include R where R: Default here, but then the caller ends up always having to +// specify R explicitly since type parameter defaults (like the one for R) aren't picked up when R +// cannot be inferred. This is, arguably, a compiler bug/missing language feature, but is +// complicated: https://github.com/rust-lang/rust/issues/27336. +// +// For the time being, we stick with just for ::new. Those can usually be inferred since we +// only implement .constructor and .middleware when C and M are () respectively. Users who really +// need a builder for a custom R can use ::default instead. +impl Builder +where + C: Default, + M: Default, +{ + /// Construct a new builder. + /// + /// This will + /// + /// You will likely want to , as it does not specify a [connector](Builder::connector) + /// or [middleware](Builder::middleware). It uses the [standard retry + /// mechanism](retry::Standard). + pub fn new() -> Self { + Self::default() + } +} + +impl Builder<(), M, R> { + /// Specify the connector for the eventual client to use. + /// + /// The connector dictates how requests are turned into responses. Normally, this would entail + /// sending the request to some kind of remote server, but in certain settings it's useful to + /// be able to use a custom connector instead, such as to mock the network for tests. + /// + /// If you want to use a custom hyper connector, use [`Builder::hyper`]. + /// + /// If you just want to specify a function from request to response instead, use + /// [`Builder::map_connector`]. + pub fn connector(self, connector: C) -> Builder { + Builder { + connector, + retry_policy: self.retry_policy, + middleware: self.middleware, + } + } + + /// Use a function that directly maps each request to a response as a connector. + /// + /// ```rust + /// use smithy_client::Builder; + /// use smithy_http::body::SdkBody; + /// let client = Builder::new() + /// # /* + /// .middleware(..) + /// # */ + /// # .middleware(tower::layer::util::Identity::new()) + /// .connector_fn(|req: http::Request| { + /// async move { + /// Ok(http::Response::new(SdkBody::empty())) + /// } + /// }) + /// .build(); + /// # client.check(); + /// ``` + pub fn connector_fn(self, map: F) -> Builder, M, R> + where + F: Fn(http::Request) -> FF + Send, + FF: std::future::Future, BoxError>>, + // NOTE: The extra bound here is to help the type checker give better errors earlier. + tower::util::ServiceFn: bounds::SmithyConnector, + { + self.connector(tower::service_fn(map)) + } +} + +impl Builder { + /// Specify the middleware for the eventual client ot use. + /// + /// The middleware adjusts requests before they are dispatched to the connector. It is + /// responsible for filling in any request parameters that aren't specified by the Smithy + /// protocol definition, such as those used for routing (like the URL), authentication, and + /// authorization. + /// + /// The middleware takes the form of a [`tower::Layer`] that wraps the actual connection for + /// each request. The [`tower::Service`] that the middleware produces must accept requests of + /// the type [`smithy_http::operation::Request`] and return responses of the type + /// [`http::Response`], most likely by modifying the provided request in place, + /// passing it to the inner service, and then ultimately returning the inner service's + /// response. + /// + /// If your requests are already ready to be sent and need no adjustment, you can use + /// [`tower::layer::util::Identity`] as your middleware. + pub fn middleware(self, middleware: M) -> Builder { + Builder { + connector: self.connector, + retry_policy: self.retry_policy, + middleware, + } + } + + /// Use a function-like middleware that directly maps each request. + /// + /// ```rust + /// use smithy_client::Builder; + /// use smithy_http::body::SdkBody; + /// let client = Builder::new() + /// .https() + /// .middleware_fn(|req: smithy_http::operation::Request| { + /// req + /// }) + /// .build(); + /// # client.check(); + /// ``` + pub fn middleware_fn(self, map: F) -> Builder, R> + where + F: Fn(smithy_http::operation::Request) -> smithy_http::operation::Request + + Clone + + Send + + Sync + + 'static, + { + self.middleware(tower::util::MapRequestLayer::new(map)) + } +} + +impl Builder { + /// Specify the retry policy for the eventual client to use. + /// + /// By default, the Smithy client uses a standard retry policy that works well in most + /// settings. You can use this method to override that policy with a custom one. A new policy + /// instance will be instantiated for each request using [`retry::NewRequestPolicy`]. Each + /// policy instance must implement [`tower::retry::Policy`]. + /// + /// If you just want to modify the policy _configuration_ for the standard retry policy, use + /// [`Builder::set_retry_config`]. + pub fn retry_policy(self, retry_policy: R) -> Builder { + Builder { + connector: self.connector, + retry_policy, + middleware: self.middleware, + } + } +} + +impl Builder { + /// Set the standard retry policy's configuration. + pub fn set_retry_config(&mut self, config: retry::Config) { + self.retry_policy.with_config(config); + } +} + +impl Builder { + /// Use a connector that wraps the current connector. + pub fn map_connector(self, map: F) -> Builder + where + F: FnOnce(C) -> C2, + { + Builder { + connector: map(self.connector), + middleware: self.middleware, + retry_policy: self.retry_policy, + } + } + + /// Use a middleware that wraps the current middleware. + pub fn map_middleware(self, map: F) -> Builder + where + F: FnOnce(M) -> M2, + { + Builder { + connector: self.connector, + middleware: map(self.middleware), + retry_policy: self.retry_policy, + } + } + + /// Build a Smithy service [`Client`]. + pub fn build(self) -> Client { + Client { + connector: self.connector, + retry_policy: self.retry_policy, + middleware: self.middleware, + } + } +} + +impl Builder +where + C: bounds::SmithyConnector, + M: bounds::SmithyMiddleware + Send + Sync + 'static, + R: retry::NewRequestPolicy, +{ + /// Build a type-erased Smithy service [`Client`]. + /// + /// Note that if you're using the standard retry mechanism, [`retry::Standard`], `DynClient` + /// is equivalent to [`Client`] with no type arguments. + /// + /// ```rust + /// # #[cfg(feature = "https")] + /// # fn not_main() { + /// use smithy_client::{Builder, Client}; + /// struct MyClient { + /// client: smithy_client::Client, + /// } + /// + /// let client = Builder::new() + /// .https() + /// .middleware(tower::layer::util::Identity::new()) + /// .build_dyn(); + /// let client = MyClient { client }; + /// # client.client.check(); + /// # } + pub fn build_dyn(self) -> erase::DynClient { + self.build().into_dyn() + } +} diff --git a/rust-runtime/smithy-client/src/erase.rs b/rust-runtime/smithy-client/src/erase.rs new file mode 100644 index 0000000000..e3176cc564 --- /dev/null +++ b/rust-runtime/smithy-client/src/erase.rs @@ -0,0 +1,209 @@ +//! Type-erased variants of [`Client`] and friends. + +// These types are technically public in that they're reachable from the public trait impls on +// DynMiddleware, but no-one should ever look at them or use them. +#[doc(hidden)] +pub mod boxclone; +use boxclone::*; + +use crate::{bounds, retry, BoxError, Client}; +use smithy_http::body::SdkBody; +use std::fmt; +use tower::{Layer, Service, ServiceExt}; + +/// A [`Client`] whose connector and middleware types have been erased. +/// +/// Mainly useful if you need to name `R` in a type-erased client. If you do not, you can instead +/// just use `Client` with no type parameters, which ends up being the same type. +pub type DynClient = Client, R>; + +impl Client +where + C: bounds::SmithyConnector, + M: bounds::SmithyMiddleware + Send + Sync + 'static, + R: retry::NewRequestPolicy, +{ + /// Erase the middleware type from the client type signature. + /// + /// This makes the final client type easier to name, at the cost of a marginal increase in + /// runtime performance. See [`DynMiddleware`] for details. + /// + /// In practice, you'll use this method once you've constructed a client to your liking: + /// + /// ```rust + /// # #[cfg(feature = "https")] + /// # fn not_main() { + /// use smithy_client::{Builder, Client}; + /// struct MyClient { + /// client: Client, + /// } + /// + /// let client = Builder::new() + /// .https() + /// .middleware(tower::layer::util::Identity::new()) + /// .build(); + /// let client = MyClient { client: client.into_dyn_middleware() }; + /// # client.client.check(); + /// # } + pub fn into_dyn_middleware(self) -> Client, R> { + Client { + connector: self.connector, + middleware: DynMiddleware::new(self.middleware), + retry_policy: self.retry_policy, + } + } +} + +impl Client +where + C: bounds::SmithyConnector, + M: bounds::SmithyMiddleware + Send + Sync + 'static, + R: retry::NewRequestPolicy, +{ + /// Erase the connector type from the client type signature. + /// + /// This makes the final client type easier to name, at the cost of a marginal increase in + /// runtime performance. See [`DynConnector`] for details. + /// + /// In practice, you'll use this method once you've constructed a client to your liking: + /// + /// ```rust + /// # #[cfg(feature = "https")] + /// # fn not_main() { + /// # type MyMiddleware = smithy_client::DynMiddleware; + /// use smithy_client::{Builder, Client}; + /// struct MyClient { + /// client: Client, + /// } + /// + /// let client = Builder::new() + /// .https() + /// .middleware(tower::layer::util::Identity::new()) + /// .build(); + /// let client = MyClient { client: client.into_dyn_connector() }; + /// # client.client.check(); + /// # } + pub fn into_dyn_connector(self) -> Client { + Client { + connector: DynConnector::new(self.connector), + middleware: self.middleware, + retry_policy: self.retry_policy, + } + } + + /// Erase the connector and middleware types from the client type signature. + /// + /// This makes the final client type easier to name, at the cost of a marginal increase in + /// runtime performance. See [`DynConnector`] and [`DynMiddleware`] for details. + /// + /// Note that if you're using the standard retry mechanism, [`retry::Standard`], `DynClient` + /// is equivalent to `Client` with no type arguments. + /// + /// In practice, you'll use this method once you've constructed a client to your liking: + /// + /// ```rust + /// # #[cfg(feature = "https")] + /// # fn not_main() { + /// use smithy_client::{Builder, Client}; + /// struct MyClient { + /// client: smithy_client::Client, + /// } + /// + /// let client = Builder::new() + /// .https() + /// .middleware(tower::layer::util::Identity::new()) + /// .build(); + /// let client = MyClient { client: client.into_dyn() }; + /// # client.client.check(); + /// # } + pub fn into_dyn(self) -> DynClient { + self.into_dyn_connector().into_dyn_middleware() + } +} + +/// A Smithy connector that uses dynamic dispatch. +/// +/// This type allows you to pay a small runtime cost to avoid having to name the exact connector +/// you're using anywhere you want to hold a [`Client`]. Specifically, this will use `Box` to +/// enable dynamic dispatch for every request that goes through the connector, which increases +/// memory pressure and suffers an additional vtable indirection for each request, but is unlikely +/// to matter in all but the highest-performance settings. +#[non_exhaustive] +#[derive(Clone)] +pub struct DynConnector(BoxCloneService, http::Response, BoxError>); + +impl fmt::Debug for DynConnector { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("DynConnector").finish() + } +} + +impl DynConnector { + /// Construct a new dynamically-dispatched Smithy middleware. + pub fn new(connector: C) -> Self + where + C: bounds::SmithyConnector + Send + 'static, + E: Into, + { + Self(BoxCloneService::new(connector.map_err(|e| e.into()))) + } +} + +impl Service> for DynConnector { + type Response = http::Response; + type Error = BoxError; + type Future = BoxFuture; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.0.poll_ready(cx) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + self.0.call(req) + } +} + +/// A Smithy middleware that uses dynamic dispatch. +/// +/// This type allows you to pay a small runtime cost to avoid having to name the exact middleware +/// you're using anywhere you want to hold a [`Client`]. Specifically, this will use `Box` to +/// enable dynamic dispatch for every request that goes through the middleware, which increases +/// memory pressure and suffers an additional vtable indirection for each request, but is unlikely +/// to matter in all but the highest-performance settings. +#[non_exhaustive] +pub struct DynMiddleware( + BoxCloneLayer< + smithy_http_tower::dispatch::DispatchService, + smithy_http::operation::Request, + http::Response, + smithy_http_tower::SendOperationError, + >, +); + +impl fmt::Debug for DynMiddleware { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("DynMiddleware").finish() + } +} + +impl DynMiddleware { + /// Construct a new dynamically-dispatched Smithy middleware. + pub fn new + Send + Sync + 'static>(middleware: M) -> Self { + Self(BoxCloneLayer::new(middleware)) + } +} + +impl Layer> for DynMiddleware { + type Service = BoxCloneService< + smithy_http::operation::Request, + http::Response, + smithy_http_tower::SendOperationError, + >; + + fn layer(&self, inner: smithy_http_tower::dispatch::DispatchService) -> Self::Service { + self.0.layer(inner) + } +} diff --git a/rust-runtime/smithy-client/src/erase/boxclone.rs b/rust-runtime/smithy-client/src/erase/boxclone.rs new file mode 100644 index 0000000000..90079ae727 --- /dev/null +++ b/rust-runtime/smithy-client/src/erase/boxclone.rs @@ -0,0 +1,158 @@ +// This is an adaptation of tower::util::{BoxLayer, BoxService} that includes Clone and doesn't +// include Sync. + +use std::fmt; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tower::layer::{layer_fn, Layer}; +use tower::Service; + +pub(super) struct BoxCloneLayer { + boxed: Box> + Send + Sync>, +} + +impl BoxCloneLayer { + /// Create a new [`BoxLayer`]. + pub fn new(inner_layer: L) -> Self + where + L: Layer + Send + Sync + 'static, + L::Service: Service + Clone + Send + Sync + 'static, + >::Future: Send + 'static, + { + let layer = layer_fn(move |inner: In| { + let out = inner_layer.layer(inner); + BoxCloneService::new(out) + }); + + Self { + boxed: Box::new(layer), + } + } +} + +impl Layer for BoxCloneLayer { + type Service = BoxCloneService; + + fn layer(&self, inner: In) -> Self::Service { + self.boxed.layer(inner) + } +} + +impl fmt::Debug for BoxCloneLayer { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("BoxCloneLayer").finish() + } +} + +trait CloneService: Service { + fn clone_box( + &self, + ) -> Box< + dyn CloneService + + Send + + Sync + + 'static, + >; +} + +impl CloneService for T +where + T: Service + Clone + Send + Sync + 'static, +{ + fn clone_box( + &self, + ) -> Box< + dyn CloneService< + Request, + Response = Self::Response, + Error = Self::Error, + Future = Self::Future, + > + + 'static + + Send + + Sync, + > { + Box::new(self.clone()) + } +} + +pub type BoxFuture = Pin> + Send>>; +pub struct BoxCloneService { + inner: Box< + dyn CloneService> + + Send + + Sync + + 'static, + >, +} + +#[derive(Debug, Clone)] +struct Boxed { + inner: S, +} + +impl BoxCloneService { + #[allow(missing_docs)] + pub fn new(inner: S) -> Self + where + S: Service + Send + Sync + 'static + Clone, + S::Future: Send + 'static, + { + let inner = Box::new(Boxed { inner }); + BoxCloneService { inner } + } +} + +impl Clone for BoxCloneService +where + T: 'static, + U: 'static, + E: 'static, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone_box(), + } + } +} + +impl Service for BoxCloneService { + type Response = U; + type Error = E; + type Future = BoxFuture; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, request: T) -> BoxFuture { + self.inner.call(request) + } +} + +impl fmt::Debug for BoxCloneService { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("BoxCloneService").finish() + } +} + +impl Service for Boxed +where + S: Service + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + + #[allow(clippy::type_complexity)] + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, request: Request) -> Self::Future { + Box::pin(self.inner.call(request)) + } +} diff --git a/rust-runtime/smithy-client/src/hyper_impls.rs b/rust-runtime/smithy-client/src/hyper_impls.rs new file mode 100644 index 0000000000..041a59bf21 --- /dev/null +++ b/rust-runtime/smithy-client/src/hyper_impls.rs @@ -0,0 +1,112 @@ +use crate::Builder; +use smithy_http::body::SdkBody; +pub use smithy_http::result::{SdkError, SdkSuccess}; +use tower::Service; + +/// Adapter from a [`hyper::Client`] to a connector useable by a [`Client`](crate::Client). +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct HyperAdapter(hyper::Client); + +impl Service> for HyperAdapter +where + C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, +{ + type Response = http::Response; + type Error = hyper::Error; + + #[allow(clippy::type_complexity)] + type Future = std::pin::Pin< + Box> + Send + 'static>, + >; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.0.poll_ready(cx) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + let fut = self.0.call(req); + Box::pin(async move { Ok(fut.await?.map(SdkBody::from)) }) + } +} + +impl From> for HyperAdapter { + fn from(hc: hyper::Client) -> Self { + Self(hc) + } +} + +impl Builder<(), M, R> { + /// Connect to the service using the provided `hyper` client. + pub fn hyper(self, connector: hyper::Client) -> Builder, M, R> + where + HC: hyper::client::connect::Connect + Clone + Send + Sync + 'static, + { + self.connector(HyperAdapter::from(connector)) + } +} + +#[cfg(any(feature = "rustls", feature = "native_tls"))] +impl crate::Client +where + M: Default, + M: crate::bounds::SmithyMiddleware + Send + Sync + 'static, +{ + /// Create a Smithy client that uses HTTPS and the [standard retry + /// policy](crate::retry::Standard) over the default middleware implementation. + /// + /// For convenience, this constructor type-erases the concrete TLS connector backend used using + /// dynamic dispatch. This comes at a slight runtime performance cost. See + /// [`DynConnector`](crate::erase::DynConnector) for details. To avoid that overhead, use + /// [`Builder::rustls`] or `Builder::native_tls` instead. + pub fn https() -> Self { + #[cfg(feature = "rustls")] + let with_https = |b: Builder<_>| b.rustls(); + // If we are compiling this function & rustls is not enabled, then native-tls MUST be enabled + #[cfg(not(feature = "rustls"))] + let with_https = |b: Builder<_>| b.native_tls(); + + with_https(Builder::new()) + .middleware(M::default()) + .build() + .into_dyn_connector() + } +} + +#[cfg(feature = "rustls")] +impl Builder<(), M, R> { + /// Connect to the service over HTTPS using Rustls. + pub fn rustls( + self, + ) -> Builder>, M, R> + { + let https = hyper_rustls::HttpsConnector::with_native_roots(); + let client = hyper::Client::builder().build::<_, SdkBody>(https); + self.connector(HyperAdapter::from(client)) + } + + /// Connect to the service over HTTPS using Rustls. + /// + /// This is exactly equivalent to [`Builder::rustls`]. If you instead wish to use `native_tls`, + /// use `Builder::native_tls`. + pub fn https( + self, + ) -> Builder>, M, R> + { + self.rustls() + } +} +#[cfg(feature = "native-tls")] +impl Builder<(), M, R> { + /// Connect to the service over HTTPS using the native TLS library on your platform. + pub fn native_tls( + self, + ) -> Builder>, M, R> { + let https = hyper_tls::HttpsConnector::new(); + let client = hyper::Client::builder().build::<_, SdkBody>(https); + self.connector(HyperAdapter::from(client)) + } +} diff --git a/rust-runtime/smithy-client/src/lib.rs b/rust-runtime/smithy-client/src/lib.rs new file mode 100644 index 0000000000..4a1fdfde4f --- /dev/null +++ b/rust-runtime/smithy-client/src/lib.rs @@ -0,0 +1,193 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +//! A Hyper-based Smithy service client. +#![warn( + missing_debug_implementations, + missing_docs, + rustdoc::all, + rust_2018_idioms +)] + +pub mod bounds; +pub mod erase; +pub mod retry; + +// https://github.com/rust-lang/rust/issues/72081 +#[allow(rustdoc::private_doc_tests)] +mod builder; +pub use builder::Builder; + +#[cfg(feature = "test-util")] +pub mod test_connection; + +#[cfg(feature = "hyper")] +mod hyper_impls; + +// The types in this module are only used to write the bounds in [`Client::check`]. Customers will +// not need them. But the module and its types must be public so that we can call `check` from +// doc-tests. +#[doc(hidden)] +pub mod static_tests; + +/// Type aliases for standard connection types. +#[cfg(feature = "hyper")] +#[allow(missing_docs)] +pub mod conns { + #[cfg(feature = "rustls")] + pub type Https = crate::hyper_impls::HyperAdapter< + hyper_rustls::HttpsConnector, + >; + + #[cfg(feature = "native-tls")] + pub type NativeTls = + crate::hyper_impls::HyperAdapter>; + + #[cfg(feature = "rustls")] + pub type Rustls = crate::hyper_impls::HyperAdapter< + hyper_rustls::HttpsConnector, + >; +} + +use smithy_http::body::SdkBody; +use smithy_http::operation::Operation; +use smithy_http::response::ParseHttpResponse; +pub use smithy_http::result::{SdkError, SdkSuccess}; +use smithy_http::retry::ClassifyResponse; +use smithy_http_tower::dispatch::DispatchLayer; +use smithy_http_tower::parse_response::ParseResponseLayer; +use smithy_types::retry::ProvideErrorKind; +use std::error::Error; +use tower::{Layer, Service, ServiceBuilder, ServiceExt}; + +type BoxError = Box; + +/// Smithy service client. +/// +/// The service client is customizeable in a number of ways (see [`Builder`]), but most customers +/// can stick with the standard constructor provided by [`Client::new`]. It takes only a single +/// argument, which is the middleware that fills out the [`http::Request`] for each higher-level +/// operation so that it can ultimately be sent to the remote host. The middleware is responsible +/// for filling in any request parameters that aren't specified by the Smithy protocol definition, +/// such as those used for routing (like the URL), authentication, and authorization. +/// +/// The middleware takes the form of a [`tower::Layer`] that wraps the actual connection for each +/// request. The [`tower::Service`] that the middleware produces must accept requests of the type +/// [`smithy_http::operation::Request`] and return responses of the type +/// [`http::Response`], most likely by modifying the provided request in place, passing it +/// to the inner service, and then ultimately returning the inner service's response. +/// +/// With the `hyper` feature enabled, you can construct a `Client` directly from a +/// [`hyper::Client`] using [`Builder::hyper`]. You can also enable the `rustls` or `native-tls` +/// features to construct a Client against a standard HTTPS endpoint using [`Builder::rustls`] and +/// `Builder::native_tls` respectively. +#[derive(Debug)] +pub struct Client< + Connector = erase::DynConnector, + Middleware = erase::DynMiddleware, + RetryPolicy = retry::Standard, +> { + connector: Connector, + middleware: Middleware, + retry_policy: RetryPolicy, +} + +// Quick-create for people who just want "the default". +impl Client +where + M: Default, +{ + /// Create a Smithy client that the given connector, a middleware default, and the [standard + /// retry policy](crate::retry::Standard). + pub fn new(connector: C) -> Self { + Builder::new() + .connector(connector) + .middleware(M::default()) + .build() + } +} + +impl Client { + /// Set the standard retry policy's configuration. + pub fn set_retry_config(&mut self, config: retry::Config) { + self.retry_policy.with_config(config); + } + + /// Adjust a standard retry client with the given policy configuration. + pub fn with_retry_config(mut self, config: retry::Config) -> Self { + self.set_retry_config(config); + self + } +} + +impl Client +where + C: bounds::SmithyConnector, + M: bounds::SmithyMiddleware, + R: retry::NewRequestPolicy, +{ + /// Dispatch this request to the network + /// + /// For ergonomics, this does not include the raw response for successful responses. To + /// access the raw response use `call_raw`. + pub async fn call(&self, input: Operation) -> Result> + where + R::Policy: bounds::SmithyRetryPolicy, + bounds::Parsed<>::Service, O, Retry>: + Service, Response = SdkSuccess, Error = SdkError> + Clone, + { + self.call_raw(input).await.map(|res| res.parsed) + } + + /// Dispatch this request to the network + /// + /// The returned result contains the raw HTTP response which can be useful for debugging or + /// implementing unsupported features. + pub async fn call_raw( + &self, + input: Operation, + ) -> Result, SdkError> + where + R::Policy: bounds::SmithyRetryPolicy, + // This bound is not _technically_ inferred by all the previous bounds, but in practice it + // is because _we_ know that there is only implementation of Service for Parsed + // (ParsedResponseService), and it will apply as long as the bounds on C, M, and R hold, + // and will produce (as expected) Response = SdkSuccess, Error = SdkError. But Rust + // doesn't know that -- there _could_ theoretically be other implementations of Service for + // Parsed that don't return those same types. So, we must give the bound. + bounds::Parsed<>::Service, O, Retry>: + Service, Response = SdkSuccess, Error = SdkError> + Clone, + { + let connector = self.connector.clone(); + let mut svc = ServiceBuilder::new() + // Create a new request-scoped policy + .retry(self.retry_policy.new_request_policy()) + .layer(ParseResponseLayer::::new()) + // These layers can be considered as occuring in order. That is, first invoke the + // customer-provided middleware, then dispatch dispatch over the wire. + .layer(&self.middleware) + .layer(DispatchLayer::new()) + .service(connector); + svc.ready().await?.call(input).await + } + + /// Statically check the validity of a `Client` without a request to send. + /// + /// This will make sure that all the bounds hold that would be required by `call` and + /// `call_raw` (modulo those that relate to the specific `Operation` type). Comes in handy to + /// ensure (statically) that all the various constructors actually produce "useful" types. + #[doc(hidden)] + pub fn check(&self) + where + R::Policy: tower::retry::Policy< + static_tests::ValidTestOperation, + SdkSuccess<()>, + SdkError, + > + Clone, + { + let _ = |o: static_tests::ValidTestOperation| { + let _ = self.call_raw(o); + }; + } +} diff --git a/aws/rust-runtime/aws-hyper/src/retry.rs b/rust-runtime/smithy-client/src/retry.rs similarity index 81% rename from aws/rust-runtime/aws-hyper/src/retry.rs rename to rust-runtime/smithy-client/src/retry.rs index 08d003dff6..38cc1d831e 100644 --- a/aws/rust-runtime/aws-hyper/src/retry.rs +++ b/rust-runtime/smithy-client/src/retry.rs @@ -10,12 +10,11 @@ //! implementation is intended to be _correct_ but not especially long lasting. //! //! Components: -//! - [`RetryHandlerFactory`](crate::retry::RetryHandlerFactory): Top level manager, intended -//! to be associated with a [`Client`](crate::Client). Its sole purpose in life is to create a RetryHandler -//! for individual requests. -//! - [`RetryHandler`](crate::retry::RetryHandler): A request-scoped retry policy, -//! backed by request-local state and shared state contained within [`RetryHandlerFactory`](crate::retry::RetryHandlerFactory) -//! - [`RetryConfig`](crate::retry::RetryConfig): Static configuration (max retries, max backoff etc.) +//! - [`Standard`]: Top level manager, intended to be associated with a [`Client`](crate::Client). +//! Its sole purpose in life is to create a [`RetryHandler`] for individual requests. +//! - [`RetryHandler`]: A request-scoped retry policy, backed by request-local state and shared +//! state contained within [`Standard`]. +//! - [`Config`]: Static configuration (max retries, max backoff etc.) use crate::{SdkError, SdkSuccess}; use smithy_http::operation; @@ -28,13 +27,26 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use tracing::Instrument; +/// A policy instantiator. +/// +/// Implementors are essentially "policy factories" that can produce a new instance of a retry +/// policy mechanism for each request, which allows both shared global state _and_ per-request +/// local state. +pub trait NewRequestPolicy { + /// The type of the per-request policy mechanism. + type Policy; + + /// Create a new policy mechanism instance. + fn new_request_policy(&self) -> Self::Policy; +} + /// Retry Policy Configuration /// -/// Without specific use cases, users should generally rely on the default values set by `[RetryConfig::default]`(RetryConfig::default).` +/// Without specific use cases, users should generally rely on the default values set by `[Config::default]`(Config::default).` /// /// Currently these fields are private and no setters provided. As needed, this configuration will become user-modifiable in the future.. #[derive(Clone, Debug)] -pub struct RetryConfig { +pub struct Config { initial_retry_tokens: usize, retry_cost: usize, no_retry_increment: usize, @@ -44,14 +56,14 @@ pub struct RetryConfig { base: fn() -> f64, } -impl RetryConfig { +impl Config { /// Override `b` in the exponential backoff computation /// /// By default, `base` is a randomly generated value between 0 and 1. In tests, it can /// be helpful to override this: /// ```rust - /// use aws_hyper::RetryConfig; - /// let conf = RetryConfig::default().with_base(||1_f64); + /// use smithy_client::retry::Config; + /// let conf = Config::default().with_base(||1_f64); /// ``` pub fn with_base(mut self, base: fn() -> f64) -> Self { self.base = base; @@ -59,7 +71,7 @@ impl RetryConfig { } } -impl Default for RetryConfig { +impl Default for Config { fn default() -> Self { Self { initial_retry_tokens: INITIAL_RETRY_TOKENS, @@ -81,31 +93,38 @@ const RETRY_COST: usize = 5; /// Manage retries for a service /// /// An implementation of the `standard` AWS retry strategy as specified in the SEP. A `Strategy` is scoped to a client. -/// For an individual request, call [`RetryHandlerFactory::new_handler()`](RetryHandlerFactory::new_handler) +/// For an individual request, call [`Standard::new_request_policy()`](Standard::new_request_policy) /// /// In the future, adding support for the adaptive retry strategy will be added by adding a `TokenBucket` to /// `CrossRequestRetryState` -/// Its main functionality is via `new_handler` which creates a `RetryHandler` to manage the retry for +/// Its main functionality is via `new_request_policy` which creates a `RetryHandler` to manage the retry for /// an individual request. #[derive(Debug)] -pub struct RetryHandlerFactory { - config: RetryConfig, +pub struct Standard { + config: Config, shared_state: CrossRequestRetryState, } -impl RetryHandlerFactory { - pub fn new(config: RetryConfig) -> Self { +impl Standard { + /// Construct a new standard retry policy from the given policy configuration. + pub fn new(config: Config) -> Self { Self { shared_state: CrossRequestRetryState::new(config.initial_retry_tokens), config, } } - pub fn with_config(&mut self, config: RetryConfig) { + /// Set the configuration for this retry policy. + pub fn with_config(&mut self, config: Config) -> &mut Self { self.config = config; + self } +} + +impl NewRequestPolicy for Standard { + type Policy = RetryHandler; - pub(crate) fn new_handler(&self) -> RetryHandler { + fn new_request_policy(&self) -> Self::Policy { RetryHandler { local: RequestLocalRetryState::new(), shared: self.shared_state.clone(), @@ -114,7 +133,13 @@ impl RetryHandlerFactory { } } -#[derive(Default, Clone)] +impl Default for Standard { + fn default() -> Self { + Self::new(Config::default()) + } +} + +#[derive(Default, Clone, Debug)] struct RequestLocalRetryState { attempts: u32, last_quota_usage: Option, @@ -148,7 +173,7 @@ impl CrossRequestRetryState { } } - fn quota_release(&self, value: Option, config: &RetryConfig) { + fn quota_release(&self, value: Option, config: &Config) { let mut quota = self.quota_available.lock().unwrap(); *quota += value.unwrap_or(config.no_retry_increment); } @@ -157,7 +182,7 @@ impl CrossRequestRetryState { /// /// If quota is available, the amount of quota consumed is returned /// If no quota is available, `None` is returned. - fn quota_acquire(&self, err: &ErrorKind, config: &RetryConfig) -> Option { + fn quota_acquire(&self, err: &ErrorKind, config: &Config) -> Option { let mut quota = self.quota_available.lock().unwrap(); let retry_cost = if err == &ErrorKind::TransientError { config.timeout_retry_cost @@ -178,11 +203,11 @@ impl CrossRequestRetryState { /// Implement retries for an individual request. /// It is intended to be used as a [Tower Retry Policy](tower::retry::Policy) for use in tower-based /// middleware stacks. -#[derive(Clone)] -pub(crate) struct RetryHandler { +#[derive(Clone, Debug)] +pub struct RetryHandler { local: RequestLocalRetryState, shared: CrossRequestRetryState, - config: RetryConfig, + config: Config, } #[cfg(test)] @@ -197,7 +222,7 @@ impl RetryHandler { /// /// If a retry is specified, this function returns `(next, backoff_duration)` /// If no retry is specified, this function returns None - pub fn attempt_retry(&self, retry_kind: Result<(), ErrorKind>) -> Option<(Self, Duration)> { + fn attempt_retry(&self, retry_kind: Result<(), ErrorKind>) -> Option<(Self, Duration)> { let quota_used = match retry_kind { Ok(_) => { self.shared @@ -277,14 +302,14 @@ fn check_send_sync(t: T) -> T { #[cfg(test)] mod test { - use crate::retry::{RetryConfig, RetryHandler, RetryHandlerFactory}; + use crate::retry::{Config, NewRequestPolicy, RetryHandler, Standard}; use smithy_types::retry::ErrorKind; use std::time::Duration; fn assert_send_sync() {} - fn test_config() -> RetryConfig { - RetryConfig::default().with_base(|| 1_f64) + fn test_config() -> Config { + Config::default().with_base(|| 1_f64) } #[test] @@ -294,7 +319,7 @@ mod test { #[test] fn eventual_success() { - let policy = RetryHandlerFactory::new(test_config()).new_handler(); + let policy = Standard::new(test_config()).new_request_policy(); let (policy, dur) = policy .attempt_retry(Err(ErrorKind::ServerError)) .expect("should retry"); @@ -314,7 +339,7 @@ mod test { #[test] fn no_more_attempts() { - let policy = RetryHandlerFactory::new(test_config()).new_handler(); + let policy = Standard::new(test_config()).new_request_policy(); let (policy, dur) = policy .attempt_retry(Err(ErrorKind::ServerError)) .expect("should retry"); @@ -336,7 +361,7 @@ mod test { fn no_quota() { let mut conf = test_config(); conf.initial_retry_tokens = 5; - let policy = RetryHandlerFactory::new(conf).new_handler(); + let policy = Standard::new(conf).new_request_policy(); let (policy, dur) = policy .attempt_retry(Err(ErrorKind::ServerError)) .expect("should retry"); @@ -351,7 +376,7 @@ mod test { fn backoff_timing() { let mut conf = test_config(); conf.max_retries = 5; - let policy = RetryHandlerFactory::new(conf).new_handler(); + let policy = Standard::new(conf).new_request_policy(); let (policy, dur) = policy .attempt_retry(Err(ErrorKind::ServerError)) .expect("should retry"); @@ -386,7 +411,7 @@ mod test { let mut conf = test_config(); conf.max_retries = 5; conf.max_backoff = Duration::from_secs(3); - let policy = RetryHandlerFactory::new(conf).new_handler(); + let policy = Standard::new(conf).new_request_policy(); let (policy, dur) = policy .attempt_retry(Err(ErrorKind::ServerError)) .expect("should retry"); diff --git a/rust-runtime/smithy-client/src/static_tests.rs b/rust-runtime/smithy-client/src/static_tests.rs new file mode 100644 index 0000000000..ebad8c062c --- /dev/null +++ b/rust-runtime/smithy-client/src/static_tests.rs @@ -0,0 +1,113 @@ +//! This module provides types useful for static tests. +#![allow(missing_docs, missing_debug_implementations)] + +use crate::*; + +#[derive(Debug)] +#[non_exhaustive] +pub struct TestOperationError; +impl std::fmt::Display for TestOperationError { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + unreachable!("only used for static tests") + } +} +impl Error for TestOperationError {} +impl ProvideErrorKind for TestOperationError { + fn retryable_error_kind(&self) -> Option { + unreachable!("only used for static tests") + } + + fn code(&self) -> Option<&str> { + unreachable!("only used for static tests") + } +} +#[derive(Clone)] +#[non_exhaustive] +pub struct TestOperation; +impl ParseHttpResponse for TestOperation { + type Output = Result<(), TestOperationError>; + + fn parse_unloaded(&self, _: &mut http::Response) -> Option { + unreachable!("only used for static tests") + } + + fn parse_loaded(&self, _response: &http::Response) -> Self::Output { + unreachable!("only used for static tests") + } +} +pub type ValidTestOperation = Operation; + +// Statically check that a standard retry can actually be used to build a Client. +#[allow(dead_code)] +#[cfg(test)] +fn sanity_retry() { + Builder::new() + .middleware(tower::layer::util::Identity::new()) + .connector_fn(|_| async { unreachable!() }) + .build() + .check(); +} + +// Statically check that a hyper client can actually be used to build a Client. +#[allow(dead_code)] +#[cfg(all(test, feature = "hyper"))] +fn sanity_hyper(hc: hyper::Client) +where + C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, +{ + Builder::new() + .middleware(tower::layer::util::Identity::new()) + .hyper(hc) + .build() + .check(); +} + +// Statically check that a type-erased middleware client is actually a valid Client. +#[allow(dead_code)] +fn sanity_erase_middleware() { + Builder::new() + .middleware(tower::layer::util::Identity::new()) + .connector_fn(|_| async { unreachable!() }) + .build() + .into_dyn_middleware() + .check(); +} + +// Statically check that a type-erased connector client is actually a valid Client. +#[allow(dead_code)] +fn sanity_erase_connector() { + Builder::new() + .middleware(tower::layer::util::Identity::new()) + .connector_fn(|_| async { unreachable!() }) + .build() + .into_dyn_connector() + .check(); +} + +// Statically check that a fully type-erased client is actually a valid Client. +#[allow(dead_code)] +fn sanity_erase_full() { + Builder::new() + .middleware(tower::layer::util::Identity::new()) + .connector_fn(|_| async { unreachable!() }) + .build() + .into_dyn() + .check(); +} + +fn is_send_sync(_: T) {} +fn noarg_is_send_sync() {} + +// Statically check that a fully type-erased client is still Send + Sync. +#[allow(dead_code)] +fn erased_is_send_sync() { + noarg_is_send_sync::(); + noarg_is_send_sync::>(); + is_send_sync( + Builder::new() + .middleware(tower::layer::util::Identity::new()) + .connector_fn(|_| async { unreachable!() }) + .build() + .into_dyn(), + ); +} diff --git a/aws/rust-runtime/aws-hyper/src/test_connection.rs b/rust-runtime/smithy-client/src/test_connection.rs similarity index 82% rename from aws/rust-runtime/aws-hyper/src/test_connection.rs rename to rust-runtime/smithy-client/src/test_connection.rs index a2749fb647..7623d64b2d 100644 --- a/aws/rust-runtime/aws-hyper/src/test_connection.rs +++ b/rust-runtime/smithy-client/src/test_connection.rs @@ -2,6 +2,10 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ +//! Module with client connectors usefule for testing. + +// TODO +#![allow(missing_docs)] use http::header::{HeaderName, CONTENT_TYPE}; use http::Request; @@ -15,6 +19,7 @@ use tower::BoxError; type ConnectVec = Vec<(http::Request, http::Response)>; +#[derive(Debug)] pub struct ValidateRequest { pub expected: http::Request, pub actual: http::Request, @@ -52,7 +57,7 @@ impl ValidateRequest { } } -/// TestConnection for use with a [`aws_hyper::Client`](crate::Client) +/// TestConnection for use with a [`Client`](crate::Client). /// /// A basic test connection. It will: /// - Response to requests with a preloaded series of responses @@ -62,7 +67,7 @@ impl ValidateRequest { /// For more complex use cases, see [Tower Test](https://docs.rs/tower-test/0.4.0/tower_test/) /// Usage example: /// ```rust -/// use aws_hyper::test_connection::TestConnection; +/// use smithy_client::test_connection::TestConnection; /// use smithy_http::body::SdkBody; /// let events = vec![( /// http::Request::new(SdkBody::from("request body")), @@ -72,8 +77,9 @@ impl ValidateRequest { /// .unwrap(), /// )]; /// let conn = TestConnection::new(events); -/// let client = aws_hyper::Client::new(conn); +/// let client = smithy_client::Client::from(conn); /// ``` +#[derive(Debug)] pub struct TestConnection { data: Arc>>, requests: Arc>>, @@ -103,7 +109,10 @@ impl TestConnection { } } -impl> tower::Service> for TestConnection { +impl tower::Service> for TestConnection +where + SdkBody: From, +{ type Response = http::Response; type Error = BoxError; type Future = Ready>; @@ -119,24 +128,37 @@ impl> tower::Service> for TestConnec .lock() .unwrap() .push(ValidateRequest { expected, actual }); - std::future::ready(Ok(resp.map(|body| SdkBody::from(body.into())))) + std::future::ready(Ok(resp.map(SdkBody::from))) } else { std::future::ready(Err("No more data".into())) } } } +impl From> for crate::Client, tower::layer::util::Identity> +where + B: Send + 'static, + SdkBody: From, +{ + fn from(tc: TestConnection) -> Self { + crate::Builder::new() + .middleware(tower::layer::util::Identity::new()) + .connector(tc) + .build() + } +} + #[cfg(test)] mod tests { use crate::test_connection::TestConnection; - use crate::{conn, Client}; + use crate::Client; fn is_send_sync(_: T) {} #[test] fn construct_test_client() { let test_conn = TestConnection::::new(vec![]); - let client = Client::new(conn::Standard::new(test_conn)); + let client: Client<_, _, _> = test_conn.into(); is_send_sync(client); } } diff --git a/rust-runtime/smithy-http-tower/src/map_request.rs b/rust-runtime/smithy-http-tower/src/map_request.rs index 5f741e3f23..ba63257a0e 100644 --- a/rust-runtime/smithy-http-tower/src/map_request.rs +++ b/rust-runtime/smithy-http-tower/src/map_request.rs @@ -19,6 +19,7 @@ pub struct MapRequestService { mapper: M, } +#[derive(Debug)] pub struct MapRequestLayer { mapper: M, }