Unverified Commit 66a3acf5 authored by Fahad Zubair's avatar Fahad Zubair Committed by GitHub
Browse files

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: default avatarFahad Zubair <fahadzub@amazon.com>
parent 1f7cc8e6
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ members = [
    "pokemon-service-lambda",
    "pokemon-service-server-sdk",
    "pokemon-service-client",
    "pokemon-service-client-usage",

]

+37 −0
Original line number Diff line number Diff line
[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 −0
Original line number Diff line number Diff line
# 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 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */
/// 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")
}
+158 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */
/// 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");
}
Loading