AWS SDK

AWS SDK

rev. a16ec9092803c091887fa244df7aa8e7df1c79c8

Files changed:

tmp-codegen-diff/aws-sdk/Cargo.lock

@@ -397,397 +459,459 @@
  417    417   
checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878"
  418    418   
dependencies = [
  419    419   
 "aws-lc-fips-sys",
  420    420   
 "aws-lc-sys",
  421    421   
 "untrusted 0.7.1",
  422    422   
 "zeroize",
  423    423   
]
  424    424   
  425    425   
[[package]]
  426    426   
name = "aws-lc-sys"
  427         -
version = "0.28.0"
         427  +
version = "0.28.2"
  428    428   
source = "registry+https://github.com/rust-lang/crates.io-index"
  429         -
checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f"
         429  +
checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1"
  430    430   
dependencies = [
  431    431   
 "bindgen",
  432    432   
 "cc",
  433    433   
 "cmake",
  434    434   
 "dunce",
  435    435   
 "fs_extra",
  436    436   
]
  437    437   
  438    438   
[[package]]
  439    439   
name = "aws-runtime"
@@ -1243,1243 +1356,1360 @@
 1263   1263   
 "tokio",
 1264   1264   
 "tracing",
 1265   1265   
]
 1266   1266   
 1267   1267   
[[package]]
 1268   1268   
name = "aws-smithy-http-auth"
 1269   1269   
version = "0.60.3"
 1270   1270   
 1271   1271   
[[package]]
 1272   1272   
name = "aws-smithy-http-client"
 1273         -
version = "1.0.1"
        1273  +
version = "1.0.2"
 1274   1274   
dependencies = [
 1275   1275   
 "aws-smithy-async",
 1276   1276   
 "aws-smithy-protocol-test",
 1277   1277   
 "aws-smithy-runtime-api",
 1278   1278   
 "aws-smithy-types",
 1279   1279   
 "bytes",
 1280   1280   
 "h2 0.4.9",
 1281   1281   
 "http 0.2.12",
 1282   1282   
 "http 1.3.1",
 1283   1283   
 "http-body 0.4.6",
 1284   1284   
 "http-body 1.0.1",
 1285   1285   
 "http-body-util",
 1286   1286   
 "hyper 0.14.32",
 1287   1287   
 "hyper 1.6.0",
 1288   1288   
 "hyper-rustls 0.24.2",
 1289   1289   
 "hyper-rustls 0.27.5",
 1290   1290   
 "hyper-util",
 1291   1291   
 "indexmap",
 1292   1292   
 "pin-project-lite",
 1293   1293   
 "rustls 0.21.12",
 1294   1294   
 "rustls 0.23.26",
 1295   1295   
 "rustls-native-certs 0.8.1",
 1296   1296   
 "rustls-pemfile 2.2.0",
 1297   1297   
 "rustls-pki-types",
 1298   1298   
 "s2n-tls",
 1299   1299   
 "s2n-tls-hyper",
 1300   1300   
 "serde",
 1301   1301   
 "serde_json",
 1302   1302   
 "tokio",
 1303   1303   
 "tokio-rustls 0.26.2",
 1304   1304   
 "tower",
 1305   1305   
 "tracing",
 1306   1306   
]
 1307   1307   
 1308   1308   
[[package]]
 1309   1309   
name = "aws-smithy-http-tower"
 1310   1310   
version = "0.60.3"
 1311   1311   
 1312   1312   
[[package]]
 1313   1313   
name = "aws-smithy-json"
 1314   1314   
version = "0.61.3"
 1315   1315   
dependencies = [
 1316   1316   
 "aws-smithy-types",
 1317   1317   
 "proptest",
 1318   1318   
 "serde_json",
 1319   1319   
]
 1320   1320   
 1321   1321   
[[package]]
 1322         -
name = "aws-smithy-mocks-experimental"
 1323         -
version = "0.2.3"
        1322  +
name = "aws-smithy-mocks"
        1323  +
version = "0.1.0"
 1324   1324   
dependencies = [
        1325  +
 "aws-smithy-async",
        1326  +
 "aws-smithy-http-client",
        1327  +
 "aws-smithy-runtime",
 1325   1328   
 "aws-smithy-runtime-api",
 1326   1329   
 "aws-smithy-types",
        1330  +
 "http 1.3.1",
 1327   1331   
 "tokio",
 1328   1332   
]
 1329   1333   
 1330   1334   
[[package]]
 1331   1335   
name = "aws-smithy-observability"
 1332   1336   
version = "0.1.3"
 1333   1337   
dependencies = [
 1334   1338   
 "aws-smithy-runtime-api",
 1335   1339   
 "serial_test",
 1336   1340   
]
@@ -1736,1740 +1807,1811 @@
 1756   1760   
source = "registry+https://github.com/rust-lang/crates.io-index"
 1757   1761   
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
 1758   1762   
dependencies = [
 1759   1763   
 "glob",
 1760   1764   
 "libc",
 1761   1765   
 "libloading",
 1762   1766   
]
 1763   1767   
 1764   1768   
[[package]]
 1765   1769   
name = "clap"
 1766         -
version = "4.5.36"
        1770  +
version = "4.5.37"
 1767   1771   
source = "registry+https://github.com/rust-lang/crates.io-index"
 1768         -
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
        1772  +
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
 1769   1773   
dependencies = [
 1770   1774   
 "clap_builder",
 1771   1775   
]
 1772   1776   
 1773   1777   
[[package]]
 1774   1778   
name = "clap_builder"
 1775         -
version = "4.5.36"
        1779  +
version = "4.5.37"
 1776   1780   
source = "registry+https://github.com/rust-lang/crates.io-index"
 1777         -
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
        1781  +
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
 1778   1782   
dependencies = [
 1779   1783   
 "anstyle",
 1780   1784   
 "clap_lex",
 1781   1785   
]
 1782   1786   
 1783   1787   
[[package]]
 1784   1788   
name = "clap_lex"
 1785   1789   
version = "0.7.4"
 1786   1790   
source = "registry+https://github.com/rust-lang/crates.io-index"
 1787   1791   
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
@@ -3420,3424 +3482,3486 @@
 3440   3444   
source = "registry+https://github.com/rust-lang/crates.io-index"
 3441   3445   
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
 3442   3446   
dependencies = [
 3443   3447   
 "proc-macro2",
 3444   3448   
 "quote",
 3445   3449   
 "version_check",
 3446   3450   
]
 3447   3451   
 3448   3452   
[[package]]
 3449   3453   
name = "proc-macro2"
 3450         -
version = "1.0.94"
        3454  +
version = "1.0.95"
 3451   3455   
source = "registry+https://github.com/rust-lang/crates.io-index"
 3452         -
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
        3456  +
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
 3453   3457   
dependencies = [
 3454   3458   
 "unicode-ident",
 3455   3459   
]
 3456   3460   
 3457   3461   
[[package]]
 3458   3462   
name = "proptest"
 3459   3463   
version = "1.6.0"
 3460   3464   
source = "registry+https://github.com/rust-lang/crates.io-index"
 3461   3465   
checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50"
 3462   3466   
