Skip to main content

mockito/
matcher.rs

1use crate::request::Request;
2use assert_json_diff::{assert_json_matches_no_panic, CompareMode};
3use http::header::HeaderValue;
4use regex::Regex;
5use std::collections::HashMap;
6use std::fmt;
7use std::fs::File;
8use std::io;
9use std::io::Read;
10use std::path::Path;
11use std::sync::Arc;
12
13///
14/// Allows matching the request path, headers or body in multiple ways: by the exact value, by any value (as
15/// long as it is present), by regular expression or by checking that a particular header is missing.
16///
17/// These matchers can be used within the `Server::mock`, `Mock::match_header` or `Mock::match_body` calls.
18///
19#[derive(Clone, PartialEq, Debug)]
20#[allow(deprecated)] // Rust bug #38832
21pub enum Matcher {
22    /// Matches the exact path or header value. There's also an implementation of `From<&str>`
23    /// to keep things simple and backwards compatible.
24    Exact(String),
25    /// Matches the body content as a binary file
26    Binary(BinaryBody),
27    /// Matches a path or header value by a regular expression.
28    Regex(String),
29    /// Matches a specified JSON body from a `serde_json::Value`
30    Json(serde_json::Value),
31    /// Matches a specified JSON body from a `String`
32    JsonString(String),
33    /// Matches a partial JSON body from a `serde_json::Value`
34    PartialJson(serde_json::Value),
35    /// Matches a specified partial JSON body from a `String`
36    PartialJsonString(String),
37    /// Matches a URL-encoded key/value pair, where both key and value should be specified
38    /// in plain (unencoded) format
39    UrlEncoded(String, String),
40    /// At least one matcher must match
41    AnyOf(Vec<Matcher>),
42    /// All matchers must match
43    AllOf(Vec<Matcher>),
44    /// Matches any path or any header value.
45    Any,
46    /// Checks that a header is not present in the request.
47    Missing,
48}
49
50impl<'a> From<&'a str> for Matcher {
51    fn from(value: &str) -> Self {
52        Matcher::Exact(value.to_string())
53    }
54}
55
56#[allow(clippy::fallible_impl_from)]
57impl From<&Path> for Matcher {
58    fn from(value: &Path) -> Self {
59        // We want the code to panic if the path is not readable.
60        Matcher::Binary(BinaryBody::from_path(value).unwrap())
61    }
62}
63
64impl From<&mut File> for Matcher {
65    fn from(value: &mut File) -> Self {
66        Matcher::Binary(BinaryBody::from_file(value))
67    }
68}
69
70impl From<Vec<u8>> for Matcher {
71    fn from(value: Vec<u8>) -> Self {
72        Matcher::Binary(BinaryBody::from_bytes(value))
73    }
74}
75
76impl fmt::Display for Matcher {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        let join_matches = |matches: &[Self]| {
79            matches
80                .iter()
81                .map(Self::to_string)
82                .fold(String::new(), |acc, matcher| {
83                    if acc.is_empty() {
84                        matcher
85                    } else {
86                        format!("{}, {}", acc, matcher)
87                    }
88                })
89        };
90
91        let result = match self {
92            Matcher::Exact(ref value) => value.to_string(),
93            Matcher::Binary(ref file) => format!("{} (binary)", file),
94            Matcher::Regex(ref value) => format!("{} (regex)", value),
95            Matcher::Json(ref json_obj) => format!("{} (json)", json_obj),
96            Matcher::JsonString(ref value) => format!("{} (json)", value),
97            Matcher::PartialJson(ref json_obj) => format!("{} (partial json)", json_obj),
98            Matcher::PartialJsonString(ref value) => format!("{} (partial json)", value),
99            Matcher::UrlEncoded(ref field, ref value) => {
100                format!("{}={} (urlencoded)", field, value)
101            }
102            Matcher::Any => "(any)".to_string(),
103            Matcher::AnyOf(x) => format!("({}) (any of)", join_matches(x)),
104            Matcher::AllOf(x) => format!("({}) (all of)", join_matches(x)),
105            Matcher::Missing => "(missing)".to_string(),
106        };
107        write!(f, "{}", result)
108    }
109}
110
111impl Matcher {
112    pub(crate) fn matches_values(&self, header_values: &[&HeaderValue]) -> bool {
113        match self {
114            Matcher::Missing => header_values.is_empty(),
115            // AnyOf([…Missing…]) is handled here, but
116            // AnyOf([Something]) is handled in the last block.
117            // That's because Missing matches against all values at once,
118            // but other matchers match against individual values.
119            Matcher::AnyOf(ref matchers) if header_values.is_empty() => {
120                matchers.iter().any(|m| m.matches_values(header_values))
121            }
122            Matcher::AllOf(ref matchers) if header_values.is_empty() => {
123                matchers.iter().all(|m| m.matches_values(header_values))
124            }
125            _ => {
126                !header_values.is_empty()
127                    && header_values.iter().all(|val| {
128                        val.to_str()
129                            .map(|val| self.matches_value(val))
130                            .unwrap_or(false)
131                    })
132            }
133        }
134    }
135
136    pub(crate) fn matches_binary_value(&self, binary: &[u8]) -> bool {
137        match self {
138            Matcher::Binary(ref file) => binary == &*file.content,
139            _ => false,
140        }
141    }
142
143    #[allow(deprecated)]
144    pub(crate) fn matches_value(&self, other: &str) -> bool {
145        let compare_json_config = assert_json_diff::Config::new(CompareMode::Inclusive);
146        match self {
147            Matcher::Exact(ref value) => value == other,
148            Matcher::Binary(_) => false,
149            Matcher::Regex(ref regex) => Regex::new(regex).unwrap().is_match(other),
150            Matcher::Json(ref json_obj) => {
151                let other: serde_json::Value = serde_json::from_str(other).unwrap();
152                *json_obj == other
153            }
154            Matcher::JsonString(ref value) => {
155                let value: serde_json::Value = serde_json::from_str(value).unwrap();
156                let other: serde_json::Value = serde_json::from_str(other).unwrap();
157                value == other
158            }
159            Matcher::PartialJson(ref json_obj) => {
160                let actual: serde_json::Value = serde_json::from_str(other).unwrap();
161                let expected = json_obj.clone();
162                assert_json_matches_no_panic(&actual, &expected, compare_json_config).is_ok()
163            }
164            Matcher::PartialJsonString(ref value) => {
165                let expected: serde_json::Value = serde_json::from_str(value).unwrap();
166                let actual: serde_json::Value = serde_json::from_str(other).unwrap();
167                assert_json_matches_no_panic(&actual, &expected, compare_json_config).is_ok()
168            }
169            Matcher::UrlEncoded(ref expected_field, ref expected_value) => {
170                serde_urlencoded::from_str::<HashMap<String, String>>(other)
171                    .map(|params: HashMap<_, _>| {
172                        params.into_iter().any(|(ref field, ref value)| {
173                            field == expected_field && value == expected_value
174                        })
175                    })
176                    .unwrap_or(false)
177            }
178            Matcher::Any => true,
179            Matcher::AnyOf(ref matchers) => matchers.iter().any(|m| m.matches_value(other)),
180            Matcher::AllOf(ref matchers) => matchers.iter().all(|m| m.matches_value(other)),
181            Matcher::Missing => other.is_empty(),
182        }
183    }
184}
185
186#[derive(Clone, PartialEq, Debug)]
187pub(crate) enum PathAndQueryMatcher {
188    Unified(Matcher),
189    Split(Box<Matcher>, Box<Matcher>),
190}
191
192impl PathAndQueryMatcher {
193    pub(crate) fn matches_value(&self, other: &str) -> bool {
194        match self {
195            PathAndQueryMatcher::Unified(matcher) => matcher.matches_value(other),
196            PathAndQueryMatcher::Split(ref path_matcher, ref query_matcher) => {
197                let mut parts = other.splitn(2, '?');
198                let path = parts.next().unwrap();
199                let query = parts.next().unwrap_or("");
200
201                path_matcher.matches_value(path) && query_matcher.matches_value(query)
202            }
203        }
204    }
205}
206
207impl fmt::Display for PathAndQueryMatcher {
208    #[allow(deprecated)]
209    #[allow(clippy::write_with_newline)]
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            PathAndQueryMatcher::Unified(matcher) => write!(f, "{}\r\n", &matcher),
213            PathAndQueryMatcher::Split(path, query) => write!(f, "{}?{}\r\n", &path, &query),
214        }
215    }
216}
217
218///
219/// Represents a binary object the body should be matched against
220///
221#[derive(Debug, Clone)]
222pub struct BinaryBody {
223    path: Option<String>,
224    content: Vec<u8>,
225}
226
227impl BinaryBody {
228    /// Read the content from path and initialize a `BinaryBody`
229    ///
230    /// # Errors
231    ///
232    /// The same resulting from a failed `std::fs::read`.
233    pub fn from_path(path: &Path) -> Result<Self, io::Error> {
234        Ok(Self {
235            path: path.to_str().map(ToString::to_string),
236            content: std::fs::read(path)?,
237        })
238    }
239
240    /// Read the content from a &mut File and initialize a `BinaryBody`
241    pub fn from_file(file: &mut File) -> Self {
242        Self {
243            path: None,
244            content: get_content_from(file),
245        }
246    }
247
248    /// Instantiate the matcher directly passing the content
249    #[allow(clippy::missing_const_for_fn)]
250    pub fn from_bytes(content: Vec<u8>) -> Self {
251        Self {
252            path: None,
253            content,
254        }
255    }
256}
257
258fn get_content_from(file: &mut File) -> Vec<u8> {
259    let mut filecontent: Vec<u8> = Vec::new();
260    file.read_to_end(&mut filecontent).unwrap();
261    filecontent
262}
263
264impl PartialEq for BinaryBody {
265    fn eq(&self, other: &Self) -> bool {
266        match (self.path.as_ref(), other.path.as_ref()) {
267            (Some(p), Some(o)) => p == o,
268            _ => self.content == other.content,
269        }
270    }
271}
272
273impl fmt::Display for BinaryBody {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        if let Some(filepath) = self.path.as_ref() {
276            write!(f, "filepath: {}", filepath)
277        } else {
278            let len: usize = std::cmp::min(self.content.len(), 8);
279            let first_bytes: Vec<u8> = self.content.iter().copied().take(len).collect();
280            write!(f, "filecontent: {:?}", first_bytes)
281        }
282    }
283}
284
285#[derive(Clone)]
286pub(crate) struct RequestMatcher(Arc<dyn Fn(&Request) -> bool + Send + Sync>);
287
288impl RequestMatcher {
289    pub(crate) fn matches(&self, value: &Request) -> bool {
290        self.0(value)
291    }
292}
293
294impl<F> From<F> for RequestMatcher
295where
296    F: Fn(&Request) -> bool + Send + Sync + 'static,
297{
298    fn from(value: F) -> Self {
299        Self(Arc::new(value))
300    }
301}
302
303impl Default for RequestMatcher {
304    fn default() -> Self {
305        RequestMatcher(Arc::new(|_| true))
306    }
307}
308
309impl fmt::Debug for RequestMatcher {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        write!(f, "(RequestMatcher)")
312    }
313}