aws_smithy_mocks_experimental/
lib.rs1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
10#![allow(deprecated)]
12
13use std::collections::VecDeque;
14use std::fmt::{Debug, Formatter};
15use std::future::Future;
16use std::marker::PhantomData;
17use std::sync::atomic::{AtomicUsize, Ordering};
18use std::sync::{Arc, Mutex};
19
20use aws_smithy_runtime_api::box_error::BoxError;
21use aws_smithy_runtime_api::client::interceptors::context::{
22 BeforeDeserializationInterceptorContextMut, BeforeSerializationInterceptorContextMut, Error,
23 FinalizerInterceptorContextMut, Input, Output,
24};
25use aws_smithy_runtime_api::client::interceptors::Intercept;
26use aws_smithy_runtime_api::client::orchestrator::{HttpResponse, OrchestratorError};
27use aws_smithy_runtime_api::client::result::SdkError;
28use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
29use aws_smithy_runtime_api::http::{Response, StatusCode};
30use aws_smithy_types::body::SdkBody;
31use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace};
32
33#[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)]
69macro_rules! mock {
70 ($operation: expr) => {
71 #[allow(unreachable_code)]
72 {
73 $crate::RuleBuilder::new(
74 || $operation(todo!()).as_input().clone().build().unwrap(),
80 || $operation(todo!()).send(),
81 )
82 }
83 };
84}
85
86#[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)]
129macro_rules! mock_client {
130 ($aws_crate: ident, $rules: expr) => {
131 $crate::mock_client!($aws_crate, $crate::RuleMode::Sequential, $rules)
132 };
133 ($aws_crate: ident, $rule_mode: expr, $rules: expr) => {{
134 $crate::mock_client!($aws_crate, $rule_mode, $rules, |conf| conf)
135 }};
136 ($aws_crate: ident, $rule_mode: expr, $rules: expr, $additional_configuration: expr) => {{
137 let mut mock_response_interceptor =
138 $crate::MockResponseInterceptor::new().rule_mode($rule_mode);
139 for rule in $rules {
140 mock_response_interceptor = mock_response_interceptor.with_rule(rule)
141 }
142 fn coerce<T: Fn($aws_crate::config::Builder) -> $aws_crate::config::Builder>(f: T) -> T {
144 f
145 }
146 $aws_crate::client::Client::from_conf(
147 coerce($additional_configuration)(
148 $aws_crate::config::Config::builder()
149 .with_test_defaults()
150 .region($aws_crate::config::Region::from_static("us-east-1"))
151 .interceptor(mock_response_interceptor),
152 )
153 .build(),
154 )
155 }};
156}
157
158type MatchFn = Arc<dyn Fn(&Input) -> bool + Send + Sync>;
159type OutputFn = Arc<dyn Fn() -> Result<Output, OrchestratorError<Error>> + Send + Sync>;
160
161impl Debug for MockResponseInterceptor {
162 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
163 write!(f, "{} rules", self.rules.lock().unwrap().len())
164 }
165}
166
167#[derive(Clone)]
168enum MockOutput {
169 HttpResponse(Arc<dyn Fn() -> Result<HttpResponse, BoxError> + Send + Sync>),
170 ModeledResponse(OutputFn),
171}
172
173#[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)]
180pub enum RuleMode {
181 MatchAny,
182 Sequential,
183}
184
185#[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)]
190pub struct MockResponseInterceptor {
191 rules: Arc<Mutex<VecDeque<Rule>>>,
192 rule_mode: RuleMode,
193 must_match: bool,
194}
195
196impl Default for MockResponseInterceptor {
197 fn default() -> Self {
198 Self::new()
199 }
200}
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)]
206pub struct RuleBuilder<I, O, E> {
207 _ty: PhantomData<(I, O, E)>,
208 input_filter: MatchFn,
209}
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)]
215impl<I, O, E> RuleBuilder<I, O, E>
216where
217 I: Send + Sync + Debug + 'static,
218 O: Send + Sync + Debug + 'static,
219 E: Send + Sync + Debug + std::error::Error + 'static,
220{
221 pub fn new<F, R>(_input_hint: impl Fn() -> I, _output_hint: impl Fn() -> F) -> Self
223 where
224 F: Future<Output = Result<O, SdkError<E, R>>>,
225 {
226 Self {
227 _ty: Default::default(),
228 input_filter: Arc::new(|i: &Input| i.downcast_ref::<I>().is_some()),
229 }
230 }
231
232 pub fn match_requests(mut self, filter: impl Fn(&I) -> bool + Send + Sync + 'static) -> Self {
236 self.input_filter = Arc::new(move |i: &Input| match i.downcast_ref::<I>() {
237 Some(typed_input) => filter(typed_input),
238 _ => false,
239 });
240 self
241 }
242
243 pub fn then_http_response(
247 self,
248 response: impl Fn() -> HttpResponse + Send + Sync + 'static,
249 ) -> Rule {
250 Rule::new(
251 self.input_filter,
252 MockOutput::HttpResponse(Arc::new(move || Ok(response()))),
253 )
254 }
255
256 pub fn then_output(self, output: impl Fn() -> O + Send + Sync + 'static) -> Rule {
258 Rule::new(
259 self.input_filter,
260 MockOutput::ModeledResponse(Arc::new(move || Ok(Output::erase(output())))),
261 )
262 }
263
264 pub fn then_error(self, output: impl Fn() -> E + Send + Sync + 'static) -> Rule {
270 Rule::new(
271 self.input_filter,
272 MockOutput::ModeledResponse(Arc::new(move || {
273 Err(OrchestratorError::operation(Error::erase(output())))
274 })),
275 )
276 }
277}
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)]
283#[derive(Clone)]
284pub struct Rule {
285 matcher: MatchFn,
286 output: MockOutput,
287 used_count: Arc<AtomicUsize>,
288}
289
290impl Debug for Rule {
291 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
292 write!(f, "Rule")
293 }
294}
295
296impl Rule {
297 fn new(matcher: MatchFn, output: MockOutput) -> Self {
298 Self {
299 matcher,
300 output,
301 used_count: Default::default(),
302 }
303 }
304 fn record_usage(&self) {
305 self.used_count.fetch_add(1, Ordering::Relaxed);
306 }
307
308 pub fn num_calls(&self) -> usize {
310 self.used_count.load(Ordering::Relaxed)
311 }
312}
313
314#[derive(Debug)]
315struct ActiveRule(Rule);
316impl Storable for ActiveRule {
317 type Storer = StoreReplace<ActiveRule>;
318}
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)]
324impl MockResponseInterceptor {
325 pub fn new() -> Self {
326 Self {
327 rules: Default::default(),
328 rule_mode: RuleMode::MatchAny,
329 must_match: true,
330 }
331 }
332 pub fn with_rule(self, rule: &Rule) -> Self {
336 self.rules.lock().unwrap().push_back(rule.clone());
337 self
338 }
339
340 pub fn rule_mode(mut self, rule_mode: RuleMode) -> Self {
344 self.rule_mode = rule_mode;
345 self
346 }
347
348 pub fn allow_passthrough(mut self) -> Self {
349 self.must_match = false;
350 self
351 }
352}
353
354impl Intercept for MockResponseInterceptor {
355 fn name(&self) -> &'static str {
356 "test"
357 }
358
359 fn modify_before_serialization(
360 &self,
361 context: &mut BeforeSerializationInterceptorContextMut<'_>,
362 _runtime_components: &RuntimeComponents,
363 cfg: &mut ConfigBag,
364 ) -> Result<(), BoxError> {
365 let mut rules = self.rules.lock().unwrap();
366 let rule = match self.rule_mode {
367 RuleMode::Sequential => {
368 let rule = rules
369 .pop_front()
370 .expect("no more rules but a new request was received");
371 if !(rule.matcher)(context.input()) {
372 panic!(
373 "In order matching was enforced but the next rule did not match {:?}",
374 context.input()
375 );
376 }
377 Some(rule)
378 }
379 RuleMode::MatchAny => rules
380 .iter()
381 .find(|rule| (rule.matcher)(context.input()))
382 .cloned(),
383 };
384 match rule {
385 Some(rule) => {
386 cfg.interceptor_state().store_put(ActiveRule(rule.clone()));
387 }
388 None => {
389 if self.must_match {
390 panic!(
391 "must_match was enabled but no rules matches {:?}",
392 context.input()
393 );
394 }
395 }
396 }
397 Ok(())
398 }
399
400 fn modify_before_deserialization(
401 &self,
402 context: &mut BeforeDeserializationInterceptorContextMut<'_>,
403 _runtime_components: &RuntimeComponents,
404 cfg: &mut ConfigBag,
405 ) -> Result<(), BoxError> {
406 if let Some(rule) = cfg.load::<ActiveRule>() {
407 let rule = &rule.0;
408 let result = match &rule.output {
409 MockOutput::HttpResponse(output_fn) => output_fn(),
410 _ => return Ok(()),
411 };
412 rule.record_usage();
413
414 match result {
415 Ok(http_response) => *context.response_mut() = http_response,
416 Err(e) => context
417 .inner_mut()
418 .set_output_or_error(Err(OrchestratorError::response(e))),
419 }
420 }
421 Ok(())
422 }
423
424 fn modify_before_attempt_completion(
425 &self,
426 context: &mut FinalizerInterceptorContextMut<'_>,
427 _runtime_components: &RuntimeComponents,
428 _cfg: &mut ConfigBag,
429 ) -> Result<(), BoxError> {
430 if let Some(rule) = _cfg.load::<ActiveRule>() {
431 let rule = &rule.0;
432 let result = match &rule.output {
433 MockOutput::ModeledResponse(output_fn) => output_fn(),
434 _ => return Ok(()),
435 };
436
437 rule.record_usage();
438 if result.is_err() {
439 context.inner_mut().set_response(Response::new(
441 StatusCode::try_from(500).unwrap(),
442 SdkBody::from("stubbed error response"),
443 ))
444 }
445 context.inner_mut().set_output_or_error(result);
446 }
447 Ok(())
448 }
449}