Unverified Commit 5da03af9 authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Revert "Introduce generic fluent client generator to non-sdk codegen (#463)" (#493)

This reverts commit 7774d687.
parent 7774d687
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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, Debug)]
#[derive(Clone)]
pub struct AwsEndpointStage;

#[derive(Debug)]
+1 −1
Original line number Diff line number Diff line
@@ -216,7 +216,7 @@ impl Display for ExecEnvMetadata {
}

#[non_exhaustive]
#[derive(Default, Clone, Debug)]
#[derive(Default, Clone)]
pub struct UserAgentStage;

impl UserAgentStage {
+2 −3
Original line number Diff line number Diff line
@@ -10,8 +10,8 @@ license = "Apache-2.0"
[features]
test-util = ["protocol-test-helpers"]
default = ["test-util"]
native-tls = ["hyper-tls", "smithy-client/native-tls"]
rustls = ["hyper-rustls", "smithy-client/rustls"]
native-tls = ["hyper-tls"]
rustls = ["hyper-rustls"]

[dependencies]
hyper = { version = "0.14.2", features = ["client", "http1", "http2", "tcp", "runtime"] }
@@ -28,7 +28,6 @@ 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"] }

+203 −0
Original line number Diff line number Diff line
/*
 * 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<hyper_tls::HttpsConnector<hyper::client::HttpConnector>, SdkBody>),

    /// An Https Connection
    ///
    /// This is the correct connection for use cases talking to real AWS services.
    #[cfg(feature = "rustls")]
    RustlsHttps(hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>, 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<dyn HttpService>),
}

impl Clone for Box<dyn HttpService> {
    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<Result<(), BoxError>>;

    /// Call this service and return a response
    ///
    /// See [`Service::call`](tower::Service::call)
    fn call(
        &mut self,
        req: http::Request<SdkBody>,
    ) -> Pin<Box<dyn Future<Output = Result<http::Response<SdkBody>, 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<dyn HttpService>;
}

/// 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<S> HttpService for S
where
    S: Service<http::Request<SdkBody>, Response = http::Response<SdkBody>>
        + Send
        + Sync
        + Clone
        + 'static,
    S::Error: Into<BoxError> + Send + Sync + 'static,
    S::Future: Send + 'static,
{
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
        Service::poll_ready(self, cx).map_err(|err| err.into())
    }

    fn call(
        &mut self,
        req: Request<SdkBody>,
    ) -> Pin<Box<dyn Future<Output = Result<Response<SdkBody>, 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<dyn HttpService> {
        Box::new(self.clone())
    }
}

impl tower::Service<http::Request<SdkBody>> for Standard {
    type Response = http::Response<SdkBody>;
    type Error = BoxError;
    type Future = StandardFuture;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        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<SdkBody>) -> 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<Box<dyn Future<Output = Result<http::Response<SdkBody>, BoxError>> + Send>>),
}

impl Future for StandardFuture {
    type Output = Result<http::Response<SdkBody>, BoxError>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        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),
        }
    }
}
+113 −64
Original line number Diff line number Diff line
@@ -3,54 +3,41 @@
 * SPDX-License-Identifier: Apache-2.0.
 */

#[doc(inline)]
pub use smithy_client::test_connection;
pub mod conn;
mod retry;
#[cfg(feature = "test-util")]
pub mod test_connection;

pub use smithy_client::retry::Config as RetryConfig;
pub use retry::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 std::fmt::Debug;
use tower::layer::util::Stack;
use tower::ServiceBuilder;
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};

type AwsMiddlewareStack = Stack<
    MapRequestLayer<SigV4SigningStage>,
    Stack<MapRequestLayer<UserAgentStage>, MapRequestLayer<AwsEndpointStage>>,
>;

#[derive(Debug, Default)]
#[non_exhaustive]
pub struct AwsMiddleware;
impl<S> tower::Layer<S> for AwsMiddleware {
    type Service = <AwsMiddlewareStack as tower::Layer<S>>::Service;

    fn layer(&self, inner: S) -> Self::Service {
        let signer = MapRequestLayer::for_mapper(SigV4SigningStage::new(SigV4Signer::new()));
        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)
    }
}
type BoxError = Box<dyn Error + Send + Sync>;
pub type StandardClient = Client<conn::Standard>;

/// AWS Service Client
///
/// Hyper-based AWS Service Client. Most customers will want to construct a client with
/// [`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).
/// [`Client::https()`](Client::https). For testing & other more advanced use cases, a custom
/// connector may be used via [`Client::new(connector)`](Client::new).
///
/// The internal connector must implement the following trait bound to be used to dispatch requests:
/// ```rust,ignore
@@ -61,54 +48,116 @@ impl<S> tower::Layer<S> for AwsMiddleware {
///    S::Error: Into<BoxError> + Send + Sync + 'static,
///    S::Future: Send + 'static,
/// ```
#[doc(inline)]
pub type Client<C> = smithy_client::Client<C, AwsMiddleware>;
pub struct Client<S> {
    inner: S,
    retry_handler: RetryHandlerFactory,
}

#[doc(inline)]
pub use smithy_client::erase::DynConnector;
pub type StandardClient = Client<DynConnector>;
impl<S> Debug for Client<S> {
    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::bounds::SmithyConnector;
impl<S> Client<S> {
    /// 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 type Builder<C> = smithy_client::Builder<C, AwsMiddleware>;
    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
        self.retry_handler.with_config(retry_config);
        self
    }
}

impl Client<Standard> {
    /// 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();
        Client {
            inner: Standard::https(),
            retry_handler: RetryHandlerFactory::new(RetryConfig::default()),
        }
    }
}

    with_https(smithy_client::Builder::new())
        .build()
        .into_dyn_connector()
impl<S> Client<S>
where
    S: Service<http::Request<SdkBody>, Response = http::Response<SdkBody>> + Send + Clone + 'static,
    S::Error: Into<BoxError> + 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<O, T, E, Retry>(&self, input: Operation<O, Retry>) -> Result<T, SdkError<E>>
    where
        O: ParseHttpResponse<SdkBody, Output = Result<T, E>> + Send + Sync + Clone + 'static,
        E: Error + ProvideErrorKind,
        Retry: ClassifyResponse<SdkSuccess<T>, SdkError<E>>,
    {
        self.call_raw(input).await.map(|res| res.parsed)
    }

mod static_tests {
    /// 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<O, R, E, Retry>(
        &self,
        input: Operation<O, Retry>,
    ) -> Result<SdkSuccess<R>, SdkError<E>>
    where
        O: ParseHttpResponse<SdkBody, Output = Result<R, E>> + Send + Sync + Clone + 'static,
        E: Error + ProvideErrorKind,
        Retry: ClassifyResponse<SdkSuccess<R>, SdkError<E>>,
    {
        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::<O, Retry>::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
    }
}

#[cfg(test)]
mod tests {

    #[cfg(any(feature = "rustls", feature = "native-tls"))]
    #[allow(dead_code)]
    #[test]
    fn construct_default_client() {
        let c = crate::Client::https();
        fn is_send_sync<T: 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"));
    }
}
Loading