aws_smithy_mocks_experimental/
lib.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! This crate has been deprecated. Please migrate to the `aws-smithy-mocks` crate.
7
8/* Automatically managed default lints */
9#![cfg_attr(docsrs, feature(doc_auto_cfg))]
10/* End of automatically managed default lints */
11#![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// why do we need a macro for this?
34// We want customers to be able to provide an ergonomic way to say the method they're looking for,
35// `Client::list_buckets`, e.g. But there isn't enough information on that type to recover everything.
36// This macro commits a small amount of crimes to recover that type information so we can construct
37// a rule that can intercept these operations.
38
39/// `mock!` macro that produces a [`RuleBuilder`] from a client invocation
40///
41/// See the `examples` folder of this crate for fully worked examples.
42///
43/// # Examples
44/// **Mock and return a success response**:
45/// ```rust,ignore
46/// use aws_sdk_s3::operation::get_object::GetObjectOutput;
47/// use aws_sdk_s3::Client;
48/// use aws_smithy_types::byte_stream::ByteStream;
49/// use aws_smithy_mocks_experimental::mock;
50/// let get_object_happy_path = mock!(Client::get_object)
51///   .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
52///   .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build());
53/// ```
54///
55/// **Mock and return an error**:
56/// ```rust,ignore
57/// use aws_sdk_s3::operation::get_object::GetObjectError;
58/// use aws_sdk_s3::types::error::NoSuchKey;
59/// use aws_sdk_s3::Client;
60/// use aws_smithy_mocks_experimental::mock;
61/// let get_object_error_path = mock!(Client::get_object)
62///   .then_error(||GetObjectError::NoSuchKey(NoSuchKey::builder().build()));
63/// ```
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)]
69macro_rules! mock {
70    ($operation: expr) => {
71        #[allow(unreachable_code)]
72        {
73            $crate::RuleBuilder::new(
74                // We don't actually want to run this code, so we put it in a closure. The closure
75                // has the types we want which makes this whole thing type-safe (and the IDE can even
76                // figure out the right input/output types in inference!)
77                // The code generated here is:
78                // `Client::list_buckets(todo!())`
79                || $operation(todo!()).as_input().clone().build().unwrap(),
80                || $operation(todo!()).send(),
81            )
82        }
83    };
84}
85
86// This could be obviated by a reasonable trait, since you can express it with SdkConfig if clients implement From<&SdkConfig>.
87
88/// `mock_client!` macro produces a Client configured with a number of Rules and appropriate test default configuration.
89///
90/// # Examples
91/// **Create a client that uses a mock failure and then a success**:
92/// ```rust,ignore
93/// use aws_sdk_s3::operation::get_object::{GetObjectOutput, GetObjectError};
94/// use aws_sdk_s3::types::error::NoSuchKey;
95/// use aws_sdk_s3::Client;
96/// use aws_smithy_types::byte_stream::ByteStream;
97/// use aws_smithy_mocks_experimental::{mock_client, mock, RuleMode};
98/// let get_object_happy_path = mock!(Client::get_object)
99///   .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
100///   .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build());
101/// let get_object_error_path = mock!(Client::get_object)
102///   .then_error(||GetObjectError::NoSuchKey(NoSuchKey::builder().build()));
103/// let client = mock_client!(aws_sdk_s3, RuleMode::Sequential, &[&get_object_error_path, &get_object_happy_path]);
104/// ```
105///
106/// **Create a client but customize a specific setting**:
107/// ```rust,ignore
108/// use aws_sdk_s3::operation::get_object::GetObjectOutput;
109/// use aws_sdk_s3::Client;
110/// use aws_smithy_types::byte_stream::ByteStream;
111/// use aws_smithy_mocks_experimental::{mock_client, mock, RuleMode};
112/// let get_object_happy_path = mock!(Client::get_object)
113///   .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
114///   .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build());
115/// let client = mock_client!(
116///     aws_sdk_s3,
117///     RuleMode::Sequential,
118///     &[&get_object_happy_path],
119///     // Perhaps you need to force path style
120///     |client_builder|client_builder.force_path_style(true)
121/// );
122/// ```
123///
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)]
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        // allow callers to avoid explicitly specifying the type
143        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/// RuleMode describes how rules will be interpreted.
174/// - In RuleMode::MatchAny, the first matching rule will be applied, and the rules will remain unchanged.
175/// - In RuleMode::Sequential, the first matching rule will be applied, and that rule will be removed from the list of rules.
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)]
180pub enum RuleMode {
181    MatchAny,
182    Sequential,
183}
184
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)]
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    /// Creates a new [`RuleBuilder`]. This is normally constructed with the [`mock!`] macro
222    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    /// Add an additional filter to constrain which inputs match this rule.
233    ///
234    /// For examples, see the examples directory of this repository.
235    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    /// If the rule matches, then return a specific HTTP response.
244    ///
245    /// This is the recommended way of testing error behavior.
246    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    /// If a rule matches, then return a specific output
257    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    /// If a rule matches, then return a specific error
265    ///
266    /// Although this _basically_ works, using `then_http_response` is strongly recommended to
267    /// create a higher fidelity mock. Error handling is quite complex in practice and returning errors
268    /// directly often will not perfectly capture the way the error is actually returned to the SDK.
269    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    /// Returns the number of times this rule has been hit.
309    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    /// Add a rule to the Interceptor
333    ///
334    /// Rules are matched in order—this rule will only apply if all previous rules do not match.
335    pub fn with_rule(self, rule: &Rule) -> Self {
336        self.rules.lock().unwrap().push_back(rule.clone());
337        self
338    }
339
340    /// Set the RuleMode to use when evaluating rules.
341    ///
342    /// See `RuleMode` enum for modes and how they are applied.
343    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                // the orchestrator will panic of no response is present
440                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}