dependencies = [
@@ -3865,3869 +3927,3931 @@
 3885   3889   
name = "same-file"
 3886   3890   
version = "1.0.6"
 3887   3891   
source = "registry+https://github.com/rust-lang/crates.io-index"
 3888   3892   
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
 3889   3893   
dependencies = [
 3890   3894   
 "winapi-util",
 3891   3895   
]
 3892   3896   
 3893   3897   
[[package]]
 3894   3898   
name = "scc"
 3895         -
version = "2.3.3"
        3899  +
version = "2.3.4"
 3896   3900   
source = "registry+https://github.com/rust-lang/crates.io-index"
 3897         -
checksum = "ea091f6cac2595aa38993f04f4ee692ed43757035c36e67c180b6828356385b1"
        3901  +
checksum = "22b2d775fb28f245817589471dd49c5edf64237f4a19d10ce9a92ff4651a27f4"
 3898   3902   
dependencies = [
 3899   3903   
 "sdd",
 3900   3904   
]
 3901   3905   
 3902   3906   
[[package]]
 3903   3907   
name = "schannel"
 3904   3908   
version = "0.1.27"
 3905   3909   
source = "registry+https://github.com/rust-lang/crates.io-index"
 3906   3910   
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
 3907   3911   
dependencies = [
@@ -4062,4066 +4124,4128 @@
 4082   4086   
]
 4083   4087   
 4084   4088   
[[package]]
 4085   4089   
name = "shlex"
 4086   4090   
version = "1.3.0"
 4087   4091   
source = "registry+https://github.com/rust-lang/crates.io-index"
 4088   4092   
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 4089   4093   
 4090   4094   
[[package]]
 4091   4095   
name = "signal-hook-registry"
 4092         -
version = "1.4.2"
        4096  +
version = "1.4.5"
 4093   4097   
source = "registry+https://github.com/rust-lang/crates.io-index"
 4094         -
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
        4098  +
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
 4095   4099   
dependencies = [
 4096   4100   
 "libc",
 4097   4101   
]
 4098   4102   
 4099   4103   
[[package]]
 4100   4104   
name = "signature"
 4101   4105   
version = "1.6.4"
 4102   4106   
source = "registry+https://github.com/rust-lang/crates.io-index"
 4103   4107   
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
 4104   4108   
dependencies = [

tmp-codegen-diff/aws-sdk/sdk/aws-config/Cargo.toml

@@ -106,106 +157,157 @@
  126    126   
version = "1.2.5"
  127    127   
  128    128   
[dev-dependencies.aws-smithy-runtime]
  129    129   
path = "../aws-smithy-runtime"
  130    130   
features = ["client", "test-util"]
  131    131   
version = "1.8.2"
  132    132   
  133    133   
[dev-dependencies.aws-smithy-http-client]
  134    134   
path = "../aws-smithy-http-client"
  135    135   
features = ["default-client", "test-util"]
  136         -
version = "1.0.1"
         136  +
version = "1.0.2"
  137    137   
  138    138   
[dev-dependencies.aws-smithy-runtime-api]
  139    139   
path = "../aws-smithy-runtime-api"
  140    140   
features = ["test-util"]
  141    141   
version = "1.7.4"
  142    142   
  143    143   
[dev-dependencies.futures-util]
  144    144   
version = "0.3.29"
  145    145   
default-features = false
  146    146   

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/Cargo.toml

@@ -1,1 +73,73 @@
   16     16   
   17     17   
[[example]]
   18     18   
name = "custom-dns"
   19     19   
required-features = ["rustls-ring"]
   20     20   
doc-scrape-examples = true
   21     21   
   22     22   
[package]
   23     23   
name = "aws-smithy-http-client"
   24     24   
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
   25     25   
description = "HTTP client abstractions for generated smithy clients"
   26         -
version = "1.0.1"
          26  +
version = "1.0.2"
   27     27   
license = "Apache-2.0"
   28     28   
edition = "2021"
   29     29   
repository = "https://github.com/smithy-lang/smithy-rs"
   30     30   
[package.metadata.smithy-rs-release-tooling]
   31     31   
stable = true
   32     32   
[package.metadata.docs.rs]
   33     33   
all-features = false
   34     34   
features = ["default-client ", "wire-mock", "test-util", "rustls-ring", "rustls-aws-lc"]
   35     35   
targets = ["x86_64-unknown-linux-gnu"]
   36     36   
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
   37     37   
rustdoc-args = ["--cfg", "docsrs"]
   38     38   
   39     39   
[features]
   40     40   
hyper-014 = ["aws-smithy-runtime-api/http-02x", "aws-smithy-types/http-body-0-4-x", "dep:http-02x", "dep:http-body-04x", "dep:hyper-0-14"]
   41     41   
default-client = ["aws-smithy-runtime-api/http-1x", "aws-smithy-types/http-body-1-x", "dep:hyper", "dep:hyper-util", "hyper-util?/client-legacy", "dep:http-1x", "dep:tower", "dep:rustls-pki-types", "dep:rustls-native-certs"]
   42     42   
wire-mock = ["test-util", "default-client", "hyper-util?/server", "hyper-util?/server-auto", "hyper-util?/service", "hyper-util?/server-graceful", "tokio/macros", "dep:http-body-util"]
   43         -
test-util = ["dep:aws-smithy-protocol-test", "dep:serde", "dep:serde_json", "dep:indexmap", "dep:bytes", "dep:http-1x", "aws-smithy-runtime-api/http-1x", "dep:http-body-1x", "aws-smithy-types/http-body-1-x"]
          43  +
test-util = ["dep:aws-smithy-protocol-test", "dep:serde", "dep:serde_json", "dep:indexmap", "dep:bytes", "dep:http-1x", "aws-smithy-runtime-api/http-1x", "dep:http-body-1x", "aws-smithy-types/http-body-1-x", "tokio/rt"]
   44     44   
legacy-test-util = ["test-util", "dep:http-02x", "aws-smithy-runtime-api/http-02x", "aws-smithy-types/http-body-0-4-x"]
   45     45   
legacy-rustls-ring = ["dep:legacy-hyper-rustls", "dep:legacy-rustls", "hyper-014"]
   46     46   
rustls-ring = ["dep:rustls", "rustls?/ring", "dep:hyper-rustls", "default-client"]
   47     47   
rustls-aws-lc = ["dep:rustls", "rustls?/aws_lc_rs", "dep:hyper-rustls", "default-client"]
   48     48   
rustls-aws-lc-fips = ["dep:rustls", "rustls?/fips", "dep:hyper-rustls", "default-client"]
   49     49   
s2n-tls = ["dep:s2n-tls", "dep:s2n-tls-hyper", "default-client"]
   50     50   
   51     51   
[dependencies]
   52     52   
pin-project-lite = "0.2.14"
   53     53   
tracing = "0.1.40"

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-mocks-experimental/Cargo.toml

@@ -1,1 +29,29 @@
    1      1   
# Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT.
    2      2   
[[example]]
    3      3   
name = "s3-getobject-mocks"
    4      4   
doc-scrape-examples = true
    5      5   
    6      6   
[package]
    7      7   
name = "aws-smithy-mocks-experimental"
    8         -
version = "0.2.3"
           8  +
version = "0.2.4"
    9      9   
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
   10     10   
description = "Experimental testing utilities for smithy-rs generated clients"
   11     11   
edition = "2021"
   12     12   
license = "Apache-2.0"
   13     13   
repository = "https://github.com/smithy-lang/smithy-rs"
   14     14   
[package.metadata.docs.rs]
   15     15   
all-features = true
   16     16   
targets = ["x86_64-unknown-linux-gnu"]
   17     17   
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
   18     18   
rustdoc-args = ["--cfg", "docsrs"]

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-mocks-experimental/README.md

@@ -1,0 +42,8 @@
    1         -
# aws-smithy-mocks
           1  +
# aws-smithy-mocks-experimental
    2      2   
    3         -
This package allows testing clients generated by smithy-rs (including all packages of the AWS Rust SDK) by using interceptors to return stub responses. This approach is quite useful for testing both happy-path and simple error scenarios and avoids the need for mocking the entire client or using traits.
    4         -
    5         -
As an example, consider this simple usage with S3:
    6         -
    7         -
```rust
    8         -
#[tokio::test]
    9         -
async fn test_s3() {
   10         -
     let s3_real_object = mock!(Client::get_object).then_output(|| {
   11         -
         GetObjectOutput::builder()
   12         -
             .body(ByteStream::from_static(b"test-test-test"))
   13         -
             .build()
   14         -
     });
   15         -
    let s3 = mock_client!(aws_sdk_s3, [&s3_real_object]);
   16         -
    let data = s3
   17         -
        .get_object()
   18         -
        .bucket("test-bucket")
   19         -
        .key("correct-key")
   20         -
        .send()
   21         -
        .await
   22         -
        .expect("success response")
   23         -
        .body
   24         -
        .collect()
   25         -
        .await
   26         -
        .expect("successful read")
   27         -
        .to_vec();
   28         -
    assert_eq!(data, b"test-test-test");
   29         -
    assert_eq!(s3_real_object.num_calls(), 1);
   30         -
}
   31         -
```
   32         -
   33         -
You can find more examples in the `tests` folder of this crate.
   34         -
   35         -
## Shortcomings of this approach
   36         -
This approach is not well suited for testing precise error handling, especially when considering retries or interactions with HTTP responses—This approach hijacks the request response flow entirely and is not a faithful model in these cases.
   37         -
   38         -
If you need to test behavior around retries or connection management, you should use HTTP-connection based mocking instead.
           3  +
This crate was an experimental playground for mocking Smithy generated clients. It is now deprecated and
           4  +
has been replaced by [`aws-smithy-mocks`](https://crates.io/crates/aws-smithy-mocks). Please migrate to the newer crate.
   39      5   
   40      6   
<!-- anchor_start:footer -->
   41      7   
This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/smithy-lang/smithy-rs) code generator.
   42      8   
<!-- anchor_end:footer -->

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-mocks-experimental/src/lib.rs

@@ -1,1 +219,244 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6         -
//! This crate allows mocking of smithy clients.
           6  +
//! This crate has been deprecated. Please migrate to the `aws-smithy-mocks` crate.
    7      7   
    8      8   
/* Automatically managed default lints */
    9      9   
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
   10     10   
/* End of automatically managed default lints */
          11  +
#![allow(deprecated)]
          12  +
   11     13   
use std::collections::VecDeque;
   12     14   
use std::fmt::{Debug, Formatter};
   13     15   
use std::future::Future;
   14     16   
use std::marker::PhantomData;
   15     17   
use std::sync::atomic::{AtomicUsize, Ordering};
   16     18   
use std::sync::{Arc, Mutex};
   17     19   
   18     20   
use aws_smithy_runtime_api::box_error::BoxError;
   19     21   
use aws_smithy_runtime_api::client::interceptors::context::{
   20     22   
    BeforeDeserializationInterceptorContextMut, BeforeSerializationInterceptorContextMut, Error,
   21     23   
    FinalizerInterceptorContextMut, Input, Output,
   22     24   
};
   23     25   
use aws_smithy_runtime_api::client::interceptors::Intercept;
   24     26   
use aws_smithy_runtime_api::client::orchestrator::{HttpResponse, OrchestratorError};
   25     27   
use aws_smithy_runtime_api::client::result::SdkError;
   26     28   
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
   27     29   
use aws_smithy_runtime_api::http::{Response, StatusCode};
   28     30   
use aws_smithy_types::body::SdkBody;
   29     31   
use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace};
   30     32   
   31     33   
// why do we need a macro for this?
   32     34   
// We want customers to be able to provide an ergonomic way to say the method they're looking for,
   33     35   
// `Client::list_buckets`, e.g. But there isn't enough information on that type to recover everything.
   34     36   
// This macro commits a small amount of crimes to recover that type information so we can construct
   35     37   
// a rule that can intercept these operations.
   36     38   
   37     39   
/// `mock!` macro that produces a [`RuleBuilder`] from a client invocation
   38     40   
///
   39     41   
/// See the `examples` folder of this crate for fully worked examples.
   40     42   
///
   41     43   
/// # Examples
   42     44   
/// **Mock and return a success response**:
   43     45   
/// ```rust,ignore
   44     46   
/// use aws_sdk_s3::operation::get_object::GetObjectOutput;
   45     47   
/// use aws_sdk_s3::Client;
   46     48   
/// use aws_smithy_types::byte_stream::ByteStream;
   47     49   
/// use aws_smithy_mocks_experimental::mock;
   48     50   
/// let get_object_happy_path = mock!(Client::get_object)
   49     51   
///   .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
   50     52   
///   .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build());
   51     53   
/// ```
   52     54   
///
   53     55   
/// **Mock and return an error**:
   54     56   
/// ```rust,ignore
   55     57   
/// use aws_sdk_s3::operation::get_object::GetObjectError;
   56     58   
/// use aws_sdk_s3::types::error::NoSuchKey;
   57     59   
/// use aws_sdk_s3::Client;
   58     60   
/// use aws_smithy_mocks_experimental::mock;
   59     61   
/// let get_object_error_path = mock!(Client::get_object)
   60     62   
///   .then_error(||GetObjectError::NoSuchKey(NoSuchKey::builder().build()));
   61     63   
/// ```
   62     64   
#[macro_export]
          65  +
#[deprecated(
          66  +
    since = "0.2.4",
          67  +
    note = "The `aws-smithy-mocks-experimental` crate is now deprecated and is replaced by the `aws-smithy-mocks` crate. Please migrate to the non-experimental crate."
          68  +
)]
   63     69   
