1use std::collections::BTreeMap;
8
9#[derive(Debug, Clone, PartialEq)]
34#[non_exhaustive]
35pub enum CountBound {
36 Exact(u64),
38 AtLeast(u64),
40 AtMost(u64),
42 Range(u64, u64),
44}
45
46#[derive(Debug, Clone, PartialEq)]
49#[non_exhaustive]
50pub enum PathFilter {
51 Exact(String),
53 Contains(String),
55 Matches(String),
57}
58
59#[derive(Debug, Clone, PartialEq)]
62pub struct RequestExpectation {
63 pub bound: CountBound,
65 pub method: Option<String>,
67 pub path: Option<PathFilter>,
69 pub query: Option<BTreeMap<String, String>>,
73}
74
75#[derive(Debug, Clone, PartialEq)]
87#[non_exhaustive]
88pub enum Expectation {
89 Equals(serde_json::Value),
91 Regex(String),
93 Contains(String),
95 StartsWith(String),
97 EndsWith(String),
99 Exists,
101 JsonSubset(serde_json::Value),
103 Any,
106}
107
108#[derive(Debug, Clone, PartialEq)]
117pub struct RowsExpectation {
118 pub columns: Option<Vec<String>>,
121 pub unordered: bool,
124 pub rows: Option<Vec<Vec<Expectation>>>,
127 pub bound: Option<CountBound>,
130}
131
132pub fn bound_holds(bound: &CountBound, actual: usize) -> bool {
136 let actual = actual as u64;
137 match bound {
138 CountBound::Exact(n) => actual == *n,
139 CountBound::AtLeast(n) => actual >= *n,
140 CountBound::AtMost(n) => actual <= *n,
141 CountBound::Range(min, max) => actual >= *min && actual <= *max,
142 }
143}
144
145pub fn settles_early(bound: &CountBound, actual: usize) -> bool {
157 match bound {
158 CountBound::Exact(_) | CountBound::AtLeast(_) => bound_holds(bound, actual),
159 CountBound::AtMost(_) | CountBound::Range(..) => false,
160 }
161}
162
163pub fn above_ceiling(bound: &CountBound, actual: usize) -> bool {
168 let actual = actual as u64;
169 match bound {
170 CountBound::Exact(_) | CountBound::AtLeast(_) => false,
171 CountBound::AtMost(n) => actual > *n,
172 CountBound::Range(_, max) => actual > *max,
173 }
174}
175
176pub fn query_pairs(path_and_query: &str) -> Vec<(String, String)> {
181 match path_and_query.split_once('?') {
182 Some((_, query)) => form_urlencoded::parse(query.as_bytes())
183 .map(|(key, value)| (key.into_owned(), value.into_owned()))
184 .collect(),
185 None => Vec::new(),
186 }
187}
188
189pub fn matching_count<'a>(
203 requests: impl IntoIterator<Item = (&'a str, &'a str)>,
204 method: Option<&str>,
205 path_filter: Option<&PathFilter>,
206 query: Option<&BTreeMap<String, String>>,
207) -> usize {
208 let matches_regex = match path_filter {
212 Some(PathFilter::Matches(pattern)) => regex::Regex::new(pattern).ok(),
213 _ => None,
214 };
215 requests
216 .into_iter()
217 .filter(|(request_method, path)| {
218 let path_matches = match path_filter {
219 None => true,
220 Some(PathFilter::Exact(p)) => p.as_str() == *path,
221 Some(PathFilter::Contains(s)) => path.contains(s.as_str()),
222 Some(PathFilter::Matches(_)) => {
223 matches_regex.as_ref().is_some_and(|re| re.is_match(path))
224 }
225 };
226 let query_subset = query.is_none_or(|declared| {
227 let pairs = query_pairs(path);
228 declared
229 .iter()
230 .all(|(key, value)| pairs.iter().any(|(k, v)| k == key && v == value))
231 });
232 method.is_none_or(|m| m.eq_ignore_ascii_case(request_method))
233 && path_matches
234 && query_subset
235 })
236 .count()
237}
238
239pub fn render_bound(bound: &CountBound) -> String {
245 match bound {
246 CountBound::Exact(n) => format!("expected {n}"),
247 CountBound::AtLeast(n) => format!("expected at least {n}"),
248 CountBound::AtMost(n) => format!("expected at most {n}"),
249 CountBound::Range(min, max) => format!("expected between {min} and {max}"),
250 }
251}
252
253pub fn expectation_matches(expectation: &Expectation, value: &serde_json::Value) -> bool {
261 match expectation {
262 Expectation::Equals(expected) => value == expected,
263 Expectation::Regex(pattern) => {
264 regex::Regex::new(pattern).is_ok_and(|regex| regex.is_match(&stringify(value)))
265 }
266 Expectation::Contains(needle) => stringify(value).contains(needle),
267 Expectation::StartsWith(prefix) => stringify(value).starts_with(prefix),
268 Expectation::EndsWith(suffix) => stringify(value).ends_with(suffix),
269 Expectation::Exists => value != &serde_json::Value::Null,
270 Expectation::JsonSubset(pattern) => json_subset(pattern, value),
271 Expectation::Any => true,
272 }
273}
274
275pub fn rows_match(
285 expected: &[Vec<Expectation>],
286 actual: &[Vec<serde_json::Value>],
287 unordered: bool,
288) -> bool {
289 if expected.len() != actual.len() {
290 return false;
291 }
292 if !unordered {
293 return expected
294 .iter()
295 .zip(actual)
296 .all(|(pattern, row)| row_pattern_matches(pattern, row));
297 }
298 let compat: Vec<Vec<bool>> = expected
301 .iter()
302 .map(|pattern| {
303 actual
304 .iter()
305 .map(|row| row_pattern_matches(pattern, row))
306 .collect()
307 })
308 .collect();
309 let mut match_of_row: Vec<Option<usize>> = vec![None; actual.len()];
312 for pattern in 0..expected.len() {
313 let mut visited = vec![false; actual.len()];
314 if !try_augment(pattern, &compat, &mut match_of_row, &mut visited) {
315 return false;
316 }
317 }
318 true
319}
320
321fn row_pattern_matches(pattern: &[Expectation], row: &[serde_json::Value]) -> bool {
324 pattern.len() == row.len()
325 && pattern
326 .iter()
327 .zip(row)
328 .all(|(expectation, value)| expectation_matches(expectation, value))
329}
330
331fn try_augment(
336 pattern: usize,
337 compat: &[Vec<bool>],
338 match_of_row: &mut [Option<usize>],
339 visited: &mut [bool],
340) -> bool {
341 for (row, &compatible) in compat[pattern].iter().enumerate() {
342 if !compatible || visited[row] {
343 continue;
344 }
345 visited[row] = true;
346 match match_of_row[row] {
347 None => {
348 match_of_row[row] = Some(pattern);
349 return true;
350 }
351 Some(holder) => {
352 if try_augment(holder, compat, match_of_row, visited) {
353 match_of_row[row] = Some(pattern);
354 return true;
355 }
356 }
357 }
358 }
359 false
360}
361
362pub fn stringify(value: &serde_json::Value) -> String {
365 match value {
366 serde_json::Value::String(text) => text.clone(),
367 other => other.to_string(),
368 }
369}
370
371fn json_subset(pattern: &serde_json::Value, actual: &serde_json::Value) -> bool {
375 match (pattern, actual) {
376 (serde_json::Value::Object(pattern_object), serde_json::Value::Object(actual_object)) => {
377 pattern_object.iter().all(|(key, pattern_value)| {
378 actual_object
379 .get(key)
380 .is_some_and(|actual_value| json_subset(pattern_value, actual_value))
381 })
382 }
383 _ => pattern == actual,
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn bound_holds_covers_every_form_at_edges() {
393 let cases = [
394 (CountBound::Exact(2), [false, false, true, false, false]),
395 (CountBound::AtLeast(2), [false, false, true, true, true]),
396 (CountBound::AtMost(2), [true, true, true, false, false]),
397 (CountBound::Range(1, 3), [false, true, true, true, false]),
398 ];
399 for (bound, holds) in cases {
400 for (count, expected) in holds.into_iter().enumerate() {
401 assert_eq!(
402 bound_holds(&bound, count),
403 expected,
404 "{bound:?} at count {count}"
405 );
406 }
407 }
408 }
409
410 #[test]
411 fn settles_early_absence_claims_never_settle() {
412 for count in 0..=5usize {
413 assert!(
414 !settles_early(&CountBound::AtMost(5), count),
415 "AtMost(5) at count {count}"
416 );
417 assert!(
418 !settles_early(&CountBound::Range(0, 5), count),
419 "Range(0, 5) at count {count}"
420 );
421 assert_eq!(
422 settles_early(&CountBound::Exact(2), count),
423 count == 2,
424 "Exact(2) at count {count}"
425 );
426 assert_eq!(
427 settles_early(&CountBound::AtLeast(2), count),
428 count >= 2,
429 "AtLeast(2) at count {count}"
430 );
431 }
432 }
433
434 #[test]
435 fn above_ceiling_only_upper_breaches() {
436 assert!(above_ceiling(&CountBound::AtMost(2), 3));
437 assert!(above_ceiling(&CountBound::Range(1, 2), 3));
438 assert!(!above_ceiling(&CountBound::Exact(2), 99));
439 assert!(!above_ceiling(&CountBound::AtLeast(2), 99));
440 assert!(!above_ceiling(&CountBound::AtMost(2), 2));
441 }
442
443 #[test]
444 fn matching_count_query_subset_order_and_encoding_independent() {
445 let declared = BTreeMap::from([
446 ("a".to_string(), "1".to_string()),
447 ("b".to_string(), "2".to_string()),
448 ]);
449 let reordered_and_encoded = [("GET", "/x?b=2&a=1"), ("POST", "/x?a=%31&b=%32")];
450 assert_eq!(
451 matching_count(reordered_and_encoded, None, None, Some(&declared)),
452 2
453 );
454 assert_eq!(
455 matching_count([("GET", "/x?a=1")], None, None, Some(&declared)),
456 0
457 );
458
459 let only_a = BTreeMap::from([("a".to_string(), "1".to_string())]);
460 assert_eq!(
461 matching_count([("GET", "/x?a=1")], None, None, Some(&only_a)),
462 1
463 );
464 assert_eq!(
465 matching_count([("GET", "/x?b=2")], None, None, Some(&declared)),
466 0
467 );
468 }
469
470 #[test]
471 fn matching_count_invalid_regex_fails_closed() {
472 let filter = PathFilter::Matches("(".to_string());
473 assert_eq!(
474 matching_count([("GET", "/anything")], None, Some(&filter), None),
475 0
476 );
477 }
478
479 #[test]
480 fn matching_count_method_case_insensitive() {
481 let requests = [("POST", "/o"), ("GET", "/o")];
482 assert_eq!(matching_count(requests, Some("post"), None, None), 1);
483 }
484
485 #[test]
486 fn matching_count_path_forms() {
487 let requests = [("GET", "/o?a=1"), ("POST", "/o?a=1&x=2"), ("GET", "/diff")];
488 let exact = PathFilter::Exact("/o?a=1".to_string());
489 let contains = PathFilter::Contains("/o".to_string());
490 let matches = PathFilter::Matches("^/o".to_string());
491 assert_eq!(matching_count(requests, None, Some(&exact), None), 1);
492 assert_eq!(matching_count(requests, None, Some(&contains), None), 2);
493 assert_eq!(matching_count(requests, None, Some(&matches), None), 2);
494 }
495
496 #[test]
497 fn query_pairs_no_question_mark() {
498 assert!(query_pairs("/noquery").is_empty());
499 assert_eq!(
500 query_pairs("/q?a=1"),
501 vec![("a".to_string(), "1".to_string())]
502 );
503 }
504
505 #[test]
506 fn query_pairs_plus_decoding() {
507 assert_eq!(
508 query_pairs("/x?a=1+2"),
509 vec![("a".to_string(), "1 2".to_string())]
510 );
511 }
512
513 #[test]
514 fn expectation_matches_string_forms() {
515 let value = serde_json::json!("hello world");
516 assert!(expectation_matches(
517 &Expectation::Contains("world".to_string()),
518 &value
519 ));
520 assert!(expectation_matches(
521 &Expectation::StartsWith("hello".to_string()),
522 &value
523 ));
524 assert!(expectation_matches(
525 &Expectation::EndsWith("world".to_string()),
526 &value
527 ));
528 assert!(expectation_matches(&Expectation::Exists, &value));
529 assert!(expectation_matches(
530 &Expectation::Regex("^hello".to_string()),
531 &value
532 ));
533 assert!(expectation_matches(
534 &Expectation::Equals(serde_json::json!("hello world")),
535 &value
536 ));
537 assert!(!expectation_matches(
538 &Expectation::Contains("nope".to_string()),
539 &value
540 ));
541 }
542
543 #[test]
544 fn expectation_matches_object_forms() {
545 let value = serde_json::json!({"n": "café", "s": "hello world"});
546 assert!(expectation_matches(
547 &Expectation::Equals(serde_json::json!({"n": "café", "s": "hello world"})),
548 &value
549 ));
550 assert!(expectation_matches(
551 &Expectation::JsonSubset(serde_json::json!({"n": "café"})),
552 &value
553 ));
554 assert!(expectation_matches(
555 &Expectation::Regex("caf".to_string()),
556 &value
557 ));
558 assert!(expectation_matches(&Expectation::Exists, &value));
559 assert!(!expectation_matches(
560 &Expectation::JsonSubset(serde_json::json!({"n": "other"})),
561 &value
562 ));
563 assert!(!expectation_matches(
564 &Expectation::Exists,
565 &serde_json::Value::Null
566 ));
567 assert!(!expectation_matches(
568 &Expectation::Regex("(".to_string()),
569 &value
570 ));
571 }
572
573 #[test]
574 fn json_subset_recursive_objects() {
575 let actual = serde_json::json!({"user": {"name": "María", "role": "admin"}, "extra": 1});
576 assert!(expectation_matches(
577 &Expectation::JsonSubset(serde_json::json!({"user": {"name": "María"}})),
578 &actual
579 ));
580 assert!(!expectation_matches(
581 &Expectation::JsonSubset(serde_json::json!({"user": {"name": "other"}})),
582 &actual
583 ));
584 }
585
586 #[test]
587 fn any_matches_all_values_including_null() {
588 for value in [
589 serde_json::json!(null),
590 serde_json::json!(0),
591 serde_json::json!("x"),
592 serde_json::json!([1, 2]),
593 serde_json::json!({"k": "v"}),
594 ] {
595 assert!(
596 expectation_matches(&Expectation::Any, &value),
597 "Any vs {value}"
598 );
599 }
600 }
601
602 #[test]
603 fn any_distinct_from_exists() {
604 assert!(!expectation_matches(
605 &Expectation::Exists,
606 &serde_json::Value::Null
607 ));
608 assert!(expectation_matches(
609 &Expectation::Any,
610 &serde_json::Value::Null
611 ));
612 }
613
614 fn two_row_pattern() -> Vec<Vec<Expectation>> {
615 vec![
616 vec![
617 Expectation::Equals(serde_json::json!(1)),
618 Expectation::Contains("li".to_string()),
619 ],
620 vec![Expectation::Equals(serde_json::json!(2)), Expectation::Any],
621 ]
622 }
623
624 #[test]
625 fn rows_match_ordered_positional() {
626 let expected = two_row_pattern();
627 let actual = vec![
628 vec![serde_json::json!(1), serde_json::json!("alice")],
629 vec![serde_json::json!(2), serde_json::json!("bob")],
630 ];
631 assert!(rows_match(&expected, &actual, false));
632 let swapped = vec![actual[1].clone(), actual[0].clone()];
633 assert!(!rows_match(&expected, &swapped, false));
634 }
635
636 #[test]
637 fn rows_match_length_mismatch_fails() {
638 let expected = two_row_pattern();
639 let actual = vec![vec![serde_json::json!(1), serde_json::json!("alice")]];
640 assert!(!rows_match(&expected, &actual, false));
641 assert!(!rows_match(&expected, &actual, true));
642 }
643
644 #[test]
645 fn rows_match_unordered_reorder() {
646 let expected = two_row_pattern();
647 let actual = vec![
648 vec![serde_json::json!(2), serde_json::json!("bob")],
649 vec![serde_json::json!(1), serde_json::json!("alice")],
650 ];
651 assert!(rows_match(&expected, &actual, true));
652 }
653
654 #[test]
655 fn rows_match_unordered_duplicates() {
656 let expected = vec![
657 vec![Expectation::Equals(serde_json::json!(1))],
658 vec![Expectation::Equals(serde_json::json!(1))],
659 ];
660 let same = vec![vec![serde_json::json!(1)], vec![serde_json::json!(1)]];
661 assert!(rows_match(&expected, &same, true));
662 let mixed = vec![vec![serde_json::json!(1)], vec![serde_json::json!(2)]];
663 assert!(!rows_match(&expected, &mixed, true));
664 }
665
666 #[test]
667 fn rows_match_kuhn_needs_augmenting() {
668 let expected = vec![
669 vec![Expectation::Any, Expectation::Any],
670 vec![Expectation::Equals(serde_json::json!(1)), Expectation::Any],
671 ];
672 let actual = vec![
673 vec![serde_json::json!(1), serde_json::json!("x")],
674 vec![serde_json::json!(2), serde_json::json!("a")],
675 ];
676 assert!(rows_match(&expected, &actual, true));
680 assert!(!rows_match(&expected, &actual, false));
681 }
682
683 #[test]
684 fn rows_match_wildcard_including_null_cells() {
685 let expected = vec![vec![Expectation::Any]];
686 let actual = vec![vec![serde_json::Value::Null]];
687 assert!(rows_match(&expected, &actual, false));
688 assert!(rows_match(&expected, &actual, true));
689 }
690
691 #[test]
692 fn render_bound_forms() {
693 let bounds = [
694 CountBound::Exact(3),
695 CountBound::AtLeast(3),
696 CountBound::AtMost(2),
697 CountBound::Range(2, 4),
698 ];
699 let rendered: Vec<String> = bounds.iter().map(render_bound).collect();
700 for text in &rendered {
701 assert!(!text.is_empty(), "empty render for {text:?}");
702 }
703 for (index, left) in rendered.iter().enumerate() {
704 for right in &rendered[index + 1..] {
705 assert_ne!(left, right, "duplicate render `{left}`");
706 }
707 }
708 }
709}