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#[derive(Clone, PartialEq, Debug)]
20#[allow(deprecated)] pub enum Matcher {
22 Exact(String),
25 Binary(BinaryBody),
27 Regex(String),
29 Json(serde_json::Value),
31 JsonString(String),
33 PartialJson(serde_json::Value),
35 PartialJsonString(String),
37 UrlEncoded(String, String),
40 AnyOf(Vec<Matcher>),
42 AllOf(Vec<Matcher>),
44 Any,
46 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 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 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#[derive(Debug, Clone)]
222pub struct BinaryBody {
223 path: Option<String>,
224 content: Vec<u8>,
225}
226
227impl BinaryBody {
228 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 pub fn from_file(file: &mut File) -> Self {
242 Self {
243 path: None,
244 content: get_content_from(file),
245 }
246 }
247
248 #[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}