Skip to content

Commit

Permalink
Client examples that use the generic client have been added (#2799)
Browse files Browse the repository at this point in the history
## 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 <[email protected]>
  • Loading branch information
drganjoo and Fahad Zubair authored Oct 20, 2023
1 parent 1f7cc8e commit 66a3acf
Show file tree
Hide file tree
Showing 17 changed files with 1,427 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"pokemon-service-lambda",
"pokemon-service-server-sdk",
"pokemon-service-client",
"pokemon-service-client-usage",

]

Expand Down
37 changes: 37 additions & 0 deletions examples/pokemon-service-client-usage/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
49 changes: 49 additions & 0 deletions examples/pokemon-service-client-usage/README.md
Original file line number Diff line number Diff line change
@@ -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. |
74 changes: 74 additions & 0 deletions examples/pokemon-service-client-usage/examples/client-connector.rs
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
@@ -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");
}
62 changes: 62 additions & 0 deletions examples/pokemon-service-client-usage/examples/custom-header.rs
Original file line number Diff line number Diff line change
@@ -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");
}
Loading

0 comments on commit 66a3acf

Please sign in to comment.