macro_rules! mock {
   64     70   
    ($operation: expr) => {
   65     71   
        #[allow(unreachable_code)]
   66     72   
        {
   67     73   
            $crate::RuleBuilder::new(
   68     74   
                // We don't actually want to run this code, so we put it in a closure. The closure
   69     75   
                // has the types we want which makes this whole thing type-safe (and the IDE can even
   70     76   
                // figure out the right input/output types in inference!)
   71     77   
                // The code generated here is:
   72     78   
                // `Client::list_buckets(todo!())`
   73     79   
                || $operation(todo!()).as_input().clone().build().unwrap(),
   74     80   
                || $operation(todo!()).send(),
   75     81   
            )
   76     82   
        }
   77     83   
    };
   78     84   
}
   79     85   
   80     86   
// This could be obviated by a reasonable trait, since you can express it with SdkConfig if clients implement From<&SdkConfig>.
   81     87   
   82     88   
/// `mock_client!` macro produces a Client configured with a number of Rules and appropriate test default configuration.
   83     89   
///
   84     90   
/// # Examples
   85     91   
/// **Create a client that uses a mock failure and then a success**:
   86     92   
/// ```rust,ignore
   87     93   
/// use aws_sdk_s3::operation::get_object::{GetObjectOutput, GetObjectError};
   88     94   
/// use aws_sdk_s3::types::error::NoSuchKey;
   89     95   
/// use aws_sdk_s3::Client;
   90     96   
/// use aws_smithy_types::byte_stream::ByteStream;
   91     97   
/// use aws_smithy_mocks_experimental::{mock_client, mock, RuleMode};
   92     98   
/// let get_object_happy_path = mock!(Client::get_object)
   93     99   
///   .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
   94    100   
///   .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build());
   95    101   
/// let get_object_error_path = mock!(Client::get_object)
   96    102   
///   .then_error(||GetObjectError::NoSuchKey(NoSuchKey::builder().build()));
   97    103   
/// let client = mock_client!(aws_sdk_s3, RuleMode::Sequential, &[&get_object_error_path, &get_object_happy_path]);
   98    104   
/// ```
   99    105   
///
  100    106   
/// **Create a client but customize a specific setting**:
  101    107   
/// ```rust,ignore
  102    108   
/// use aws_sdk_s3::operation::get_object::GetObjectOutput;
  103    109   
/// use aws_sdk_s3::Client;
  104    110   
/// use aws_smithy_types::byte_stream::ByteStream;
  105    111   
/// use aws_smithy_mocks_experimental::{mock_client, mock, RuleMode};
  106    112   
/// let get_object_happy_path = mock!(Client::get_object)
  107    113   
///   .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
  108    114   
///   .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build());
  109    115   
/// let client = mock_client!(
  110    116   
///     aws_sdk_s3,
  111    117   
///     RuleMode::Sequential,
  112    118   
///     &[&get_object_happy_path],
  113    119   
///     // Perhaps you need to force path style
  114    120   
///     |client_builder|client_builder.force_path_style(true)
  115    121   
/// );
  116    122   
/// ```
  117    123   
///
  118    124   
#[macro_export]
         125  +
#[deprecated(
         126  +
    since = "0.2.4",
         127  +
    note = "The `aws-smithy-mocks-experimental` crate is now deprecated and is replaced by the `aws-smithy-mocks` crate. Please migrate to the non-experimental crate."
         128  +
)]
  119    129   
macro_rules! mock_client {
  120    130   
    ($aws_crate: ident, $rules: expr) => {
  121    131   
        $crate::mock_client!($aws_crate, $crate::RuleMode::Sequential, $rules)
  122    132   
    };
  123    133   
    ($aws_crate: ident, $rule_mode: expr, $rules: expr) => {{
  124    134   
        $crate::mock_client!($aws_crate, $rule_mode, $rules, |conf| conf)
  125    135   
    }};
  126    136   
    ($aws_crate: ident, $rule_mode: expr, $rules: expr, $additional_configuration: expr) => {{
  127    137   
        let mut mock_response_interceptor =
  128    138   
            $crate::MockResponseInterceptor::new().rule_mode($rule_mode);
  129    139   
        for rule in $rules {
  130    140   
            mock_response_interceptor = mock_response_interceptor.with_rule(rule)
  131    141   
        }
  132    142   
        // allow callers to avoid explicitly specifying the type
  133    143   
        fn coerce<T: Fn($aws_crate::config::Builder) -> $aws_crate::config::Builder>(f: T) -> T {
  134    144   
            f
  135    145   
        }
  136    146   
        $aws_crate::client::Client::from_conf(
  137    147   
            coerce($additional_configuration)(
  138    148   
                $aws_crate::config::Config::builder()
  139    149   
                    .with_test_defaults()
  140    150   
                    .region($aws_crate::config::Region::from_static("us-east-1"))
  141    151   
                    .interceptor(mock_response_interceptor),
  142    152   
            )
  143    153   
            .build(),
  144    154   
        )
  145    155   
    }};
  146    156   
}
  147    157   
  148    158   
type MatchFn = Arc<dyn Fn(&Input) -> bool + Send + Sync>;
  149    159   
type OutputFn = Arc<dyn Fn() -> Result<Output, OrchestratorError<Error>> + Send + Sync>;
  150    160   
  151    161   
impl Debug for MockResponseInterceptor {
  152    162   
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
  153    163   
        write!(f, "{} rules", self.rules.lock().unwrap().len())
  154    164   
    }
  155    165   
}
  156    166   
  157    167   
#[derive(Clone)]
  158    168   
enum MockOutput {
  159    169   
    HttpResponse(Arc<dyn Fn() -> Result<HttpResponse, BoxError> + Send + Sync>),
  160    170   
    ModeledResponse(OutputFn),
  161    171   
}
  162    172   
  163    173   
/// RuleMode describes how rules will be interpreted.
  164    174   
/// - In RuleMode::MatchAny, the first matching rule will be applied, and the rules will remain unchanged.
  165    175   
/// - In RuleMode::Sequential, the first matching rule will be applied, and that rule will be removed from the list of rules.
  166         -
#[derive()]
         176  +
#[deprecated(
         177  +
    since = "0.2.4",
         178  +
    note = "The `aws-smithy-mocks-experimental` crate is now deprecated and is replaced by the `aws-smithy-mocks` crate. Please migrate to the non-experimental crate."
         179  +
)]
  167    180   
pub enum RuleMode {
  168    181   
    MatchAny,
  169    182   
    Sequential,
  170    183   
}
  171    184   
  172    185   
/// Interceptor which produces mock responses based on a list of rules
         186  +
#[deprecated(
         187  +
    since = "0.2.4",
         188  +
    note = "The `aws-smithy-mocks-experimental` crate is now deprecated and is replaced by the `aws-smithy-mocks` crate. Please migrate to the non-experimental crate."
         189  +
)]
  173    190   
pub struct MockResponseInterceptor {
  174    191   
    rules: Arc<Mutex<VecDeque<Rule>>>,
  175    192   
    rule_mode: RuleMode,
  176    193   
    must_match: bool,
  177    194   
}
  178    195   
  179    196   
impl Default for MockResponseInterceptor {
  180    197   
    fn default() -> Self {
  181    198   
        Self::new()
  182    199   
    }
  183    200   
}
  184    201   
         202  +
#[deprecated(
         203  +
    since = "0.2.4",
         204  +
    note = "The `aws-smithy-mocks-experimental` crate is now deprecated and is replaced by the `aws-smithy-mocks` crate. Please migrate to the non-experimental crate."
         205  +
)]
  185    206   
pub struct RuleBuilder<I, O, E> {
  186    207   
    _ty: PhantomData<(I, O, E)>,
  187    208   
    input_filter: MatchFn,
  188    209   
}
  189    210   
         211  +
#[deprecated(
         212  +
    since = "0.2.4",
         213  +
    note = "The `aws-smithy-mocks-experimental` crate is now deprecated and is replaced by the `aws-smithy-mocks` crate. Please migrate to the non-experimental crate."
         214  +
)]
  190    215   
impl<I, O, E> RuleBuilder<I, O, E>
  191    216   
where
  192    217   
    I: Send + Sync + Debug + 'static,
  193    218   
    O: Send + Sync + Debug + 'static,
  194    219   
    E: Send + Sync + Debug + std::error::Error + 'static,
  195    220   
{
  196    221   
    /// Creates a new [`RuleBuilder`]. This is normally constructed with the [`mock!`] macro
  197    222   
    pub fn new<F, R>(_input_hint: impl Fn() -> I, _output_hint: impl Fn() -> F) -> Self
  198    223   
    where
  199    224   
        F: Future<Output = Result<O, SdkError<E, R>>>,
@@ -224,249 +320,353 @@
  244    269   
    pub fn then_error(self, output: impl Fn() -> E + Send + Sync + 'static) -> Rule {
  245    270   
        Rule::new(
  246    271   
            self.input_filter,
  247    272   
            MockOutput::ModeledResponse(Arc::new(move || {
  248    273   
                Err(OrchestratorError::operation(Error::erase(output())))
  249    274   
            })),
  250    275   
        )
  251    276   
    }
  252    277   
}
  253    278   
         279  +
#[deprecated(
         280  +
    since = "0.2.4",
         281  +
    note = "The `aws-smithy-mocks-experimental` crate is now deprecated and is replaced by the `aws-smithy-mocks` crate. Please migrate to the non-experimental crate."
         282  +
)]
  254    283   
#[derive(Clone)]
  255    284   
pub struct Rule {
  256    285   
    matcher: MatchFn,
  257    286   
    output: MockOutput,
  258    287   
    used_count: Arc<AtomicUsize>,
  259    288   
}
  260    289   
  261    290   
impl Debug for Rule {
  262    291   
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
  263    292   
        write!(f, "Rule")
  264    293   
    }
  265    294   
}
  266    295   
  267    296   
