Skip to content

Commit 54f4223

Browse files
greenwoodcmaajtodd
andauthored
Add then_compute_response method to aws-smithy-mocks (#4388)
Adds a new method that allows developers to stub arbitrary responses (outputs, errors, or HTTP responses) based on input request properties. Unlike then_compute_output which only returns successful outputs, this method enables conditional response generation where the response type can vary based on the input. Changes: - Make MockResponse enum public to allow user construction - Add RuleBuilder::then_compute_response() method - Add ResponseSequenceBuilder::compute_response() internal method - Export MockResponse from lib.rs - Add integration test demonstrating conditional responses - Update README with usage example Example usage: ``` mock!(Client::get_object) .then_compute_response(|req| { if req.key() == Some("error-key") { MockResponse::Error(...) } else { MockResponse::Output(...) } }) ``` ## Motivation and Context Allow for stubbing/faking that sends failures back to the caller ## Testing crate tests ## Checklist <!--- If a checkbox below is not applicable, then please DELETE it rather than leaving it unchecked --> - [ ] For changes to the smithy-rs codegen or runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "client," "server," or both in the `applies_to` key. - [ ] For changes to the AWS SDK, generated SDK code, or SDK runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "aws-sdk-rust" in the `applies_to` key. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._ Co-authored-by: Aaron Todd <aajtodd@users.noreply.github.com>
1 parent 691f512 commit 54f4223

File tree

6 files changed

+100
-4
lines changed

6 files changed

+100
-4
lines changed

.changelog/1762538321.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
applies_to: ["client"]
3+
authors:
4+
- greenwoodcm
5+
references:
6+
- smithy-rs#4388
7+
breaking: false
8+
new_feature: false
9+
bug_fix: false
10+
---
11+
Add `then_compute_response` to Smithy mock

rust-runtime/aws-smithy-mocks/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aws-smithy-mocks"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
55
description = "Testing utilities for smithy-rs generated clients"
66
edition = "2021"

rust-runtime/aws-smithy-mocks/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ let compute_rule = mock!(Client::get_object)
117117
.body(ByteStream::from_static(format!("content for {}", key).as_bytes()))
118118
.build()
119119
});
120+
121+
// Return any response type (output, error, or HTTP) based on the input
122+
let conditional_rule = mock!(Client::get_object)
123+
.then_compute_response(|req| {
124+
use aws_smithy_mocks::MockResponse;
125+
if req.key() == Some("error-key") {
126+
MockResponse::Error(GetObjectError::NoSuchKey(NoSuchKey::builder().build()))
127+
} else {
128+
MockResponse::Output(GetObjectOutput::builder()
129+
.body(ByteStream::from_static(b"success"))
130+
.build())
131+
}
132+
});
120133
```
121134

122135
### Response Sequences

rust-runtime/aws-smithy-mocks/src/interceptor.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,4 +911,34 @@ mod tests {
911911
.build();
912912
assert_eq!(rule.max_responses, usize::MAX);
913913
}
914+
915+
#[tokio::test]
916+
async fn test_compute_response_conditional() {
917+
use crate::MockResponse;
918+
919+
let rule = create_rule_builder().then_compute_response(|input| {
920+
if input.key == "error-key" {
921+
MockResponse::Error(TestError::new("conditional error"))
922+
} else {
923+
MockResponse::Output(TestOutput::new(&format!("response for {}", input.key)))
924+
}
925+
});
926+
927+
let interceptor = MockResponseInterceptor::new().with_rule(&rule);
928+
let operation = create_test_operation(interceptor, false);
929+
930+
// Test success case
931+
let result = operation
932+
.invoke(TestInput::new("test-bucket", "success-key"))
933+
.await;
934+
assert!(result.is_ok());
935+
assert_eq!(result.unwrap(), TestOutput::new("response for success-key"));
936+
937+
// Test error case
938+
let result = operation
939+
.invoke(TestInput::new("test-bucket", "error-key"))
940+
.await;
941+
assert!(result.is_err());
942+
assert_eq!(rule.num_calls(), 2);
943+
}
914944
}

rust-runtime/aws-smithy-mocks/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ mod interceptor;
1818
mod rule;
1919

2020
pub use interceptor::{create_mock_http_client, MockResponseInterceptor};
21-
pub(crate) use rule::MockResponse;
22-
pub use rule::{Rule, RuleBuilder, RuleMode};
21+
pub use rule::{MockResponse, Rule, RuleBuilder, RuleMode};
2322

2423
// why do we need a macro for this?
2524
// We want customers to be able to provide an ergonomic way to say the method they're looking for,

rust-runtime/aws-smithy-mocks/src/rule.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use std::sync::Arc;
2222
///
2323
#[derive(Debug)]
2424
#[allow(clippy::large_enum_variant)]
25-
pub(crate) enum MockResponse<O, E> {
25+
pub enum MockResponse<O, E> {
2626
/// A successful modeled response.
2727
Output(O),
2828
/// A modeled error.
@@ -251,6 +251,33 @@ where
251251
{
252252
self.sequence().compute_output(compute_fn).build_simple()
253253
}
254+
255+
/// Creates a rule that computes an arbitrary response based on the input.
256+
///
257+
/// This allows generating any type of response (output, error, or HTTP) based on the input request.
258+
/// Unlike `then_compute_output`, this method can return errors or HTTP responses conditionally.
259+
///
260+
/// # Examples
261+
///
262+
/// ```rust,ignore
263+
/// let rule = mock!(Client::get_object)
264+
/// .then_compute_response(|req| {
265+
/// if req.key() == Some("error") {
266+
/// MockResponse::Error(GetObjectError::NoSuchKey(NoSuchKey::builder().build()))
267+
/// } else {
268+
/// MockResponse::Output(GetObjectOutput::builder()
269+
/// .body(ByteStream::from_static(b"content"))
270+
/// .build())
271+
/// }
272+
/// })
273+
/// .build();
274+
/// ```
275+
pub fn then_compute_response<F>(self, compute_fn: F) -> Rule
276+
where
277+
F: Fn(&I) -> MockResponse<O, E> + Send + Sync + 'static,
278+
{
279+
self.sequence().compute_response(compute_fn).build_simple()
280+
}
254281
}
255282

256283
type SequenceGeneratorFn<O, E> = Arc<dyn Fn(&Input) -> MockResponse<O, E> + Send + Sync>;
@@ -366,6 +393,22 @@ where
366393
self
367394
}
368395

396+
/// Add a computed response to the sequence. Not `pub` for same reason as `compute_output`.
397+
fn compute_response<F>(mut self, compute_fn: F) -> Self
398+
where
399+
F: Fn(&I) -> MockResponse<O, E> + Send + Sync + 'static,
400+
{
401+
let generator = Arc::new(move |input: &Input| {
402+
if let Some(typed_input) = input.downcast_ref::<I>() {
403+
compute_fn(typed_input)
404+
} else {
405+
panic!("Input type mismatch in compute_response")
406+
}
407+
});
408+
self.generators.push((generator, 1));
409+
self
410+
}
411+
369412
/// Repeat the last added response multiple times.
370413
///
371414
/// This method sets the number of times the last response in the sequence will be used.

0 commit comments

Comments
 (0)