Client examples that use the generic client have been added (#2799)

## Motivation and Context
Example code that demonstrates the usage of pokemon-service-client. 

## Description

Examples have been added that show how to add middleware, configure
retries, timeouts, and handle errors when calling operations on the
pokemon-service.


_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._

---------

Co-authored-by: Fahad Zubair <fahadzub@amazon.com>
This commit is contained in:
Fahad Zubair 2023-10-20 10:50:59 -04:00 committed by GitHub
parent 1f7cc8e69e
commit 66a3acf5e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1427 additions and 0 deletions

View File

@ -7,6 +7,7 @@ members = [
"pokemon-service-lambda",
"pokemon-service-server-sdk",
"pokemon-service-client",
"pokemon-service-client-usage",
]

View File

@ -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"

View File

@ -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. |

View File

@ -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 Mozillas 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")
}

View File

@ -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::<aws_smithy_http::operation::Metadata>()
.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");
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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<T, R>`
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");
}
}
}
}
}

View File

@ -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();
}

View File

@ -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<String>,
}
impl Storable for RequestId {
type Storer = StoreReplace<Self>;
}
#[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::<aws_smithy_http::operation::Metadata>()
.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::<RequestId>()
.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");
}

View File

@ -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::<GetServerStatisticsError>())
{
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");
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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<Self>;
}
#[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::<RequestTimestamp>()
.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::<RequestTimestamp>()
.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");
}

View File

@ -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();
}