impl Rule {
  268    297   
    fn new(matcher: MatchFn, output: MockOutput) -> Self {
  269    298   
        Self {
  270    299   
            matcher,
  271    300   
            output,
  272    301   
            used_count: Default::default(),
  273    302   
        }
  274    303   
    }
  275    304   
    fn record_usage(&self) {
  276    305   
        self.used_count.fetch_add(1, Ordering::Relaxed);
  277    306   
    }
  278    307   
  279    308   
    /// Returns the number of times this rule has been hit.
  280    309   
    pub fn num_calls(&self) -> usize {
  281    310   
        self.used_count.load(Ordering::Relaxed)
  282    311   
    }
  283    312   
}
  284    313   
  285    314   
#[derive(Debug)]
  286    315   
struct ActiveRule(Rule);
  287    316   
impl Storable for ActiveRule {
  288    317   
    type Storer = StoreReplace<ActiveRule>;
  289    318   
}
  290    319   
         320  +
#[deprecated(
         321  +
    since = "0.2.4",
         322  +
    note = "The `aws-smithy-mocks-experimental` crate is now deprecated and is replaced by the `aws-smithy-mocks` crate. Please migrate to the non-experimental crate."
         323  +
)]
  291    324   
impl MockResponseInterceptor {
  292    325   
    pub fn new() -> Self {
  293    326   
        Self {
  294    327   
            rules: Default::default(),
  295    328   
            rule_mode: RuleMode::MatchAny,
  296    329   
            must_match: true,
  297    330   
        }
  298    331   
    }
  299    332   
    /// Add a rule to the Interceptor
  300    333   
    ///

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-mocks-experimental/tests/macros.rs

@@ -24,24 +57,59 @@
   44     44   
        }
   45     45   
   46     46   
        pub(crate) struct Region {}
   47     47   
        impl Region {
   48     48   
            pub fn from_static(_region: &'static str) -> Self {
   49     49   
                Self {}
   50     50   
            }
   51     51   
        }
   52     52   
    }
   53     53   
}
          54  +
          55  +
#[allow(deprecated)]
   54     56   
#[test]
   55     57   
