ws_mock/
matchers.rs

1/// A common [`Matcher`] trait and useful implementations for matching on JSON data.
2use serde_json::{Map, Value};
3use std::fmt::Debug;
4use std::mem::discriminant;
5
6/// An implementable trait accepted by [`WsMock`], allowing extension for arbitrary matching.
7///
8/// Users of this crate can implement any logic required for matching, so long as the implementation
9/// is Send + Sync for Tokio async and thread safety.
10///
11/// [`WsMock`]: crate::ws_mock_server::WsMock
12pub trait Matcher: Send + Sync + Debug {
13    fn matches(&self, text: &str) -> bool;
14}
15
16/// Matches on every message it sees. This will rarely be used in combination
17/// with other matchers, since it will respond to all messages.
18#[derive(Debug)]
19pub struct Any {}
20
21impl Any {
22    pub fn new() -> Any {
23        Any {}
24    }
25}
26
27impl Default for Any {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl Matcher for Any {
34    fn matches(&self, _: &str) -> bool {
35        true
36    }
37}
38
39/// Matches on arbitrary logic provided by a closure.
40///
41/// For anything you can fit in an `fn(&str) -> bool` closure, this is a great option to avoid
42/// having to create your own custom matcher for some on-the-fly matching that's needed.
43///
44/// Several provided matchers like [`Any`], [`StringExact`], and [`StringContains`] are easily
45/// expressed as closures for [`AnyThat`], but are provided explicitly for clarity and convenience.
46///
47/// # Example: Matching on Any i64-Parseable Message
48/// ```
49/// use std::str::FromStr;
50/// use ws_mock::matchers::{AnyThat, Matcher};
51///
52/// let matcher = AnyThat::new(|text| i64::from_str(text).is_ok());
53/// let matching_message = "42";
54/// let non_number_message = "...";
55/// let non_matching_message = "42.000001";
56///
57/// assert!(matcher.matches(matching_message));
58/// assert!(!matcher.matches(non_number_message));
59/// assert!(!matcher.matches(non_matching_message));
60/// ```
61///
62/// # Example: Function Pointers
63/// Rust allows regular functions to be referred to via function pointers using the same [`fn`]
64/// syntax, allowing for more complex logic than you may want to express in an in-line closure.
65/// Merely for example, the same closure used above can be expressed as:
66/// ```
67/// use std::str::FromStr;
68/// use ws_mock::matchers::{AnyThat, Matcher};
69///
70/// fn parses_to_i64(text: &str) -> bool {
71///     i64::from_str(text).is_ok()
72/// }
73///
74/// let matcher = AnyThat::new(parses_to_i64);
75///
76/// let matching_message = "42";
77/// let non_matching_message = "42.000001";
78///
79/// assert!(matcher.matches(matching_message));
80/// assert!(!matcher.matches(non_matching_message));
81/// ```
82/// [`fn`]: https://doc.rust-lang.org/std/primitive.fn.html
83#[derive(Debug)]
84pub struct AnyThat {
85    f: fn(&str) -> bool,
86}
87
88impl AnyThat {
89    pub fn new(f: fn(&str) -> bool) -> AnyThat {
90        AnyThat { f }
91    }
92}
93
94impl Matcher for AnyThat {
95    fn matches(&self, text: &str) -> bool {
96        (self.f)(text)
97    }
98}
99
100/// Matches on any message containing a given string.
101///
102/// # Example: Matching Any Message Content
103/// ```
104/// use ws_mock::matchers::{Matcher, StringContains};
105///
106/// let matcher = StringContains::new("data");
107/// let matching_message = "anything with data in it";
108/// let non_matching_message = "anything but 'd-a-t-a'";
109///
110/// assert!(matcher.matches(matching_message));
111/// assert!(!matcher.matches(non_matching_message));
112/// ```
113#[derive(Debug)]
114pub struct StringContains<'a> {
115    string: &'a str,
116}
117
118impl<'a> StringContains<'a> {
119    pub fn new(string: &'a str) -> Self {
120        Self { string }
121    }
122}
123
124impl Matcher for StringContains<'_> {
125    fn matches(&self, text: &str) -> bool {
126        text.contains(self.string)
127    }
128}
129
130/// Matches on exact string messages.
131#[derive(Debug)]
132pub struct StringExact<'a> {
133    string: &'a str,
134}
135
136impl<'a> StringExact<'a> {
137    pub fn new(string: &'a str) -> Self {
138        Self { string }
139    }
140}
141
142impl Matcher for StringExact<'_> {
143    fn matches(&self, text: &str) -> bool {
144        text == self.string
145    }
146}
147
148/// Matches on exact JSON data. This will be most useful when the exact contents
149/// of a message are important for matching, and any failure to match should cause an error.
150///
151/// # Example
152/// ```
153/// use serde_json::json;
154/// use ws_mock::matchers::{JsonExact, Matcher};
155///
156/// let matching_data = r#"{ "data": 42 }"#;
157/// let non_matching_data = r#"{ "data": 0 }"#;
158///
159/// let expected = json!({"data": 42});
160///
161/// let matcher = JsonExact::new(expected);
162///
163/// assert!(matcher.matches(matching_data));
164/// assert!(!matcher.matches(non_matching_data));
165/// ```
166#[derive(Debug)]
167pub struct JsonExact {
168    json: Value,
169}
170
171impl JsonExact {
172    pub fn new(json: Value) -> Self {
173        JsonExact { json }
174    }
175}
176
177impl Matcher for JsonExact {
178    fn matches(&self, text: &str) -> bool {
179        if let Ok(json) = serde_json::from_str::<Value>(text) {
180            json == self.json
181        } else {
182            false
183        }
184    }
185}
186
187/// Matches on JSON patterns, useful for matching on all messages that have a
188/// certain field, or matching data of only some type.
189///
190/// [`JsonPartial`] takes a `serde_json` [`Value`], and will match on anything that exhibits the same
191/// structure and matches all primitives and arrays given by the pattern. Objects are matched recursively,
192/// meaning that any keys present in the pattern must be present and compare equally to those in the
193/// object being matched, but the object being matched can have other keys not present in the pattern.
194///
195/// # Example: Matching by Message Type
196/// Matching on only data with a particular field (without caring about additional data) can be useful
197/// for matching messages of a particular type, such as disregarding heartbeats or metadata to
198/// focus on data messages only.
199/// ```
200/// use serde_json::json;
201/// use ws_mock::matchers::{JsonExact, JsonPartial, Matcher};
202///
203/// let heartbeat = r#"{"type": "heartbeat"}"#;
204/// let data = r#"{"type": "data", "data": [0, 1, 0]}"#;
205/// let metadata = r#"{"type": "metadata", "data": "details"}"#;
206///
207/// let pattern = json!({"type": "data"});
208/// let matcher = JsonPartial::new(pattern);
209///
210/// assert!(!matcher.matches(heartbeat));
211/// assert!(matcher.matches(data));
212/// assert!(!matcher.matches(metadata));
213///
214///
215/// ```
216/// ['Value']: https://docs.rs/serde_json/latest/serde_json/value/enum.Value.html
217#[derive(Debug)]
218pub struct JsonPartial {
219    pattern: Value,
220}
221
222impl JsonPartial {
223    pub fn new(pattern: Value) -> Self {
224        JsonPartial { pattern }
225    }
226
227    fn match_json(data: &Value, pattern: &Value) -> bool {
228        if discriminant(data) == discriminant(pattern) {
229            match pattern {
230                Value::Null => data.is_null(),
231                Value::Bool(b) => *b == data.as_bool().unwrap(),
232                Value::Number(n) => Some(n) == data.as_number(),
233                Value::String(s) => Some(s.as_str()) == data.as_str(),
234                Value::Array(a) => Some(a) == data.as_array(),
235                Value::Object(o) => Self::match_object(data.as_object().unwrap(), o),
236            }
237        } else {
238            false
239        }
240    }
241
242    fn match_object(data: &Map<String, Value>, pattern: &Map<String, Value>) -> bool {
243        pattern.keys().all(|k| {
244            data.contains_key(k) && Self::match_json(data.get(k).unwrap(), pattern.get(k).unwrap())
245        })
246    }
247}
248
249impl Matcher for JsonPartial {
250    fn matches(&self, text: &str) -> bool {
251        if let Ok(json) = serde_json::from_str::<Value>(text) {
252            Self::match_json(&json, &self.pattern)
253        } else {
254            false
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use crate::matchers::{
262        Any, AnyThat, JsonExact, JsonPartial, Matcher, StringContains, StringExact,
263    };
264    use serde_json;
265    use serde_json::{json, Value};
266    use std::str::FromStr;
267
268    #[test]
269    fn any_matches_anything() {
270        // ::default() is same as ::new()
271        let matcher = Any::default();
272
273        assert!(matcher.matches(""));
274        assert!(matcher.matches("[42]"));
275        assert!(matcher.matches("AnyText"));
276    }
277
278    #[test]
279    fn string_contains() {
280        let matcher = StringContains::new("heartbeat");
281        let matching_message = "heartbeats keep websockets alive";
282        let non_matching_message = "typos don't match: heatbeat";
283
284        assert!(matcher.matches(matching_message));
285        assert!(!matcher.matches(non_matching_message));
286    }
287
288    #[test]
289    fn string_exact() {
290        let matcher = StringExact::new("typos");
291        let matching_message = "typos";
292        let non_matching_message = "typo";
293        let non_matching_message_2 = "typographical issue";
294
295        assert!(matcher.matches(matching_message));
296        assert!(!matcher.matches(non_matching_message));
297        assert!(!matcher.matches(non_matching_message_2));
298    }
299
300    #[test]
301    fn any_that() {
302        let matcher = AnyThat::new(|text| text.contains(' '));
303        let matching_message = "contains spaces";
304        let non_matching_message = "doesNotContainSpaces";
305
306        assert!(matcher.matches(matching_message));
307        assert!(!matcher.matches(non_matching_message));
308    }
309
310    #[test]
311    fn any_that_parses_to_i64() {
312        let matcher = AnyThat::new(|text| i64::from_str(text).is_ok());
313        let matching_message = "42";
314        let invalid_message = "...";
315        let non_matching_message = "42.000001";
316
317        assert!(matcher.matches(matching_message));
318        assert!(!matcher.matches(invalid_message));
319        assert!(!matcher.matches(non_matching_message));
320    }
321
322    #[test]
323    fn json_exact_matches_only_exact_json() {
324        let expected_json = json!(["A", "B"]);
325        let unexpected_json = json!(["A", "B", "Z"]);
326        let serialized_expected = serde_json::to_string(&expected_json).unwrap();
327        let serialized_unexpected = serde_json::to_string(&unexpected_json).unwrap();
328
329        let matcher = JsonExact::new(expected_json);
330
331        assert!(matcher.matches(serialized_expected.as_str()));
332        assert!(!matcher.matches(serialized_unexpected.as_str()));
333    }
334
335    #[test]
336    fn json_exact_does_not_match_on_invalid_data() {
337        let expected_json = json!({});
338        let matcher = JsonExact::new(expected_json);
339
340        assert!(!matcher.matches("-"));
341    }
342
343    #[test]
344    fn json_partial_does_not_match_on_invalid_data() {
345        let expected_json = json!({});
346        let matcher = JsonPartial::new(expected_json);
347
348        assert!(!matcher.matches("-"));
349    }
350
351    #[test]
352    fn json_partial_null() {
353        let data = json!(null);
354        let other = json!("someString");
355
356        assert_partial_matching_and_non_matching(&data, &other);
357    }
358
359    #[test]
360    fn json_partial_string() {
361        let data = json!("someString");
362        let other = json!("someOtherString");
363
364        assert_partial_matching_and_non_matching(&data, &other);
365    }
366
367    #[test]
368    fn json_partial_number() {
369        let data = json!(42);
370        let other = json!(17);
371
372        assert_partial_matching_and_non_matching(&data, &other);
373    }
374
375    #[test]
376    fn json_partial_bool() {
377        let data = json!(true);
378        let other = json!(false);
379
380        assert_partial_matching_and_non_matching(&data, &other);
381    }
382
383    #[test]
384    fn json_partial_arrays() {
385        let data = json!([1, 2, 3]);
386        let other = json!([1, 2, 3, 4]);
387
388        assert_partial_matching_and_non_matching(&data, &other);
389    }
390
391    #[test]
392    fn json_partial_object_matching() {
393        let data = json!({"a": 0, "b": [1, 2]});
394        let matching_pattern = json!({"a": 0});
395
396        let matcher = JsonPartial::new(matching_pattern.clone());
397
398        let serialized_exact = serde_json::to_string(&matching_pattern).unwrap();
399        let serialized_partial = serde_json::to_string(&data).unwrap();
400
401        assert!(matcher.matches(&serialized_exact));
402        assert!(matcher.matches(&serialized_partial));
403    }
404
405    #[test]
406    fn json_partial_object_non_matching() {
407        let data = json!({"a": 0, "b": [1, 2]});
408        let non_matching_pattern = json!({"a": 0, "c": 1});
409
410        let matcher = JsonPartial::new(non_matching_pattern.clone());
411
412        let serialized_exact = serde_json::to_string(&non_matching_pattern).unwrap();
413        let serialized_unexpected = serde_json::to_string(&data).unwrap();
414
415        assert!(matcher.matches(&serialized_exact));
416        assert!(!matcher.matches(&serialized_unexpected));
417    }
418
419    fn assert_partial_matching_and_non_matching(data: &Value, other: &Value) {
420        let matcher = JsonPartial::new(data.clone());
421
422        let serialized_expected = serde_json::to_string(&data).unwrap();
423        let serialized_unexpected = serde_json::to_string(&other).unwrap();
424
425        assert!(matcher.matches(&serialized_expected));
426        assert!(!matcher.matches(&serialized_unexpected));
427    }
428}