diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 453eee98e4..9d38f8d778 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,6 +7,7 @@ members = [ "pokemon-service-lambda", "pokemon-service-server-sdk", "pokemon-service-client", + "pokemon-service-client-usage", ] diff --git a/examples/pokemon-service-client-usage/Cargo.toml b/examples/pokemon-service-client-usage/Cargo.toml new file mode 100644 index 0000000000..d3be7c83b1 --- /dev/null +++ b/examples/pokemon-service-client-usage/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "pokemon-service-client-usage" +version = "0.1.0" +edition = "2021" +publish = false + +[features] + + +[dependencies] +# The generated client utilizes types defined in other crates, such as `aws_smithy_types` +# and `aws_smithy_http`. However, most of these types are re-exported by the generated client, +# eliminating the need to directly depend on the crates that provide them. In rare instances, +# you may still need to include one of these crates as a dependency. Examples that require this +# are specifically noted in comments above the corresponding dependency in this file. +pokemon-service-client = { path = "../pokemon-service-client/" } + +# Required for getting the operation name from the `Metadata`. +aws-smithy-http = { path = "../../rust-runtime/aws-smithy-http/" } + +# Required for `Storable` and `StoreReplace` in `response-header-interceptor` example. +aws-smithy-types = { path = "../../rust-runtime/aws-smithy-types/" } + +# Required for `HyperClientBuilder` in `client-connector` example. +aws-smithy-runtime = { path = "../../rust-runtime/aws-smithy-runtime/", features=["test-util"] } + + + +hyper = { version = "0.14.25", features = ["client", "full"] } +tokio = {version = "1.26.0", features=["full"]} +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +rustls = "0.21.7" +hyper-rustls = "0.24.1" +http = "0.2.9" +uuid = {version="1.4.1", features = ["v4"]} +thiserror = "1.0.49" diff --git a/examples/pokemon-service-client-usage/README.md b/examples/pokemon-service-client-usage/README.md new file mode 100644 index 0000000000..f2b90eea95 --- /dev/null +++ b/examples/pokemon-service-client-usage/README.md @@ -0,0 +1,49 @@ +# smithy-rs Client Examples + +This package contains some examples on how to use the Smithy Client to communicate +with a Smithy-based service. + +## Pre-requisites + +1. Build the `pokemon-service-client` and `pokemon-service` by invoking `make` in the + [examples](https://github.com/awslabs/smithy-rs/tree/main/examples) folder. + +```console +make +``` + +2. Run the Pokemon service locally by issuing the following command from the + [examples](https://github.com/awslabs/smithy-rs/tree/main/examples) folder. This + will launch the Smithy-Rs based service on TCP port 13734. + +```console +cargo run --bin pokemon-service +``` + +## Running the examples + +You can view a list of examples by running `cargo run --example` from the +[pokemon-service-client-usage](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage) +folder. To run an example, pass its name to the `cargo run --example` command, e.g.: + +```console +cargo run --example simple-client +``` + +## List of examples + +| Rust Example | Description | +|--------------------------------|-------------------------------------------------------------------------| +| simple-client | Creates a Smithy Client and calls an operation on it. | +| endpoint-resolver | How to set a custom endpoint resolver. | +| handling-errors | How to send an input parameter to an operation, and to handle errors. | +| custom-header | How to add headers to a request. | +| custom-header-using-interceptor| How to access operation name being called in an interceptor. | +| response-header-interceptor | How to get operation name and access response before it is deserialized.| +| use-config-bag | How to use the property bag to pass data across interceptors. | +| retries-customize | Customize retry settings. | +| retries-disable | How to disable retries. | +| timeout-config | How to configure timeouts. | +| mock-request | Use a custom HttpConnector / Client to generate mock responses. | +| trace-serialize | Trace request and response as they are serialized / deserialized. | +| client-connector | Shows how to change TLS related configuration. | diff --git a/examples/pokemon-service-client-usage/examples/client-connector.rs b/examples/pokemon-service-client-usage/examples/client-connector.rs new file mode 100644 index 0000000000..61efcc0f69 --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/client-connector.rs @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to set connector settings. For example, how to set +/// trusted root certificates to use for HTTPs communication. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example client-connector`. +/// +use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder; +use hyper_rustls::ConfigBuilderExt; +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon +/// service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + let tls_config = rustls::ClientConfig::builder() + .with_safe_defaults() + // `with_native_roots()`: Load platform trusted root certificates. + // `with_webpki_roots()`: Load Mozilla’s set of trusted roots. + .with_native_roots() + // To use client side certificates, you can use + // `.with_client_auth_cert(client_cert, client_key)` instead of `.with_no_client_auth()` + .with_no_client_auth(); + + let tls_connector = hyper_rustls::HttpsConnectorBuilder::new() + .with_tls_config(tls_config) + // This can be changed to `.https_only()` to ensure that the client always uses HTTPs + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + + // Create a hyper-based HTTP client that uses this TLS connector. + let http_client = HyperClientBuilder::new().build(tls_connector); + + // Pass the smithy connector to the Client::ConfigBuilder + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .http_client(http_client) + .build(); + + // Instantiate a client by applying the configuration. + pokemon_service_client::Client::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(?response, "Response from service") +} diff --git a/examples/pokemon-service-client-usage/examples/custom-header-using-interceptor.rs b/examples/pokemon-service-client-usage/examples/custom-header-using-interceptor.rs new file mode 100644 index 0000000000..b01d1fe2bb --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/custom-header-using-interceptor.rs @@ -0,0 +1,158 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// In this example, a custom header `x-amzn-client-ttl-seconds` is set for all outgoing requests. +/// It serves as a demonstration of how an operation name can be retrieved and utilized within +/// the interceptor. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example custom-header-using-interceptor`. +/// +use std::{collections::HashMap, time::Duration}; + +use pokemon_service_client::config::{ConfigBag, Intercept}; +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client::{ + config::{interceptors::BeforeTransmitInterceptorContextMut, RuntimeComponents}, + error::BoxError, +}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +// The `TtlHeaderInterceptor` keeps a map of operation specific value to send +// in the header for each Request. +#[derive(Debug)] +pub struct TtlHeaderInterceptor { + /// Default time-to-live for an operation. + default_ttl: hyper::http::HeaderValue, + /// Operation specific time-to-live. + operation_ttl: HashMap<&'static str, hyper::http::HeaderValue>, +} + +// Helper function to format duration as fractional seconds. +fn format_ttl_value(ttl: Duration) -> String { + format!("{:.2}", ttl.as_secs_f64()) +} + +impl TtlHeaderInterceptor { + fn new(default_ttl: Duration) -> Self { + let duration_str = format_ttl_value(default_ttl); + let default_ttl_value = hyper::http::HeaderValue::from_str(duration_str.as_str()) + .expect("could not create a header value for the default ttl"); + + Self { + default_ttl: default_ttl_value, + operation_ttl: Default::default(), + } + } + + /// Adds an operation name specific timeout value that needs to be set in the header. + fn add_operation_ttl(&mut self, operation_name: &'static str, ttl: Duration) { + let duration_str = format_ttl_value(ttl); + + self.operation_ttl.insert( + operation_name, + hyper::http::HeaderValue::from_str(duration_str.as_str()) + .expect("cannot create header value for the given ttl duration"), + ); + } +} + +/// Appends the header `x-amzn-client-ttl-seconds` using either the default time-to-live value +/// or an operation-specific value if it was set earlier using `add_operation_ttl`. +//impl aws_smithy_runtime_api::client::interceptors::Interceptor for TtlHeaderInterceptor { +impl Intercept for TtlHeaderInterceptor { + fn name(&self) -> &'static str { + "TtlHeaderInterceptor" + } + + /// Before the request is signed, add the header to the outgoing request. + fn modify_before_signing( + &self, + context: &mut BeforeTransmitInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // Metadata in the ConfigBag has the operation name. + let metadata = cfg + .load::() + .expect("metadata should exist"); + let operation_name = metadata.name(); + + // Get operation specific or default HeaderValue to set for the header key. + let ttl = self + .operation_ttl + .get(operation_name) + .unwrap_or(&self.default_ttl); + + context + .request_mut() + .headers_mut() + .insert("x-amzn-client-ttl-seconds", ttl.clone()); + + tracing::info!("{operation_name} header set to {ttl:?}"); + + Ok(()) + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // By default set the value of all operations to 6 seconds. + const DEFAULT_TTL: Duration = Duration::from_secs(6); + + // Set up the interceptor to add an operation specific value of 3.5 seconds to be added + // for GetStorage operation. + let mut ttl_headers_interceptor = TtlHeaderInterceptor::new(DEFAULT_TTL); + ttl_headers_interceptor.add_operation_ttl("GetStorage", Duration::from_millis(3500)); + + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .interceptor(ttl_headers_interceptor) + .build(); + + pokemon_service_client::Client::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response for get_server_statistics()"); + + // Call the operation `get_storage` on the Pokémon service. The `TtlHeaderInterceptor` + // interceptor will add a specific header name / value pair for this operation. + let response = client + .get_storage() + .user("ash") + .passcode("pikachu123") + .send() + .await + .expect("operation failed"); + + // Print the response received from the service. + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/pokemon-service-client-usage/examples/custom-header.rs b/examples/pokemon-service-client-usage/examples/custom-header.rs new file mode 100644 index 0000000000..98dc20e2d5 --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/custom-header.rs @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to create a `smithy-rs` client, and call an operation with custom +/// headers in the request. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example custom-header` +/// +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon +/// service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .build(); + + // Apply the configuration on the client, and return that. + pokemon_service_client::Client::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .customize() + .mutate_request(|req| { + // For demonstration purposes, add a header `x-ttl-seconds` to the outgoing request. + let headers = req.headers_mut(); + headers.insert( + hyper::header::HeaderName::from_static("x-ttl-seconds"), + hyper::header::HeaderValue::from(30), + ); + }) + .send() + .await + .expect("operation failed"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/pokemon-service-client-usage/examples/endpoint-resolver.rs b/examples/pokemon-service-client-usage/examples/endpoint-resolver.rs new file mode 100644 index 0000000000..d37479fa6c --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/endpoint-resolver.rs @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how a custom `ResolveEndpoint` can be implemented for resolving +/// endpoint of a request. Additionally, it shows how a header can be added using the endpoint +/// builder. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example endpoint-resolver`. +/// +use pokemon_service_client::config::endpoint::{Endpoint, EndpointFuture, Params, ResolveEndpoint}; +use pokemon_service_client::primitives::{DateTime, DateTimeFormat}; +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client_usage::setup_tracing_subscriber; + +use std::time::SystemTime; + +// This struct, provided as an example, constructs the URL that should be set on each request during initialization. +// It also implements the `ResolveEndpoint` trait, enabling it to be assigned as the endpoint_resolver in the `Config`. +#[derive(Debug)] +struct RegionalEndpoint { + url_to_use: String, +} + +impl RegionalEndpoint { + fn new(regional_url: &str, port: u16) -> Self { + let url_to_use = format!("{}:{}", regional_url, port); + RegionalEndpoint { url_to_use } + } +} + +impl ResolveEndpoint for RegionalEndpoint { + fn resolve_endpoint<'a>(&'a self, _params: &'a Params) -> EndpointFuture<'a> { + // Construct an endpoint using the Endpoint::Builder. Set the URL and, + // optionally, any headers to be sent with the request. For this example, + // we'll set the 'x-amz-date' header to the current date for all outgoing requests. + // `DateTime` can be used for formatting an RFC 3339 date time. + let now = SystemTime::now(); + let date_time = DateTime::from(now); + + let endpoint = Endpoint::builder() + .url(self.url_to_use.clone()) + .header( + "x-amz-date", + date_time + .fmt(DateTimeFormat::DateTimeWithOffset) + .expect("Could not create a date in UTC format"), + ) + .build(); + tracing::info!(?endpoint, "Resolving endpoint"); + EndpointFuture::ready(Ok(endpoint)) + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + const DEFAULT_PORT: u16 = 13734; + + // Use the environment variable `REGIONAL_URL` for the URL. + let resolver = RegionalEndpoint::new( + std::env::var("REGIONAL_URL") + .as_deref() + .unwrap_or("http://localhost"), + DEFAULT_PORT, + ); + + let config = pokemon_service_client::Config::builder() + .endpoint_resolver(resolver) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(?response, "Response received"); +} diff --git a/examples/pokemon-service-client-usage/examples/handling-errors.rs b/examples/pokemon-service-client-usage/examples/handling-errors.rs new file mode 100644 index 0000000000..ee1da30cbb --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/handling-errors.rs @@ -0,0 +1,135 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// Copyright © 2023, Amazon, LLC. +/// +/// This example demonstrates how to handle service generated errors. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example handling-errors`. +/// +use pokemon_service_client::{error::SdkError, operation::get_storage::GetStorageError}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +use pokemon_service_client::Client as PokemonClient; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // The following example sends an incorrect passcode to the operation `get_storage`, + // which will return + // [StorageAccessNotAuthorized](https://github.com/awslabs/smithy-rs/blob/main/codegen-core/common-test-models/pokemon.smithy#L48) + let response_result = client + .get_storage() + .user("ash") + // Give a wrong password to generate a service error. + .passcode("pkachu123") + .send() + .await; + + // All errors are consolidated into an `SdkError` + match response_result { + Ok(response) => { + tracing::info!(?response, "Response from service") + } + Err(SdkError::ServiceError(se)) => { + // When an error response is received from the service, it is modeled + // as a `SdkError::ServiceError`. + match se.err() { + // Not authorized to access Pokémon storage. + GetStorageError::StorageAccessNotAuthorized(_) => { + tracing::error!("You do not have access to this resource."); + } + GetStorageError::ResourceNotFoundError(rnfe) => { + let message = rnfe.message(); + tracing::error!(error = %message, + "Given Pikachu does not exist on the server." + ) + } + GetStorageError::ValidationError(ve) => { + tracing::error!(error = %ve, "A required field has not been set."); + } + // An unexpected error occurred (e.g., invalid JSON returned by the service or an unknown error code). + GetStorageError::Unhandled(uh) => { + tracing::error!(error = %uh, "An unhandled error has occurred.") + } + // The SdkError is marked as `#[non_exhaustive]`. Therefore, a catch-all pattern is required to handle + // potential future variants introduced in SdkError. + _ => { + tracing::error!(error = %se.err(), "Some other error has occurred on the server") + } + } + } + Err(SdkError::TimeoutError(_)) => { + tracing::error!("The request timed out and could not be completed"); + } + Err(SdkError::ResponseError(re)) => { + // Raw response received from the service can be retrieved using + // the `raw()` method. + tracing::error!( + "An unparsable response was received. Raw response: {:?}", + re.raw() + ); + } + Err(sdk_error) => { + // To retrieve the `source()` of an error within the following match statements, + // we work with the parent `SdkError` type, as individual variants don't directly provide it. + // Converting the parent error to its source transfers ownership of the variable. + match sdk_error { + SdkError::DispatchFailure(ref failure) => { + if failure.is_io() { + tracing::error!("An I/O error occurred"); + } else if failure.is_timeout() { + tracing::error!("Request timed out"); + } else if failure.is_user() { + tracing::error!("An invalid HTTP request has been provided"); + } else { + tracing::error!("Some other dispatch error occurred."); + }; + + if let Ok(source) = sdk_error.into_source() { + tracing::error!(%source, "Error source"); + } + } + SdkError::ConstructionFailure(_) => { + if let Ok(source) = sdk_error.into_source() { + tracing::error!(%source, "Request could not be constructed."); + } else { + tracing::error!("Request could not be constructed for unknown reasons"); + } + } + _ => { + tracing::error!("An unknown error has occurred"); + } + } + } + } +} diff --git a/examples/pokemon-service-client-usage/examples/mock-request.rs b/examples/pokemon-service-client-usage/examples/mock-request.rs new file mode 100644 index 0000000000..64f21beb65 --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/mock-request.rs @@ -0,0 +1,76 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to use a mock connector with `capture_request`. This allows for +/// responding with a static `Response` while capturing the incoming request. The captured request +/// can later be asserted to verify that the correct headers and body were sent to the server. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example mock-request`. +/// +use aws_smithy_runtime::client::http::test_util::capture_request; +use aws_smithy_types::body::SdkBody; +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Build a response that should be sent when the operation is called. + let response = http::Response::builder() + .status(200) + .body(SdkBody::from(r#"{"calls_count":100}"#)) + .expect("response could not be constructed"); + + // Call `capture_request` to obtain a HTTP connector and a request receiver. + // The request receiver captures the incoming request, while the connector can be passed + // to `Config::builder().http_client`. + let (http_client, captured_request) = capture_request(Some(response)); + + // Pass the `http_client` connector to `Config::builder`. The connector won't send + // the request over the network; instead, it will return the static response provided + // during its initialization. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .http_client(http_client) + .build(); + + // Instantiate a client by applying the configuration. + let client = PokemonClient::from_conf(config); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .customize() + .mutate_request(|req| { + // For demonstration, send an extra header that can be verified to confirm + // that the client actually sends it. + let headers = req.headers_mut(); + headers.insert( + hyper::header::HeaderName::from_static("user-agent"), + hyper::header::HeaderName::from_static("sample-client"), + ); + }) + .send() + .await + .expect("operation failed"); + + // Print the response received from the service. + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); + + // The captured request can be verified to have certain headers. + let req = captured_request.expect_request(); + assert_eq!(req.headers().get("user-agent"), Some("sample-client")); + + // As an example, you can verify the URL matches. + assert_eq!(req.uri(), "http://localhost:13734/stats"); + + // You can convert the captured body into a &str and use assert! + // on it if you want to verify the contents of the request body. + // let str_body = std::str::from_utf8(req.body().bytes().unwrap()).unwrap(); +} diff --git a/examples/pokemon-service-client-usage/examples/response-header-interceptor.rs b/examples/pokemon-service-client-usage/examples/response-header-interceptor.rs new file mode 100644 index 0000000000..14928fe28c --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/response-header-interceptor.rs @@ -0,0 +1,171 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how response headers can be examined before they are deserialized +/// into the output type. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example response-header-interceptor`. +/// +//use aws_smithy_types::config_bag::{Storable, StoreReplace}; +use aws_smithy_types::config_bag::{Storable, StoreReplace}; +use pokemon_service_client::{ + config::{ + interceptors::{ + BeforeDeserializationInterceptorContextRef, BeforeTransmitInterceptorContextMut, + }, + ConfigBag, Intercept, RuntimeComponents, + }, + error::BoxError, + Client as PokemonClient, +}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; +use uuid::Uuid; + +#[derive(Debug, Clone)] +struct RequestId { + client_id: String, + server_id: Option, +} + +impl Storable for RequestId { + type Storer = StoreReplace; +} + +#[derive(Debug, thiserror::Error)] +enum RequestIdError { + /// The server-sent request ID cannot be converted into a string during parsing. + #[error("RequestID sent by the server cannot be parsed into a string. Error: {0}")] + NonParsableServerRequestId(String), + /// Client side + #[error("Client side request ID has not been set")] + ClientRequestIdMissing(), +} + +#[derive(Debug, Default)] +pub struct ResponseHeaderLoggingInterceptor; + +impl ResponseHeaderLoggingInterceptor { + /// Creates a new `ResponseHeaderLoggingInterceptor` + pub fn new() -> Self { + Self::default() + } +} + +impl Intercept for ResponseHeaderLoggingInterceptor { + fn name(&self) -> &'static str { + "ResponseHeaderLoggingInterceptor" + } + + /// Before the request is signed, add the header to the outgoing request. + fn modify_before_signing( + &self, + context: &mut BeforeTransmitInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + let client_id = Uuid::new_v4().to_string(); + + let request_id = hyper::header::HeaderValue::from_str(&client_id) + .expect("failed to construct a header value from UUID"); + context + .request_mut() + .headers_mut() + .insert("x-amzn-requestid", request_id); + + cfg.interceptor_state().store_put(RequestId { + client_id, + server_id: None, + }); + + Ok(()) + } + + fn read_before_deserialization( + &self, + context: &BeforeDeserializationInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // `Metadata` in the `ConfigBag` has the operation name in it. + let metadata = cfg + .load::() + .expect("metadata should exist"); + let operation_name = metadata.name().to_string(); + + // Get the server side request ID and set it in the RequestID data type + // that is in the ConfigBag. This way any other interceptor that requires the mapping + // can easily find it from the bag. + let response = context.response(); + let header_received = response + .headers() + .iter() + .find(|(header_name, _)| *header_name == "x-request-id"); + + if let Some((_, server_id)) = header_received { + let server_id = server_id + .to_str() + .map_err(|e| Box::new(RequestIdError::NonParsableServerRequestId(e.to_string())))?; + + let request_details = cfg + .get_mut::() + .ok_or_else(|| Box::new(RequestIdError::ClientRequestIdMissing()))?; + + tracing::info!(operation = %operation_name, + "RequestID Mapping: {} = {server_id}", + request_details.client_id, + ); + + request_details.server_id = Some(server_id.into()); + } else { + tracing::info!(operation = %operation_name, "Server RequestID missing in response"); + } + + Ok(()) + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon +/// service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .interceptor(ResponseHeaderLoggingInterceptor) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + // If you need to access the `RequestIdError` raised by the interceptor, + // you can convert `SdkError::DispatchFailure` to a `ConnectorError` + // and then use `downcast_ref` on its source to get a `RequestIdError`. + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/pokemon-service-client-usage/examples/retry-classifier.rs b/examples/pokemon-service-client-usage/examples/retry-classifier.rs new file mode 100644 index 0000000000..7ff17f0e66 --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/retry-classifier.rs @@ -0,0 +1,112 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how a custom RetryClassifier can be written to decide +/// which error conditions should be retried. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example retry-classifier`. +/// +use http::StatusCode; +use pokemon_service_client::{ + config::{ + interceptors::InterceptorContext, + retry::{ClassifyRetry, RetryAction, RetryConfig}, + }, + operation::get_server_statistics::GetServerStatisticsError, +}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; +use std::time::Duration; + +use pokemon_service_client::Client as PokemonClient; + +#[derive(Debug)] +struct SampleRetryClassifier; + +// By default, the generated client uses the `aws_http::retry::AwsResponseRetryClassifier` +// to determine whether an error should be retried. To use a custom retry classifier, +// implement the `ClassifyRetry` trait and pass it to the retry_classifier method +// of the `Config::builder`. +impl ClassifyRetry for SampleRetryClassifier { + fn name(&self) -> &'static str { + "SampleRetryClassifier" + } + + // For this example, the classifier should retry in case the error is GetServerStatisticsError + // and the status code is 503. + fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction { + // Get the output or error that has been deserialized from the response. + let output_or_error = ctx.output_or_error(); + + let error = match output_or_error { + Some(Ok(_)) | None => return RetryAction::NoActionIndicated, + Some(Err(err)) => err, + }; + + // Retry in case the error returned is GetServerStatisticsError and StatusCode is 503. + if let Some(_err) = error + .as_operation_error() + .and_then(|err| err.downcast_ref::()) + { + if let Some(response) = ctx.response() { + if response.status() == StatusCode::SERVICE_UNAVAILABLE { + return RetryAction::server_error(); + } + } + } + + // Let other classifiers run and decide if the request should be retried. + // Returning RetryAction::RetryForbidden will forbid any retries. + RetryAction::NoActionIndicated + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // By default the Smithy client uses RetryConfig::standard() strategy, with 3 retries, and + // an initial exponential back off of 1 second. To turn it off use RetryConfig::disabled(). + let retry_config = RetryConfig::standard() + .with_initial_backoff(Duration::from_secs(3)) + .with_max_attempts(5); + + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .retry_config(retry_config) + // Add the retry classifier. + .retry_classifier(SampleRetryClassifier {}) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/pokemon-service-client-usage/examples/retry-customize.rs b/examples/pokemon-service-client-usage/examples/retry-customize.rs new file mode 100644 index 0000000000..5deb3af0ff --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/retry-customize.rs @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to customize retry settings on a Smithy client. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example retry-customize`. +/// +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; +use std::time::Duration; + +use pokemon_service_client::{config::retry::RetryConfig, Client as PokemonClient}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // By default the Smithy client uses `RetryConfig::standard()` strategy, with 3 retries, and + // an initial exponential back off of 1 second. To turn it off use `RetryConfig::disabled()`. + let retry_config = RetryConfig::standard() + .with_initial_backoff(Duration::from_secs(3)) + .with_max_attempts(5); + + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .retry_config(retry_config) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/pokemon-service-client-usage/examples/simple-client.rs b/examples/pokemon-service-client-usage/examples/simple-client.rs new file mode 100644 index 0000000000..e04e2345e8 --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/simple-client.rs @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to create a `smithy-rs` Client and call an +/// [operation](https://smithy.io/2.0/spec/idl.html?highlight=operation#operation-shape). +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example simple-client`. +/// +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon +/// service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // The generated client contains a type `config::Builder` for constructing a `Config` instance. + // This enables configuration of endpoint resolvers, timeouts, retries, etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .build(); + + // Instantiate a client by applying the configuration. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + // Print the response received from the service. + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/pokemon-service-client-usage/examples/timeout-config.rs b/examples/pokemon-service-client-usage/examples/timeout-config.rs new file mode 100644 index 0000000000..cbfbb8e7e4 --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/timeout-config.rs @@ -0,0 +1,64 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to create a `smithy-rs` Client and set connection +/// and operation related timeouts on the client. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example timeout-config` +/// +use std::time::Duration; + +use pokemon_service_client::{config::timeout::TimeoutConfig, Client as PokemonClient}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // Different type of timeouts can be set on the client. These are: + // operation_attempt_timeout - If retries are enabled, this represents the timeout + // for each individual operation attempt. + // operation_timeout - Overall timeout for the operation to complete. + // connect timeout - The amount of time allowed for a connection to be established. + let timeout_config = TimeoutConfig::builder() + .operation_attempt_timeout(Duration::from_secs(1)) + .operation_timeout(Duration::from_secs(5)) + .connect_timeout(Duration::from_millis(500)) + .build(); + + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .timeout_config(timeout_config) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("Pokemon service does not seem to be running on localhost:13734"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/pokemon-service-client-usage/examples/trace-serialize.rs b/examples/pokemon-service-client-usage/examples/trace-serialize.rs new file mode 100644 index 0000000000..7826679e2e --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/trace-serialize.rs @@ -0,0 +1,113 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how an interceptor can be written to trace what is being +/// serialized / deserialized on the wire. +/// +/// Please beware that this may log sensitive information! This example is meant for pedagogical +/// purposes and may be useful in debugging scenarios. Please don't use this as-is in production. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example trace-serialize`. +/// +use http::StatusCode; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; +use std::str; + +use pokemon_service_client::{ + config::{ + interceptors::{ + BeforeDeserializationInterceptorContextRef, BeforeTransmitInterceptorContextRef, + }, + ConfigBag, Intercept, RuntimeComponents, + }, + error::BoxError, + Client as PokemonClient, +}; + +/// An example interceptor that logs the request and response as they're sent and received. +#[derive(Debug, Default)] +pub struct WireFormatInterceptor; + +impl Intercept for WireFormatInterceptor { + fn name(&self) -> &'static str { + "WireFormatInterceptor" + } + + // Called after the operation input has been serialized but before it's dispatched over the wire. + fn read_after_serialization( + &self, + context: &BeforeTransmitInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + _cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // Get the request type from the context. + let request = context.request(); + // Print the request to the debug tracing log. + tracing::debug!(?request); + + Ok(()) + } + + // Called after the operation's response has been received but before it's deserialized into the + // operation's output type. + fn read_before_deserialization( + &self, + context: &BeforeDeserializationInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + _cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // Get the response type from the context. + let response = context.response(); + // Print the response. + if response.status() == StatusCode::OK { + tracing::info!(?response, "Response received:"); + } else { + tracing::error!(?response); + } + + Ok(()) + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .interceptor(WireFormatInterceptor {}) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/pokemon-service-client-usage/examples/use-config-bag.rs b/examples/pokemon-service-client-usage/examples/use-config-bag.rs new file mode 100644 index 0000000000..6dd9cba3a5 --- /dev/null +++ b/examples/pokemon-service-client-usage/examples/use-config-bag.rs @@ -0,0 +1,141 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how different interceptor can use a property bag to pass +/// state from one interceptor to the next. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/awslabs/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example use-config-bag`. +/// +//use aws_smithy_types::config_bag::{Storable, StoreReplace}; +use aws_smithy_types::config_bag::{Storable, StoreReplace}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; +use std::time::Instant; + +use pokemon_service_client::{ + config::{ + interceptors::{ + BeforeDeserializationInterceptorContextRef, FinalizerInterceptorContextRef, + }, + ConfigBag, Intercept, RuntimeComponents, + }, + error::BoxError, + Client as PokemonClient, +}; + +#[derive(Debug)] +struct RequestTimestamp(Instant); + +impl Storable for RequestTimestamp { + type Storer = StoreReplace; +} + +#[derive(Debug, Default)] +pub struct SetTimeInterceptor; + +/// Note: This is merely an example demonstrating how state can +/// be shared between two different interceptors. In a practical +/// scenario, there wouldn't be a need to write two interceptors +/// merely to display the duration from the start of the lifecycle +/// to the receipt of the response. This task can be accomplished +/// within a single interceptor by overriding both +/// read_before_execution and read_before_deserialization. +impl Intercept for SetTimeInterceptor { + fn name(&self) -> &'static str { + "SetTimeInterceptor" + } + + fn read_before_execution( + &self, + _context: &pokemon_service_client::config::interceptors::BeforeSerializationInterceptorContextRef<'_>, + cfg: &mut aws_smithy_types::config_bag::ConfigBag, + ) -> Result<(), pokemon_service_client::error::BoxError> { + cfg.interceptor_state() + .store_put(RequestTimestamp(Instant::now())); + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct GetTimeInterceptor; + +impl Intercept for GetTimeInterceptor { + fn name(&self) -> &'static str { + "GetTimeInterceptor" + } + + fn read_before_deserialization( + &self, + _context: &BeforeDeserializationInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + let stop_watch = cfg + .load::() + .expect("StopWatch not found in the ConfigBag"); + + let time_taken = stop_watch.0.elapsed(); + tracing::info!(time_taken = %time_taken.as_micros(), "Microseconds:"); + + Ok(()) + } + + fn read_after_execution( + &self, + _context: &FinalizerInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), pokemon_service_client::error::BoxError> { + let timestamp = cfg + .load::() + .expect("RequestTimeStamp not found in the ConfigBag"); + + let time_taken = timestamp.0.elapsed(); + tracing::info!(time_taken = %time_taken.as_micros(), "Microseconds:"); + + Ok(()) + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .interceptor(SetTimeInterceptor) + .interceptor(GetTimeInterceptor) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("Pokemon service does not seem to be running on localhost:13734"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/pokemon-service-client-usage/src/lib.rs b/examples/pokemon-service-client-usage/src/lib.rs new file mode 100644 index 0000000000..6612ddb4c4 --- /dev/null +++ b/examples/pokemon-service-client-usage/src/lib.rs @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +pub static POKEMON_SERVICE_URL: &str = "http://localhost:13734"; + +/// Sets up the tracing subscriber to print `tracing::info!` and `tracing::error!` messages on the console. +pub fn setup_tracing_subscriber() { + // Add a tracing subscriber that uses the environment variable RUST_LOG + // to figure out which log level should be emitted. By default use `tracing::info!` + // as the logging level. + let filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) + .from_env_lossy(); + + tracing_subscriber::fmt::fmt() + .with_env_filter(filter) + .init(); +}