fn mock_client() {
   56     58   
    aws_smithy_mocks_experimental::mock_client!(fake_crate, &[]);
   57     59   
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-mocks/Cargo.toml

@@ -0,1 +0,44 @@
           1  +
# Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT.
           2  +
[package]
           3  +
name = "aws-smithy-mocks"
           4  +
version = "0.1.0"
           5  +
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
           6  +
description = "Testing utilities for smithy-rs generated clients"
           7  +
edition = "2021"
           8  +
license = "Apache-2.0"
           9  +
repository = "https://github.com/smithy-lang/smithy-rs"
          10  +
[package.metadata.docs.rs]
          11  +
all-features = true
          12  +
targets = ["x86_64-unknown-linux-gnu"]
          13  +
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
          14  +
rustdoc-args = ["--cfg", "docsrs"]
          15  +
          16  +
[dependencies]
          17  +
http = "1.3"
          18  +
          19  +
[dependencies.aws-smithy-types]
          20  +
path = "../aws-smithy-types"
          21  +
version = "1.3.0"
          22  +
          23  +
[dependencies.aws-smithy-runtime-api]
          24  +
path = "../aws-smithy-runtime-api"
          25  +
features = ["client", "http-1x"]
          26  +
version = "1.7.4"
          27  +
          28  +
[dependencies.aws-smithy-http-client]
          29  +
path = "../aws-smithy-http-client"
          30  +
features = ["test-util"]
          31  +
version = "1.0.2"
          32  +
[dev-dependencies.tokio]
          33  +
version = "1"
          34  +
features = ["full"]
          35  +
          36  +
[dev-dependencies.aws-smithy-async]
          37  +
path = "../aws-smithy-async"
          38  +
features = ["rt-tokio"]
          39  +
version = "1.2.5"
          40  +
          41  +
[dev-dependencies.aws-smithy-runtime]
          42  +
path = "../aws-smithy-runtime"
          43  +
features = ["client"]
          44  +
version = "1.8.2"

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-mocks/LICENSE

@@ -0,1 +0,175 @@
           1  +
           2  +
                                 Apache License
           3  +
                           Version 2.0, January 2004
           4  +
                        http://www.apache.org/licenses/
           5  +
           6  +
   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
           7  +
           8  +
   1. Definitions.
           9  +
          10  +
      "License" shall mean the terms and conditions for use, reproduction,
          11  +
      and distribution as defined by Sections 1 through 9 of this document.
          12  +
          13  +
      "Licensor" shall mean the copyright owner or entity authorized by
          14  +
      the copyright owner that is granting the License.
          15  +
          16  +
      "Legal Entity" shall mean the union of the acting entity and all
          17  +
      other entities that control, are controlled by, or are under common
          18  +
      control with that entity. For the purposes of this definition,
          19  +
      "control" means (i) the power, direct or indirect, to cause the
          20  +
      direction or management of such entity, whether by contract or
          21  +
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
          22  +
      outstanding shares, or (iii) beneficial ownership of such entity.
          23  +
          24  +
      "You" (or "Your") shall mean an individual or Legal Entity
          25  +
      exercising permissions granted by this License.
          26  +
          27  +
      "Source" form shall mean the preferred form for making modifications,
          28  +
      including but not limited to software source code, documentation
          29  +
      source, and configuration files.
          30  +
          31  +
      "Object" form shall mean any form resulting from mechanical
          32  +
      transformation or translation of a Source form, including but
          33  +
      not limited to compiled object code, generated documentation,
          34  +
      and conversions to other media types.
          35  +
          36  +
      "Work" shall mean the work of authorship, whether in Source or
          37  +
      Object form, made available under the License, as indicated by a
          38  +
      copyright notice that is included in or attached to the work
          39  +
      (an example is provided in the Appendix below).
          40  +
          41  +
      "Derivative Works" shall mean any work, whether in Source or Object
          42  +
      form, that is based on (or derived from) the Work and for which the
          43  +
      editorial revisions, annotations, elaborations, or other modifications
          44  +
      represent, as a whole, an original work of authorship. For the purposes
          45  +
      of this License, Derivative Works shall not include works that remain
          46  +
      separable from, or merely link (or bind by name) to the interfaces of,
          47  +
      the Work and Derivative Works thereof.
          48  +
          49  +
      "Contribution" shall mean any work of authorship, including
          50  +
      the original version of the Work and any modifications or additions
          51  +
      to that Work or Derivative Works thereof, that is intentionally
          52  +
      submitted to Licensor for inclusion in the Work by the copyright owner
          53  +
      or by an individual or Legal Entity authorized to submit on behalf of
          54  +
      the copyright owner. For the purposes of this definition, "submitted"
          55  +
      means any form of electronic, verbal, or written communication sent
          56  +
      to the Licensor or its representatives, including but not limited to
          57  +
      communication on electronic mailing lists, source code control systems,
          58  +
      and issue tracking systems that are managed by, or on behalf of, the
          59  +
      Licensor for the purpose of discussing and improving the Work, but
          60  +
      excluding communication that is conspicuously marked or otherwise
          61  +
      designated in writing by the copyright owner as "Not a Contribution."
          62  +
          63  +
      "Contributor" shall mean Licensor and any individual or Legal Entity
          64  +
      on behalf of whom a Contribution has been received by Licensor and
          65  +
      subsequently incorporated within the Work.
          66  +
          67  +
   2. Grant of Copyright License. Subject to the terms and conditions of
          68  +
      this License, each Contributor hereby grants to You a perpetual,
          69  +
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
          70  +
      copyright license to reproduce, prepare Derivative Works of,
          71  +
      publicly display, publicly perform, sublicense, and distribute the
          72  +
      Work and such Derivative Works in Source or Object form.
          73  +
          74  +
   3. Grant of Patent License. Subject to the terms and conditions of
          75  +
      this License, each Contributor hereby grants to You a perpetual,
          76  +
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
          77  +
      (except as stated in this section) patent license to make, have made,
          78  +
      use, offer to sell, sell, import, and otherwise transfer the Work,
          79  +
      where such license applies only to those patent claims licensable
          80  +
      by such Contributor that are necessarily infringed by their
          81  +
      Contribution(s) alone or by combination of their Contribution(s)
          82  +
      with the Work to which such Contribution(s) was submitted. If You
          83  +
      institute patent litigation against any entity (including a
          84  +
      cross-claim or counterclaim in a lawsuit) alleging that the Work
          85  +
      or a Contribution incorporated within the Work constitutes direct
          86  +
      or contributory patent infringement, then any patent licenses
          87  +
      granted to You under this License for that Work shall terminate
          88  +
      as of the date such litigation is filed.
          89  +
          90  +
   4. Redistribution. You may reproduce and distribute copies of the
          91  +
      Work or Derivative Works thereof in any medium, with or without
          92  +
      modifications, and in Source or Object form, provided that You
          93  +
      meet the following conditions:
          94  +
          95  +
      (a) You must give any other recipients of the Work or
          96  +
          Derivative Works a copy of this License; and
          97  +
          98  +
      (b) You must cause any modified files to carry prominent notices
          99  +
          stating that You changed the files; and
         100  +
         101  +
      (c) You must retain, in the Source form of any Derivative Works
         102  +
          that You distribute, all copyright, patent, trademark, and
         103  +
          attribution notices from the Source form of the Work,
         104  +
          excluding those notices that do not pertain to any part of
         105  +
          the Derivative Works; and
         106  +
         107  +
      (d) If the Work includes a "NOTICE" text file as part of its
         108  +
          distribution, then any Derivative Works that You distribute must
         109  +
          include a readable copy of the attribution notices contained
         110  +
          within such NOTICE file, excluding those notices that do not
         111  +
          pertain to any part of the Derivative Works, in at least one
         112  +
          of the following places: within a NOTICE text file distributed
         113  +
          as part of the Derivative Works; within the Source form or
         114  +
          documentation, if provided along with the Derivative Works; or,
         115  +
          within a display generated by the Derivative Works, if and
         116  +
          wherever such third-party notices normally appear. The contents
         117  +
          of the NOTICE file are for informational purposes only and
         118  +
          do not modify the License. You may add Your own attribution
         119  +
          notices within Derivative Works that You distribute, alongside
         120  +
          or as an addendum to the NOTICE text from the Work, provided
         121  +
          that such additional attribution notices cannot be construed
         122  +
          as modifying the License.
         123  +
         124  +
      You may add Your own copyright statement to Your modifications and
         125  +
      may provide additional or different license terms and conditions
         126  +
      for use, reproduction, or distribution of Your modifications, or
         127  +
      for any such Derivative Works as a whole, provided Your use,
         128  +
      reproduction, and distribution of the Work otherwise complies with
         129  +
      the conditions stated in this License.
         130  +
         131  +
   5. Submission of Contributions. Unless You explicitly state otherwise,
         132  +
      any Contribution intentionally submitted for inclusion in the Work
         133  +
      by You to the Licensor shall be under the terms and conditions of
         134  +
      this License, without any additional terms or conditions.
         135  +
      Notwithstanding the above, nothing herein shall supersede or modify
         136  +
      the terms of any separate license agreement you may have executed
         137  +
      with Licensor regarding such Contributions.
         138  +
         139  +
   6. Trademarks. This License does not grant permission to use the trade
         140  +
      names, trademarks, service marks, or product names of the Licensor,
         141  +
      except as required for reasonable and customary use in describing the
         142  +
      origin of the Work and reproducing the content of the NOTICE file.
         143  +
         144  +
   7. Disclaimer of Warranty. Unless required by applicable law or
         145  +
      agreed to in writing, Licensor provides the Work (and each
         146  +
      Contributor provides its Contributions) on an "AS IS" BASIS,
         147  +
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
         148  +
      implied, including, without limitation, any warranties or conditions
         149  +
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
         150  +
      PARTICULAR PURPOSE. You are solely responsible for determining the
         151  +
      appropriateness of using or redistributing the Work and assume any
         152  +
      risks associated with Your exercise of permissions under this License.
         153  +
         154  +
   8. Limitation of Liability. In no event and under no legal theory,
         155  +
      whether in tort (including negligence), contract, or otherwise,
         156  +
      unless required by applicable law (such as deliberate and grossly
         157  +
      negligent acts) or agreed to in writing, shall any Contributor be
         158  +
      liable to You for damages, including any direct, indirect, special,
         159  +
      incidental, or consequential damages of any character arising as a
         160  +
      result of this License or out of the use or inability to use the
         161  +
      Work (including but not limited to damages for loss of goodwill,
         162  +
      work stoppage, computer failure or malfunction, or any and all
         163  +
      other commercial damages or losses), even if such Contributor
         164  +
      has been advised of the possibility of such damages.
         165  +
         166  +
   9. Accepting Warranty or Additional Liability. While redistributing
         167  +
      the Work or Derivative Works thereof, You may choose to offer,
         168  +
      and charge a fee for, acceptance of support, warranty, indemnity,
         169  +
      or other liability obligations and/or rights consistent with this
         170  +
      License. However, in accepting such obligations, You may act only
         171  +
      on Your own behalf and on Your sole responsibility, not on behalf
         172  +
      of any other Contributor, and only if You agree to indemnify,
         173  +
      defend, and hold each Contributor harmless for any liability
         174  +
      incurred by, or claims asserted against, such Contributor by reason
         175  +
      of your accepting any such warranty or additional liability.

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-mocks/README.md

@@ -0,1 +0,201 @@
           1  +
# aws-smithy-mocks
           2  +
           3  +
This package allows testing clients generated by smithy-rs (including all packages of the AWS Rust SDK) by using interceptors to return stub responses. This approach is quite useful for testing both happy-path and error scenarios and avoids the need for mocking the entire client or using traits.
           4  +
           5  +
## Basic Usage
           6  +
           7  +
```rust
           8  +
#[tokio::test]
           9  +
async fn test_s3() {
          10  +
    // Create a rule that returns a successful response
          11  +
    let s3_rule = mock!(Client::get_object).then_output(|| {
          12  +
        GetObjectOutput::builder()
          13  +
            .body(ByteStream::from_static(b"test-content"))
          14  +
            .build()
          15  +
    });
          16  +
          17  +
    // Create a mocked client with the rule
          18  +
    let s3 = mock_client!(aws_sdk_s3, [&s3_rule]);
          19  +
          20  +
    // Use the client as you would normally
          21  +
    let result = s3
          22  +
        .get_object()
          23  +
        .bucket("test-bucket")
          24  +
        .key("test-key")
          25  +
        .send()
          26  +
        .await
          27  +
        .expect("success response");
          28  +
          29  +
    // Verify the response
          30  +
    let data = result.body.collect().await.expect("successful read").to_vec();
          31  +
    assert_eq!(data, b"test-content");
          32  +
          33  +
    // Verify the rule was used
          34  +
    assert_eq!(s3_rule.num_calls(), 1);
          35  +
}
          36  +
```
          37  +
          38  +
## Key Features
          39  +
          40  +
### Request Matching
          41  +
          42  +
You can match requests based on their properties:
          43  +
          44  +
```rust
          45  +
let rule = mock!(Client::get_object)
          46  +
    .match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
          47  +
    .then_output(|| GetObjectOutput::builder().build());
          48  +
```
          49  +
          50  +
### Different Response Types
          51  +
          52  +
Return different types of responses:
          53  +
          54  +
```rust
          55  +
// Return a modeled output
          56  +
let success_rule = mock!(Client::get_object)
          57  +
    .then_output(|| GetObjectOutput::builder().build());
          58  +
          59  +
// Return a modeled error
          60  +
let error_rule = mock!(Client::get_object)
          61  +
    .then_error(|| GetObjectError::NoSuchKey(NoSuchKey::builder().build()));
          62  +
          63  +
// Return an HTTP response
          64  +
let http_rule = mock!(Client::get_object)
          65  +
    .then_http_response(|| HttpResponse::new(
          66  +
        StatusCode::try_from(503).unwrap(),
          67  +
        SdkBody::from("service unavailable")
          68  +
    ));
          69  +
```
          70  +
          71  +
### Testing Retry Behavior
          72  +
          73  +
Define sequences of responses for testing retry behavior:
          74  +
          75  +
```rust
          76  +
// Using the sequence builder API
          77  +
let retry_rule = mock!(Client::get_object)
          78  +
    .sequence()
          79  +
    .http_status(503, None)                          // First call returns 503
          80  +
    .http_status(503, None)                          // Second call returns 503
          81  +
    .output(|| GetObjectOutput::builder().build())   // Third call succeeds
          82  +
    .build();
          83  +
          84  +
// With repetition using times()
          85  +
let retry_rule = mock!(Client::get_object)
          86  +
    .sequence()
          87  +
    .http_status(503)
          88  +
    .times(2)                                        // First two calls return 503
          89  +
    .output(|| GetObjectOutput::builder().build())   // Third call succeeds
          90  +
    .build();
          91  +
```
          92  +
          93  +
### Rule Modes
          94  +
          95  +
Control how rules are matched and applied:
          96  +
          97  +
```rust
          98  +
// Sequential mode: Rules are tried in order, and when a rule is exhausted, the next rule is used
          99  +
let client = mock_client!(aws_sdk_s3, RuleMode::Sequential, [&rule1, &rule2]);
         100  +
         101  +
// MatchAny mode: The first matching rule is used, regardless of order
         102  +
let client = mock_client!(aws_sdk_s3, RuleMode::MatchAny, [&rule1, &rule2]);
         103  +
```
         104  +
         105  +
## Advanced Examples
         106  +
         107  +
### Testing Retry Behavior
         108  +
         109  +
```rust
         110  +
#[tokio::test]
         111  +
async fn test_retry_behavior() {
         112  +
    // Create a rule that returns 503 twice, then succeeds
         113  +
    let retry_rule = mock!(Client::get_object)
         114  +
        .sequence()
         115  +
        .http_status(503, None)
         116  +
        .times(2)
         117  +
        .output(|| GetObjectOutput::builder()
         118  +
            .body(ByteStream::from_static(b"success"))
         119  +
            .build())
         120  +
        .build();
         121  +
         122  +
    // Create a mocked client with the rule
         123  +
    let s3 = mock_client!(
         124  +
        aws_sdk_s3,
         125  +
        RuleMode::Sequential,
         126  +
        [&retry_rule],
         127  +
        |client_builder| {
         128  +
            client_builder.retry_config(RetryConfig::standard().with_max_attempts(3))
         129  +
        }
         130  +
    );
         131  +
         132  +
    // This should succeed after two retries
         133  +
    let result = s3
         134  +
        .get_object()
         135  +
        .bucket("test-bucket")
         136  +
        .key("test-key")
         137  +
        .send()
         138  +
        .await
         139  +
        .expect("success after retries");
         140  +
         141  +
    // Verify the response
         142  +
    let data = result.body.collect().await.expect("successful read").to_vec();
         143  +
    assert_eq!(data, b"success");
         144  +
         145  +
    // Verify all responses were used
         146  +
    assert_eq!(retry_rule.num_calls(), 3);
         147  +
}
         148  +
```
         149  +
         150  +
### Testing Different Responses Based on Request Parameters
         151  +
         152  +
```rust
         153  +
#[tokio::test]
         154  +
async fn test_different_responses() {
         155  +
    // Create rules for different request parameters
         156  +
    let exists_rule = mock!(Client::get_object)
         157  +
        .match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("exists"))
         158  +
        .sequence()
         159  +
        .output(|| GetObjectOutput::builder()
         160  +
            .body(ByteStream::from_static(b"found"))
         161  +
            .build())
         162  +
        .build();
         163  +
         164  +
    let not_exists_rule = mock!(Client::get_object)
         165  +
        .match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("not-exists"))
         166  +
        .sequence()
         167  +
        .error(|| GetObjectError::NoSuchKey(NoSuchKey::builder().build()))
         168  +
        .build();
         169  +
         170  +
    // Create a mocked client with the rules in MatchAny mode
         171  +
    let s3 = mock_client!(aws_sdk_s3, RuleMode::MatchAny, [&exists_rule, &not_exists_rule]);
         172  +
         173  +
    // Test the "exists" case
         174  +
    let result1 = s3
         175  +
        .get_object()
         176  +
        .bucket("test-bucket")
         177  +
        .key("exists")
         178  +
        .send()
         179  +
        .await
         180  +
        .expect("object exists");
         181  +
         182  +
    let data = result1.body.collect().await.expect("successful read").to_vec();
         183  +
    assert_eq!(data, b"found");
         184  +
         185  +
    // Test the "not-exists" case
         186  +
    let result2 = s3
         187  +
        .get_object()
         188  +
        .bucket("test-bucket")
         189  +
        .key("not-exists")
         190  +
        .send()
         191  +
        .await;
         192  +
         193  +
    assert!(result2.is_err());
         194  +
    assert!(matches!(result2.unwrap_err().into_service_error(),
         195  +
                    GetObjectError::NoSuchKey(_)));
         196  +
}
         197  +
```
         198  +
         199  +
<!-- anchor_start:footer -->
         200  +
This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/smithy-lang/smithy-rs) code generator.
         201  +
<!-- anchor_end:footer -->

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-mocks/src/interceptor.rs

@@ -0,1 +0,698 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
use crate::{MockResponse, Rule, RuleMode};
           7  +
use aws_smithy_http_client::test_util::infallible_client_fn;
           8  +
use aws_smithy_runtime_api::box_error::BoxError;
           9  +
use aws_smithy_runtime_api::client::http::SharedHttpClient;
          10  +
use aws_smithy_runtime_api::client::interceptors::context::{
          11  +
    BeforeSerializationInterceptorContextMut, BeforeTransmitInterceptorContextMut, Error,
          12  +
    FinalizerInterceptorContextMut, Output,
          13  +
};
          14  +
use aws_smithy_runtime_api::client::interceptors::Intercept;
          15  +
use aws_smithy_runtime_api::client::orchestrator::{HttpResponse, OrchestratorError};
          16  +
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
          17  +
use aws_smithy_types::body::SdkBody;
          18  +
use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace};
          19  +
use std::collections::VecDeque;
          20  +
use std::fmt;
          21  +
use std::sync::{Arc, Mutex};
          22  +
          23  +
// Store active rule in config bag
          24  +
#[derive(Debug, Clone)]
          25  +
struct ActiveRule(Rule);
          26  +
          27  +
impl Storable for ActiveRule {
          28  +
    type Storer = StoreReplace<ActiveRule>;
          29  +
}
          30  +
          31  +
/// Interceptor which produces mock responses based on a list of rules
          32  +
pub struct MockResponseInterceptor {
          33  +
    rules: Arc<Mutex<VecDeque<Rule>>>,
          34  +
    rule_mode: RuleMode,
          35  +
    must_match: bool,
          36  +
    active_response: Arc<Mutex<Option<MockResponse<Output, Error>>>>,
          37  +
}
          38  +
          39  +
impl fmt::Debug for MockResponseInterceptor {
          40  +
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
          41  +
        write!(f, "{} rules", self.rules.lock().unwrap().len())
          42  +
    }
          43  +
}
          44  +
          45  +
impl Default for MockResponseInterceptor {
          46  +
    fn default() -> Self {
          47  +
        Self::new()
          48  +
    }
          49  +
}
          50  +
          51  +
impl MockResponseInterceptor {
          52  +
    /// Create a new [MockResponseInterceptor]
          53  +
    ///
          54  +
    /// This is normally created and registered on a client through the [`mock_client`] macro.
          55  +
    pub fn new() -> Self {
          56  +
        Self {
          57  +
            rules: Default::default(),
          58  +
            rule_mode: RuleMode::MatchAny,
          59  +
            must_match: true,
          60  +
            active_response: Default::default(),
          61  +
        }
          62  +
    }
          63  +
    /// Add a rule to the Interceptor
          64  +
    ///
          65  +
    /// Rules are matched in order—this rule will only apply if all previous rules do not match.
          66  +
    pub fn with_rule(self, rule: &Rule) -> Self {
          67  +
        self.rules.lock().unwrap().push_back(rule.clone());
          68  +
        self
          69  +
    }
          70  +
          71  +
    /// Set the RuleMode to use when evaluating rules.
          72  +
    ///
          73  +
    /// See `RuleMode` enum for modes and how they are applied.
          74  +
    pub fn rule_mode(mut self, rule_mode: RuleMode) -> Self {
          75  +
        self.rule_mode = rule_mode;
          76  +
        self
          77  +
    }
          78  +
          79  +
    /// Allow passthrough for unmatched requests.
          80  +
    ///
          81  +
    /// By default, if a request doesn't match any rule, the interceptor will panic.
          82  +
    /// This method allows unmatched requests to pass through.
          83  +
    pub fn allow_passthrough(mut self) -> Self {
          84  +
        self.must_match = false;
          85  +
        self
          86  +
    }
          87  +
}
          88  +
          89  +
impl Intercept for MockResponseInterceptor {
          90  +
    fn name(&self) -> &'static str {
          91  +
        "MockResponseInterceptor"
          92  +
    }
          93  +
          94  +
    fn modify_before_serialization(
          95  +
        &self,
          96  +
        context: &mut BeforeSerializationInterceptorContextMut<'_>,
          97  +
        _runtime_components: &RuntimeComponents,
          98  +
        cfg: &mut ConfigBag,
          99  +
    ) -> Result<(), BoxError> {
         100  +
        let mut rules = self.rules.lock().unwrap();
         101  +
        let input = context.inner().input().expect("input set");
         102  +
         103  +
        // Find a matching rule and get its response
         104  +
        let mut matching_rule = None;
         105  +
        let mut matching_response = None;
         106  +
         107  +
        match self.rule_mode {
         108  +
            RuleMode::Sequential => {
         109  +
                // Sequential mode requires rules match in-order
         110  +
                let i = 0;
         111  +
                while i < rules.len() && matching_response.is_none() {
         112  +
                    let rule = &rules[i];
         113  +
         114  +
                    // Check if the rule is already exhausted
         115  +
                    if rule.is_exhausted() {
         116  +
                        // Rule is exhausted, remove it and try the next one
         117  +
                        rules.remove(i);
         118  +
                        continue; // Don't increment i since we removed an element
         119  +
                    }
         120  +
         121  +
                    // Check if the rule matches
         122  +
                    if !(rule.matcher)(input) {
         123  +
                        // Rule doesn't match, this is an error in sequential mode
         124  +
                        panic!(
         125  +
                            "In order matching was enforced but rule did not match {:?}",
         126  +
                            input
         127  +
                        );
         128  +
                    }
         129  +
         130  +
                    // Rule matches and is not exhausted, get the response
         131  +
                    if let Some(response) = rule.next_response() {
         132  +
                        matching_rule = Some(rule.clone());
         133  +
                        matching_response = Some(response);
         134  +
                    } else {
         135  +
                        // Rule is exhausted, remove it and try the next one
         136  +
                        rules.remove(i);
         137  +
                        continue; // Don't increment i since we removed an element
         138  +
                    }
         139  +
         140  +
                    // We found a matching rule and got a response, so we're done
         141  +
                    break;
         142  +
                }
         143  +
            }
         144  +
            RuleMode::MatchAny => {
         145  +
                // Find any matching rule with a response
         146  +
                for rule in rules.iter() {
         147  +
                    // Skip exhausted rules
         148  +
                    if rule.is_exhausted() {
         149  +
                        continue;
         150  +
                    }
         151  +
         152  +
                    if (rule.matcher)(input) {
         153  +
                        if let Some(response) = rule.next_response() {
         154  +
                            matching_rule = Some(rule.clone());
         155  +
                            matching_response = Some(response);
         156  +
                            break;
         157  +
                        }
         158  +
                    }
         159  +
                }
         160  +
            }
         161  +
        };
         162  +
         163  +
        match (matching_rule, matching_response) {
         164  +
            (Some(rule), Some(response)) => {
         165  +
                // Store the rule in the config bag
         166  +
                cfg.interceptor_state().store_put(ActiveRule(rule));
         167  +
                // store the response on the interceptor (because going
         168  +
                // through interceptor context requires the type to impl Clone)
         169  +
                let mut active_resp = self.active_response.lock().unwrap();
         170  +
                let _ = std::mem::replace(&mut *active_resp, Some(response));
         171  +
            }
         172  +
            _ => {
         173  +
                // No matching rule or no response
         174  +
                if self.must_match {
         175  +
                    panic!(
         176  +
                        "must_match was enabled but no rules matched or all rules were exhausted for {:?}",
         177  +
                        input
         178  +
                    );
         179  +
                }
         180  +
            }
         181  +
        }
         182  +
         183  +
        Ok(())
         184  +
    }
         185  +
         186  +
    fn modify_before_transmit(
         187  +
        &self,
         188  +
        context: &mut BeforeTransmitInterceptorContextMut<'_>,
         189  +
        _runtime_components: &RuntimeComponents,
         190  +
        cfg: &mut ConfigBag,
         191  +
    ) -> Result<(), BoxError> {
         192  +
        let mut state = self.active_response.lock().unwrap();
         193  +
        let mut active_response = (*state).take();
         194  +
        if active_response.is_none() {
         195  +
            // in the case of retries we try to get the next response if it has been consumed
         196  +
            if let Some(active_rule) = cfg.load::<ActiveRule>() {
         197  +
                let next_resp = active_rule.0.next_response();
         198  +
                active_response = next_resp;
         199  +
            }
         200  +
        }
         201  +
         202  +
        if let Some(resp) = active_response {
         203  +
            match resp {
         204  +
                // place the http response into the extensions and let the HTTP client return it
         205  +
                MockResponse::Http(http_resp) => {
         206  +
                    context
         207  +
                        .request_mut()
         208  +
                        .add_extension(MockHttpResponse(Arc::new(http_resp)));
         209  +
                }
         210  +
                _ => {
         211  +
                    // put it back for modeled output/errors
         212  +
                    let _ = std::mem::replace(&mut *state, Some(resp));
         213  +
                }
         214  +
            }
         215  +
        }
         216  +
         217  +
        Ok(())
         218  +
    }
         219  +
         220  +
    fn modify_before_attempt_completion(
         221  +
        &self,
         222  +
        context: &mut FinalizerInterceptorContextMut<'_>,
         223  +
        _runtime_components: &RuntimeComponents,
         224  +
        _cfg: &mut ConfigBag,
         225  +
    ) -> Result<(), BoxError> {
         226  +
        // Handle modeled responses
         227  +
        let mut state = self.active_response.lock().unwrap();
         228  +
        let active_response = (*state).take();
         229  +
        if let Some(resp) = active_response {
         230  +
            match resp {
         231  +
                MockResponse::Output(output) => {
         232  +
                    context.inner_mut().set_output_or_error(Ok(output));
         233  +
                }
         234  +
                MockResponse::Error(error) => {
         235  +
                    context
         236  +
                        .inner_mut()
         237  +
                        .set_output_or_error(Err(OrchestratorError::operation(error)));
         238  +
                }
         239  +
                MockResponse::Http(_) => {
         240  +
                    // HTTP responses are handled by the mock HTTP client
         241  +
                }
         242  +
            }
         243  +
        }
         244  +
         245  +
        Ok(())
         246  +
    }
         247  +
}
         248  +
         249  +
/// Extension for storing mock HTTP responses in request extensions
         250  +
#[derive(Clone)]
         251  +
struct MockHttpResponse(Arc<HttpResponse>);
         252  +
         253  +
/// Create a mock HTTP client that works with the interceptor using existing utilities
         254  +
pub fn create_mock_http_client() -> SharedHttpClient {
         255  +
    infallible_client_fn(|mut req| {
         256  +
        // Try to get the mock HTTP response generator from the extensions
         257  +
        if let Some(mock_response) = req.extensions_mut().remove::<MockHttpResponse>() {
         258  +
            let http_resp =
         259  +
                Arc::try_unwrap(mock_response.0).expect("mock HTTP response has single reference");
         260  +
            return http_resp.try_into_http1x().unwrap();
         261  +
        }
         262  +
         263  +
        // Default dummy response if no mock response is defined
         264  +
        http::Response::builder()
         265  +
            .status(418)
         266  +
            .body(SdkBody::from("Mock HTTP client dummy response"))
         267  +
            .unwrap()
         268  +
    })
         269  +
}
         270  +
         271  +
#[cfg(test)]
         272  +
mod tests {
         273  +
    use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep};
         274  +
    use aws_smithy_runtime::client::orchestrator::operation::Operation;
         275  +
    use aws_smithy_runtime::client::retries::classifiers::HttpStatusCodeClassifier;
         276  +
    use aws_smithy_runtime_api::client::orchestrator::{
         277  +
        HttpRequest, HttpResponse, OrchestratorError,
         278  +
    };
         279  +
    use aws_smithy_runtime_api::client::result::SdkError;
         280  +
    use aws_smithy_runtime_api::http::StatusCode;
         281  +
    use aws_smithy_types::body::SdkBody;
         282  +
    use aws_smithy_types::retry::RetryConfig;
         283  +
    use aws_smithy_types::timeout::TimeoutConfig;
         284  +
         285  +
    use crate::{create_mock_http_client, MockResponseInterceptor, RuleBuilder, RuleMode};
         286  +
    use std::time::Duration;
         287  +
         288  +
    // Simple test input and output types
         289  +
    #[derive(Debug)]
         290  +
    struct TestInput {
         291  +
        bucket: String,
         292  +
        key: String,
         293  +
    }
         294  +
    impl TestInput {
         295  +
        fn new(bucket: &str, key: &str) -> Self {
         296  +
            Self {
         297  +
                bucket: bucket.to_string(),
         298  +
                key: key.to_string(),
         299  +
            }
         300  +
        }
         301  +
    }
         302  +
         303  +
    #[derive(Debug, PartialEq)]
         304  +
    struct TestOutput {
         305  +
        content: String,
         306  +
    }
         307  +
         308  +
    impl TestOutput {
         309  +
        fn new(content: &str) -> Self {
         310  +
            Self {
         311  +
                content: content.to_string(),
         312  +
            }
         313  +
        }
         314  +
    }
         315  +
         316  +
    #[derive(Debug)]
         317  +
    struct TestError {
         318  +
        message: String,
         319  +
    }
         320  +
         321  +
    impl TestError {
         322  +
        fn new(message: &str) -> Self {
         323  +
            Self {
         324  +
                message: message.to_string(),
         325  +
            }
         326  +
        }
         327  +
    }
         328  +
         329  +
    impl std::fmt::Display for TestError {
         330  +
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         331  +
            write!(f, "{}", self.message)
         332  +
        }
         333  +
    }
         334  +
         335  +
    impl std::error::Error for TestError {}
         336  +
         337  +
    // Helper function to create a RuleBuilder with proper type hints
         338  +
    fn create_rule_builder() -> RuleBuilder<TestInput, TestOutput, TestError> {
         339  +
        RuleBuilder::new_from_mock(
         340  +
            || TestInput {
         341  +
                bucket: "".to_string(),
         342  +
                key: "".to_string(),
         343  +
            },
         344  +
            || {
         345  +
                let fut: std::future::Ready<Result<TestOutput, SdkError<TestError, HttpResponse>>> =
         346  +
                    std::future::ready(Ok(TestOutput {
         347  +
                        content: "".to_string(),
         348  +
                    }));
         349  +
                fut
         350  +
            },
         351  +
        )
         352  +
    }
         353  +
         354  +
    // Helper function to create an Operation with common configuration
         355  +
    fn create_test_operation(
         356  +
        interceptor: MockResponseInterceptor,
         357  +
        enable_retries: bool,
         358  +
    ) -> Operation<TestInput, TestOutput, TestError> {
         359  +
        let builder = Operation::builder()
         360  +
            .service_name("test")
         361  +
            .operation_name("test")
         362  +
            .http_client(create_mock_http_client())
         363  +
            .endpoint_url("http://localhost:1234")
         364  +
            .no_auth()
         365  +
            .sleep_impl(SharedAsyncSleep::new(TokioSleep::new()))
         366  +
            .timeout_config(TimeoutConfig::disabled())
         367  +
            .interceptor(interceptor)
         368  +
            .serializer(|input: TestInput| {
         369  +
                let mut request = HttpRequest::new(SdkBody::empty());
         370  +
                request
         371  +
                    .set_uri(format!("/{}/{}", input.bucket, input.key))
         372  +
                    .expect("valid URI");
         373  +
                Ok(request)
         374  +
            })
         375  +
            .deserializer::<TestOutput, TestError>(|response| {
         376  +
                if response.status().is_success() {
         377  +
                    let body = std::str::from_utf8(response.body().bytes().unwrap())
         378  +
                        .unwrap_or("empty body")
         379  +
                        .to_string();
         380  +
                    Ok(TestOutput { content: body })
         381  +
                } else {
         382  +
                    Err(OrchestratorError::operation(TestError {
         383  +
                        message: format!("Error: {}", response.status()),
         384  +
                    }))
         385  +
                }
         386  +
            });
         387  +
         388  +
        if enable_retries {
         389  +
            let retry_config = RetryConfig::standard()
         390  +
                .with_max_attempts(5)
         391  +
                .with_initial_backoff(Duration::from_millis(1))
         392  +
                .with_max_backoff(Duration::from_millis(5));
         393  +
         394  +
            builder
         395  +
                .retry_classifier(HttpStatusCodeClassifier::default())
         396  +
                .standard_retry(&retry_config)
         397  +
                .build()
         398  +
        } else {
         399  +
            builder.no_retry().build()
         400  +
        }
         401  +
    }
         402  +
         403  +
    #[tokio::test]
         404  +
    async fn test_retry_sequence() {
         405  +
        // Create a rule with repeated error responses followed by success
         406  +
        let rule = create_rule_builder()
         407  +
            .match_requests(|input| input.bucket == "test-bucket" && input.key == "test-key")
         408  +
            .sequence()
         409  +
            .http_status(503, None)
         410  +
            .times(2)
         411  +
            .output(|| TestOutput::new("success after retries"))
         412  +
            .build();
         413  +
         414  +
        // Create an interceptor with the rule
         415  +
        let interceptor = MockResponseInterceptor::new()
         416  +
            .rule_mode(RuleMode::Sequential)
         417  +
            .with_rule(&rule);
         418  +
         419  +
        let operation = create_test_operation(interceptor, true);
         420  +
         421  +
        // Make a single request - it should automatically retry through the sequence
         422  +
        let result = operation
         423  +
            .invoke(TestInput::new("test-bucket", "test-key"))
         424  +
            .await;
         425  +
         426  +
        // Should succeed with the final output after retries
         427  +
        assert!(
         428  +
            result.is_ok(),
         429  +
            "Expected success but got error: {:?}",
         430  +
            result.err()
         431  +
        );
         432  +
        assert_eq!(
         433  +
            result.unwrap(),
         434  +
            TestOutput {
         435  +
                content: "success after retries".to_string()
         436  +
            }
         437  +
        );
         438  +
         439  +
        // Verify the rule was used the expected number of times (all 4 responses: 2 errors + 1 success)
         440  +
        assert_eq!(rule.num_calls(), 3);
         441  +
    }
         442  +
         443  +
    #[should_panic(
         444  +
        expected = "must_match was enabled but no rules matched or all rules were exhausted for"
         445  +
    )]
         446  +
    #[tokio::test]
         447  +
    async fn test_exhausted_rules() {
         448  +
        // Create a rule with a single response
         449  +
        let rule = create_rule_builder().then_output(|| TestOutput::new("only response"));
         450  +
         451  +
        // Create an interceptor with the rule
         452  +
        let interceptor = MockResponseInterceptor::new()
         453  +
            .rule_mode(RuleMode::Sequential)
         454  +
            .with_rule(&rule);
         455  +
         456  +
        let operation = create_test_operation(interceptor, false);
         457  +
         458  +
        // First call should succeed
         459  +
        let result1 = operation
         460  +
            .invoke(TestInput::new("test-bucket", "test-key"))
         461  +
            .await;
         462  +
        assert!(result1.is_ok());
         463  +
         464  +
        // Second call should panic because the rules are exhausted
         465  +
        let _result2 = operation
         466  +
            .invoke(TestInput::new("test-bucket", "test-key"))
         467  +
            .await;
         468  +
    }
         469  +
         470  +
    #[tokio::test]
         471  +
    async fn test_rule_mode_match_any() {
         472  +
        // Create two rules with different matchers
         473  +
        let rule1 = create_rule_builder()
         474  +
            .match_requests(|input| input.bucket == "bucket1")
         475  +
            .then_output(|| TestOutput::new("response1"));
         476  +
         477  +
        let rule2 = create_rule_builder()
         478  +
            .match_requests(|input| input.bucket == "bucket2")
         479  +
            .then_output(|| TestOutput::new("response2"));
         480  +
         481  +
        // Create an interceptor with both rules in MatchAny mode
         482  +
        let interceptor = MockResponseInterceptor::new()
         483  +
            .rule_mode(RuleMode::MatchAny)
         484  +
            .with_rule(&rule1)
         485  +
            .with_rule(&rule2);
         486  +
         487  +
        let operation = create_test_operation(interceptor, false);
         488  +
         489  +
        // Call with bucket1 should match rule1
         490  +
        let result1 = operation
         491  +
            .invoke(TestInput::new("bucket1", "test-key"))
         492  +
            .await;
         493  +
        assert!(result1.is_ok());
         494  +
        assert_eq!(result1.unwrap(), TestOutput::new("response1"));
         495  +
         496  +
        // Call with bucket2 should match rule2
         497  +
        let result2 = operation
         498  +
            .invoke(TestInput::new("bucket2", "test-key"))
         499  +
            .await;
         500  +
        assert!(result2.is_ok());
         501  +
        assert_eq!(result2.unwrap(), TestOutput::new("response2"));
         502  +
         503  +
        // Verify the rules were used the expected number of times
         504  +
        assert_eq!(rule1.num_calls(), 1);
         505  +
        assert_eq!(rule2.num_calls(), 1);
         506  +
    }
         507  +
         508  +
    #[tokio::test]
         509  +
    async fn test_mixed_response_types() {
         510  +
        // Create a rule with all three types of responses
         511  +
        let rule = create_rule_builder()
         512  +
            .sequence()
         513  +
            .output(|| TestOutput::new("first output"))
         514  +
            .error(|| TestError::new("expected error"))
         515  +
            .http_response(|| {
         516  +
                HttpResponse::new(
         517  +
                    StatusCode::try_from(200).unwrap(),
         518  +
                    SdkBody::from("http response"),
         519  +
                )
         520  +
            })
         521  +
            .build();
         522  +
         523  +
        // Create an interceptor with the rule
         524  +
        let interceptor = MockResponseInterceptor::new()
         525  +
            .rule_mode(RuleMode::Sequential)
         526  +
            .with_rule(&rule);
         527  +
         528  +
        let operation = create_test_operation(interceptor, false);
         529  +
         530  +
        // First call should return the modeled output
         531  +
        let result1 = operation
         532  +
            .invoke(TestInput::new("test-bucket", "test-key"))
         533  +
            .await;
         534  +
        assert!(result1.is_ok());
         535  +
        assert_eq!(result1.unwrap(), TestOutput::new("first output"));
         536  +
         537  +
        // Second call should return the modeled error
         538  +
        let result2 = operation
         539  +
            .invoke(TestInput::new("test-bucket", "test-key"))
         540  +
            .await;
         541  +
        assert!(result2.is_err());
         542  +
        let sdk_err = result2.unwrap_err();
         543  +
        let err = sdk_err.as_service_error().expect("expected service error");
         544  +
        assert_eq!(err.to_string(), "expected error");
         545  +
         546  +
        // Third call should return the HTTP response
         547  +
        let result3 = operation
         548  +
            .invoke(TestInput::new("test-bucket", "test-key"))
         549  +
            .await;
         550  +
        assert!(result3.is_ok());
         551  +
        assert_eq!(result3.unwrap(), TestOutput::new("http response"));
         552  +
         553  +
        // Verify the rule was used the expected number of times
         554  +
        assert_eq!(rule.num_calls(), 3);
         555  +
    }
         556  +
         557  +
    #[tokio::test]
         558  +
    async fn test_exhausted_sequence() {
         559  +
        // Create a rule with a sequence that will be exhausted
         560  +
        let rule = create_rule_builder()
         561  +
            .sequence()
         562  +
            .output(|| TestOutput::new("response 1"))
         563  +
            .output(|| TestOutput::new("response 2"))
         564  +
            .build();
         565  +
         566  +
        // Create another rule to use after the first one is exhausted
         567  +
        let fallback_rule =
         568  +
            create_rule_builder().then_output(|| TestOutput::new("fallback response"));
         569  +
         570  +
        // Create an interceptor with both rules
         571  +
        let interceptor = MockResponseInterceptor::new()
         572  +
            .rule_mode(RuleMode::Sequential)
         573  +
            .with_rule(&rule)
         574  +
            .with_rule(&fallback_rule);
         575  +
         576  +
        let operation = create_test_operation(interceptor, false);
         577  +
         578  +
        // First two calls should use the first rule
         579  +
        let result1 = operation
         580  +
            .invoke(TestInput::new("test-bucket", "test-key"))
         581  +
            .await;
         582  +
        assert!(result1.is_ok());
         583  +
        assert_eq!(result1.unwrap(), TestOutput::new("response 1"));
         584  +
         585  +
        let result2 = operation
         586  +
            .invoke(TestInput::new("test-bucket", "test-key"))
         587  +
            .await;
         588  +
        assert!(result2.is_ok());
         589  +
        assert_eq!(result2.unwrap(), TestOutput::new("response 2"));
         590  +
         591  +
        // Third call should use the fallback rule
         592  +
        let result3 = operation
         593  +
            .invoke(TestInput::new("test-bucket", "test-key"))
         594  +
            .await;
         595  +
        assert!(result3.is_ok());
         596  +
        assert_eq!(result3.unwrap(), TestOutput::new("fallback response"));
         597  +
         598  +
        // Verify the rules were used the expected number of times
         599  +
        assert_eq!(rule.num_calls(), 2);
         600  +
        assert_eq!(fallback_rule.num_calls(), 1);
         601  +
    }
         602  +
         603  +
    #[tokio::test]
         604  +
    async fn test_concurrent_usage() {
         605  +
        use std::sync::Arc;
         606  +
        use tokio::task;
         607  +
         608  +
        // Create a rule with multiple responses
         609  +
        let rule = Arc::new(
         610  +
            create_rule_builder()
         611  +
                .sequence()
         612  +
                .output(|| TestOutput::new("response 1"))
         613  +
                .output(|| TestOutput::new("response 2"))
         614  +
                .output(|| TestOutput::new("response 3"))
         615  +
                .build(),
         616  +
        );
         617  +
         618  +
        // Create an interceptor with the rule
         619  +
        let interceptor = MockResponseInterceptor::new()
         620  +
            .rule_mode(RuleMode::Sequential)
         621  +
            .with_rule(&rule);
         622  +
         623  +
        let operation = Arc::new(create_test_operation(interceptor, false));
         624  +
         625  +
        // Spawn multiple tasks that use the operation concurrently
         626  +
        let mut handles = vec![];
         627  +
        for i in 0..3 {
         628  +
            let op = operation.clone();
         629  +
            let handle = task::spawn(async move {
         630  +
                let result = op
         631  +
                    .invoke(TestInput::new(&format!("bucket-{}", i), "test-key"))
         632  +
                    .await;
         633  +
                result.unwrap()
         634  +
            });
         635  +
            handles.push(handle);
         636  +
        }
         637  +
         638  +
        // Wait for all tasks to complete
         639  +
        let mut results = vec![];
         640  +
        for handle in handles {
         641  +
            results.push(handle.await.unwrap());
         642  +
        }
         643  +
         644  +
        // Sort the results to make the test deterministic
         645  +
        results.sort_by(|a, b| a.content.cmp(&b.content));
         646  +
         647  +
        // Verify we got all three responses
         648  +
        assert_eq!(results.len(), 3);
         649  +
        assert_eq!(results[0], TestOutput::new("response 1"));
         650  +
        assert_eq!(results[1], TestOutput::new("response 2"));
         651  +
        assert_eq!(results[2], TestOutput::new("response 3"));
         652  +
         653  +
        // Verify the rule was used the expected number of times
         654  +
        assert_eq!(rule.num_calls(), 3);
         655  +
    }
         656  +
         657  +
    #[tokio::test]
         658  +
    async fn test_sequential_rule_removal() {
         659  +
        // Create a rule that matches only when key != "correct-key"
         660  +
        let rule1 = create_rule_builder()
         661  +
            .match_requests(|input| input.bucket == "test-bucket" && input.key != "correct-key")
         662  +
            .then_http_response(|| {
         663  +
                HttpResponse::new(
         664  +
                    StatusCode::try_from(404).unwrap(),
         665  +
                    SdkBody::from("not found"),
         666  +
                )
         667  +
            });
         668  +
         669  +
        // Create a rule that matches only when key == "correct-key"
         670  +
        let rule2 = create_rule_builder()
         671  +
            .match_requests(|input| input.bucket == "test-bucket" && input.key == "correct-key")
         672  +
            .then_output(|| TestOutput::new("success"));
         673  +
         674  +
        // Create an interceptor with both rules in Sequential mode
         675  +
        let interceptor = MockResponseInterceptor::new()
         676  +
            .rule_mode(RuleMode::Sequential)
         677  +
            .with_rule(&rule1)
         678  +
            .with_rule(&rule2);
         679  +
         680  +
        let operation = create_test_operation(interceptor, true);
         681  +
         682  +
        // First call with key="foo" should match rule1
         683  +
        let result1 = operation.invoke(TestInput::new("test-bucket", "foo")).await;
         684  +
        assert!(result1.is_err());
         685  +
        assert_eq!(rule1.num_calls(), 1);
         686  +
         687  +
        // Second call with key="correct-key" should match rule2
         688  +
        // But this will fail if rule1 is not removed after being used
         689  +
        let result2 = operation
         690  +
            .invoke(TestInput::new("test-bucket", "correct-key"))
         691  +
            .await;
         692  +
         693  +
        // This should succeed, rule1 doesn't match but should have been removed
         694  +
        assert!(result2.is_ok());
         695  +
        assert_eq!(result2.unwrap(), TestOutput::new("success"));
         696  +
        assert_eq!(rule2.num_calls(), 1);
         697  +
    }
         698  +
}