1use serde_json::Value;
7
8use crate::types::{
9 ArgMatcher, FieldCondition, MatchOp, MatchResult, PathSegment, Specificity, ToolCallPattern,
10 ToolMatcher,
11};
12
13#[must_use]
15pub fn pattern_matches(pattern: &ToolCallPattern, tool_id: &str, tool_args: &Value) -> MatchResult {
16 let tool_kind = match &pattern.tool {
18 ToolMatcher::Exact(name) => {
19 if name != tool_id {
20 return MatchResult::NoMatch;
21 }
22 3
23 }
24 ToolMatcher::Glob(pat) => {
25 if !wildcard_match(pat, tool_id) {
26 return MatchResult::NoMatch;
27 }
28 2
29 }
30 ToolMatcher::Regex(re) => {
31 if !re.is_match(tool_id) {
32 return MatchResult::NoMatch;
33 }
34 1
35 }
36 };
37
38 match &pattern.args {
40 ArgMatcher::Any => MatchResult::Match {
41 specificity: Specificity {
42 tool_kind,
43 has_args: false,
44 field_count: 0,
45 field_precision: 0,
46 },
47 },
48 ArgMatcher::Primary { op, value } => {
49 let primary_value = infer_primary_value(tool_args);
50 if evaluate_op(op, value, &primary_value) {
51 MatchResult::Match {
52 specificity: Specificity {
53 tool_kind,
54 has_args: true,
55 field_count: 1,
56 field_precision: op_precision(op),
57 },
58 }
59 } else {
60 MatchResult::NoMatch
61 }
62 }
63 ArgMatcher::Fields(conditions) => {
64 let mut field_precision = 0u8;
65 for cond in conditions {
66 if !evaluate_field_condition(cond, tool_args) {
67 return MatchResult::NoMatch;
68 }
69 field_precision = field_precision.saturating_add(op_precision(&cond.op));
70 }
71 MatchResult::Match {
72 specificity: Specificity {
73 tool_kind,
74 has_args: true,
75 field_count: conditions.len().min(255) as u8,
76 field_precision,
77 },
78 }
79 }
80 }
81}
82
83#[must_use]
89pub fn resolve_path<'a>(value: &'a Value, path: &[PathSegment]) -> Vec<&'a Value> {
90 if path.is_empty() {
91 return vec![value];
92 }
93
94 let Some((head, tail)) = path.split_first() else {
95 return vec![];
96 };
97
98 match head {
99 PathSegment::Field(name) => match value.get(name.as_str()) {
100 Some(child) => resolve_path(child, tail),
101 None => vec![],
102 },
103 PathSegment::Index(i) => match value.as_array().and_then(|arr| arr.get(*i)) {
104 Some(child) => resolve_path(child, tail),
105 None => vec![],
106 },
107 PathSegment::AnyIndex => match value.as_array() {
108 Some(arr) => arr
109 .iter()
110 .flat_map(|elem| resolve_path(elem, tail))
111 .collect(),
112 None => vec![],
113 },
114 PathSegment::Wildcard => match value.as_object() {
115 Some(obj) => obj.values().flat_map(|v| resolve_path(v, tail)).collect(),
116 None => vec![],
117 },
118 }
119}
120
121fn infer_primary_value(args: &Value) -> String {
126 if let Some(obj) = args.as_object()
127 && obj.len() == 1
128 && let Some(v) = obj.values().next()
129 {
130 return value_to_string(v);
131 }
132 value_to_string(args)
133}
134
135pub fn value_to_string(v: &Value) -> String {
137 match v {
138 Value::String(s) => s.clone(),
139 Value::Null => String::new(),
140 Value::Bool(b) => b.to_string(),
141 Value::Number(n) => n.to_string(),
142 other => other.to_string(),
143 }
144}
145
146#[must_use]
155pub fn evaluate_field_condition(cond: &FieldCondition, args: &Value) -> bool {
156 let resolved = resolve_path(args, &cond.path);
157 if resolved.is_empty() {
158 return false;
159 }
160 resolved
162 .iter()
163 .any(|v| evaluate_op(&cond.op, &cond.value, &value_to_string(v)))
164}
165
166#[must_use]
168pub fn evaluate_op(op: &MatchOp, pattern: &str, value: &str) -> bool {
169 match op {
170 MatchOp::Glob => wildcard_match(pattern, value),
171 MatchOp::Exact => pattern == value,
172 MatchOp::Regex => regex::Regex::new(pattern)
173 .map(|re| re.is_match(value))
174 .unwrap_or(false),
175 MatchOp::NotGlob => !wildcard_match(pattern, value),
176 MatchOp::NotExact => pattern != value,
177 MatchOp::NotRegex => regex::Regex::new(pattern)
178 .map(|re| !re.is_match(value))
179 .unwrap_or(true),
180 }
181}
182
183#[must_use]
185pub fn wildcard_match(pattern: &str, value: &str) -> bool {
186 let normalized = normalize_wildcards(pattern);
187 glob_match::glob_match(&normalized, value)
188}
189
190fn normalize_wildcards(pattern: &str) -> String {
192 let bytes = pattern.as_bytes();
193 let len = bytes.len();
194 let mut result = String::with_capacity(len + 8);
195 let mut i = 0;
196 while i < len {
197 if bytes[i] == b'*' {
198 let start = i;
199 while i < len && bytes[i] == b'*' {
200 i += 1;
201 }
202 let count = i - start;
203 if count >= 2 {
204 for _ in 0..count {
205 result.push('*');
206 }
207 } else {
208 let preceded_by_globstar = start >= 3 && &bytes[start - 3..start] == b"**/";
209 let followed_by_globstar =
210 i + 2 < len && &bytes[i..i + 2] == b"/*" && bytes[i + 2] == b'*';
211
212 if preceded_by_globstar || followed_by_globstar {
213 result.push('*');
214 } else {
215 result.push_str("**");
216 }
217 }
218 } else {
219 result.push(bytes[i] as char);
220 i += 1;
221 }
222 }
223 result
224}
225
226#[must_use]
232pub fn validate_pattern_fields(
233 pattern: &ToolCallPattern,
234 parameters_schema: &Value,
235) -> Vec<String> {
236 let conditions = match &pattern.args {
237 ArgMatcher::Any | ArgMatcher::Primary { .. } => return vec![],
238 ArgMatcher::Fields(conditions) => conditions,
239 };
240
241 let mut warnings = Vec::new();
242 for cond in conditions {
243 if !schema_has_path(parameters_schema, &cond.path) {
244 let path_str = cond
245 .path
246 .iter()
247 .map(|s| s.to_string())
248 .collect::<Vec<_>>()
249 .join(".");
250 warnings.push(path_str);
251 }
252 }
253 warnings
254}
255
256#[must_use]
258pub fn schema_has_path(schema: &Value, path: &[PathSegment]) -> bool {
259 if path.is_empty() {
260 return true;
261 }
262
263 let Some((head, tail)) = path.split_first() else {
264 return true;
265 };
266 match head {
267 PathSegment::Field(name) => {
268 let prop = schema.get("properties").and_then(|p| p.get(name.as_str()));
269 match prop {
270 Some(sub_schema) => schema_has_path(sub_schema, tail),
271 None => schema
272 .get("additionalProperties")
273 .is_some_and(|ap| ap.is_object() && schema_has_path(ap, tail)),
274 }
275 }
276 PathSegment::Index(_) | PathSegment::AnyIndex => schema
277 .get("items")
278 .is_some_and(|items| schema_has_path(items, tail)),
279 PathSegment::Wildcard => {
280 if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
281 props.values().any(|sub| schema_has_path(sub, tail))
282 } else {
283 schema
284 .get("additionalProperties")
285 .is_some_and(|ap| ap.is_object() && schema_has_path(ap, tail))
286 }
287 }
288 }
289}
290
291#[must_use]
293pub fn op_precision(op: &MatchOp) -> u8 {
294 match op {
295 MatchOp::Exact | MatchOp::NotExact => 3,
296 MatchOp::Glob | MatchOp::NotGlob => 2,
297 MatchOp::Regex | MatchOp::NotRegex => 1,
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use serde_json::json;
305
306 fn exact(name: &str) -> ToolCallPattern {
307 ToolCallPattern::tool(name)
308 }
309
310 fn primary(name: &str, pat: &str) -> ToolCallPattern {
311 ToolCallPattern::tool_with_primary(name, pat)
312 }
313
314 fn field_rule(name: &str, field: &str, op: MatchOp, value: &str) -> ToolCallPattern {
315 ToolCallPattern {
316 tool: ToolMatcher::Exact(name.into()),
317 args: ArgMatcher::Fields(vec![FieldCondition {
318 path: vec![PathSegment::Field(field.into())],
319 op,
320 value: value.into(),
321 }]),
322 }
323 }
324
325 #[test]
328 fn exact_tool_matches() {
329 assert!(pattern_matches(&exact("Bash"), "Bash", &json!({})).is_match());
330 }
331
332 #[test]
333 fn exact_tool_no_match() {
334 assert!(!pattern_matches(&exact("Bash"), "Read", &json!({})).is_match());
335 }
336
337 #[test]
338 fn glob_tool_matches() {
339 let p = ToolCallPattern::tool_glob("mcp__github__*");
340 assert!(pattern_matches(&p, "mcp__github__create_issue", &json!({})).is_match());
341 assert!(pattern_matches(&p, "mcp__github__list_repos", &json!({})).is_match());
342 assert!(!pattern_matches(&p, "mcp__slack__post", &json!({})).is_match());
343 }
344
345 #[test]
346 fn regex_tool_matches() {
347 let p = ToolCallPattern {
348 tool: ToolMatcher::Regex(regex::Regex::new(r"mcp__(github|gitlab)__.*").unwrap()),
349 args: ArgMatcher::Any,
350 };
351 assert!(pattern_matches(&p, "mcp__github__create_issue", &json!({})).is_match());
352 assert!(pattern_matches(&p, "mcp__gitlab__merge", &json!({})).is_match());
353 assert!(!pattern_matches(&p, "mcp__slack__post", &json!({})).is_match());
354 }
355
356 #[test]
359 fn primary_glob_matches() {
360 let p = primary("Bash", "npm *");
361 assert!(pattern_matches(&p, "Bash", &json!({"command": "npm install"})).is_match());
362 assert!(!pattern_matches(&p, "Bash", &json!({"command": "git status"})).is_match());
363 }
364
365 #[test]
366 fn primary_glob_multi_key_uses_stringify() {
367 let p = primary("Bash", "*npm*");
368 assert!(pattern_matches(&p, "Bash", &json!({"a": "npm", "b": "x"})).is_match());
369 let p2 = primary("Bash", "*cargo*");
370 assert!(!pattern_matches(&p2, "Bash", &json!({"a": "npm", "b": "x"})).is_match());
371 }
372
373 #[test]
376 fn named_field_glob() {
377 let p = field_rule("Edit", "file_path", MatchOp::Glob, "src/**/*.rs");
378 assert!(pattern_matches(&p, "Edit", &json!({"file_path": "src/main.rs"})).is_match());
379 assert!(pattern_matches(&p, "Edit", &json!({"file_path": "src/sub/lib.rs"})).is_match());
380 assert!(!pattern_matches(&p, "Edit", &json!({"file_path": "tests/test.rs"})).is_match());
381 }
382
383 #[test]
384 fn named_field_exact() {
385 let p = field_rule("Bash", "command", MatchOp::Exact, "ls");
386 assert!(pattern_matches(&p, "Bash", &json!({"command": "ls"})).is_match());
387 assert!(!pattern_matches(&p, "Bash", &json!({"command": "ls -la"})).is_match());
388 }
389
390 #[test]
391 fn named_field_regex() {
392 let p = field_rule("Bash", "command", MatchOp::Regex, "(?i)eval|exec");
393 assert!(pattern_matches(&p, "Bash", &json!({"command": "eval foo"})).is_match());
394 assert!(pattern_matches(&p, "Bash", &json!({"command": "EXEC bar"})).is_match());
395 assert!(!pattern_matches(&p, "Bash", &json!({"command": "npm install"})).is_match());
396 }
397
398 #[test]
399 fn named_field_not_glob() {
400 let p = field_rule("Bash", "command", MatchOp::NotGlob, "rm *");
401 assert!(!pattern_matches(&p, "Bash", &json!({"command": "rm -rf /"})).is_match());
402 assert!(pattern_matches(&p, "Bash", &json!({"command": "ls"})).is_match());
403 }
404
405 #[test]
406 fn missing_field_positive_op_no_match() {
407 let p = field_rule("Bash", "command", MatchOp::Glob, "npm *");
408 assert!(!pattern_matches(&p, "Bash", &json!({})).is_match());
409 }
410
411 #[test]
412 fn missing_field_negative_op_no_match() {
413 let p = field_rule("Bash", "command", MatchOp::NotGlob, "rm *");
414 assert!(!pattern_matches(&p, "Bash", &json!({})).is_match());
415 }
416
417 #[test]
420 fn nested_path_dot_notation() {
421 let p = ToolCallPattern {
422 tool: ToolMatcher::Exact("Tool".into()),
423 args: ArgMatcher::Fields(vec![FieldCondition {
424 path: vec![
425 PathSegment::Field("config".into()),
426 PathSegment::Field("host".into()),
427 ],
428 op: MatchOp::Exact,
429 value: "localhost".into(),
430 }]),
431 };
432 assert!(pattern_matches(&p, "Tool", &json!({"config": {"host": "localhost"}})).is_match());
433 assert!(!pattern_matches(&p, "Tool", &json!({"config": {"host": "prod"}})).is_match());
434 }
435
436 #[test]
437 fn nested_path_any_index() {
438 let p = ToolCallPattern {
439 tool: ToolMatcher::Exact("Tool".into()),
440 args: ArgMatcher::Fields(vec![FieldCondition {
441 path: vec![
442 PathSegment::Field("items".into()),
443 PathSegment::AnyIndex,
444 PathSegment::Field("name".into()),
445 ],
446 op: MatchOp::Exact,
447 value: "target".into(),
448 }]),
449 };
450 assert!(
451 pattern_matches(
452 &p,
453 "Tool",
454 &json!({"items": [{"name": "other"}, {"name": "target"}]})
455 )
456 .is_match()
457 );
458 assert!(
459 !pattern_matches(
460 &p,
461 "Tool",
462 &json!({"items": [{"name": "a"}, {"name": "b"}]})
463 )
464 .is_match()
465 );
466 }
467
468 #[test]
469 fn specificity_exact_tool_higher_than_glob() {
470 let exact_result = pattern_matches(&exact("Bash"), "Bash", &json!({}));
471 let glob_result = pattern_matches(&ToolCallPattern::tool_glob("Bas*"), "Bash", &json!({}));
472 if let (MatchResult::Match { specificity: a }, MatchResult::Match { specificity: b }) =
473 (&exact_result, &glob_result)
474 {
475 assert!(a > b);
476 } else {
477 panic!("both should match");
478 }
479 }
480
481 #[test]
482 fn specificity_with_args_higher_than_without() {
483 let no_args = pattern_matches(&exact("Bash"), "Bash", &json!({"command": "npm install"}));
484 let with_args = pattern_matches(
485 &primary("Bash", "npm *"),
486 "Bash",
487 &json!({"command": "npm install"}),
488 );
489 if let (MatchResult::Match { specificity: a }, MatchResult::Match { specificity: b }) =
490 (&no_args, &with_args)
491 {
492 assert!(b > a);
493 } else {
494 panic!("both should match");
495 }
496 }
497
498 #[test]
499 fn wildcard_match_crosses_slashes() {
500 assert!(wildcard_match("rm *", "rm -rf /"));
501 assert!(wildcard_match("curl *", "curl https://example.com"));
502 assert!(wildcard_match("npm *", "npm install"));
503 assert!(wildcard_match("mcp__*", "mcp__github__create"));
504 assert!(wildcard_match("rm **", "rm -rf /"));
505 assert!(wildcard_match("src/**/*.rs", "src/main.rs"));
506 assert!(wildcard_match("src/**/*.rs", "src/sub/lib.rs"));
507 assert!(!wildcard_match("src/**/*.rs", "tests/test.rs"));
508 }
509
510 #[test]
511 fn validate_pattern_fields_ok() {
512 let schema = json!({
513 "type": "object",
514 "properties": {
515 "command": { "type": "string" }
516 }
517 });
518 let p = field_rule("Bash", "command", MatchOp::Glob, "npm *");
519 assert!(validate_pattern_fields(&p, &schema).is_empty());
520 }
521
522 #[test]
523 fn validate_pattern_fields_missing() {
524 let schema = json!({
525 "type": "object",
526 "properties": {
527 "file_path": { "type": "string" }
528 }
529 });
530 let p = field_rule("Edit", "command", MatchOp::Glob, "npm *");
531 let warnings = validate_pattern_fields(&p, &schema);
532 assert_eq!(warnings, vec!["command"]);
533 }
534
535 #[test]
538 fn value_to_string_variants() {
539 assert_eq!(value_to_string(&json!("hello")), "hello");
540 assert_eq!(value_to_string(&json!(null)), "");
541 assert_eq!(value_to_string(&json!(true)), "true");
542 assert_eq!(value_to_string(&json!(false)), "false");
543 assert_eq!(value_to_string(&json!(42)), "42");
544 assert_eq!(value_to_string(&json!(2.5)), "2.5");
545 assert_eq!(value_to_string(&json!([1, 2])), "[1,2]");
547 assert_eq!(value_to_string(&json!({"a": 1})), "{\"a\":1}");
548 }
549
550 #[test]
553 fn evaluate_op_not_exact() {
554 assert!(evaluate_op(&MatchOp::NotExact, "a", "b"));
555 assert!(!evaluate_op(&MatchOp::NotExact, "a", "a"));
556 }
557
558 #[test]
559 fn evaluate_op_not_regex() {
560 assert!(evaluate_op(&MatchOp::NotRegex, "^rm", "ls"));
561 assert!(!evaluate_op(&MatchOp::NotRegex, "^rm", "rm -rf"));
562 }
563
564 #[test]
565 fn evaluate_op_invalid_regex_returns_false() {
566 assert!(!evaluate_op(&MatchOp::Regex, "[invalid", "anything"));
568 }
569
570 #[test]
571 fn evaluate_op_invalid_not_regex_returns_true() {
572 assert!(evaluate_op(&MatchOp::NotRegex, "[invalid", "anything"));
574 }
575
576 #[test]
579 fn resolve_path_specific_index() {
580 let val = json!({"items": ["a", "b", "c"]});
581 let path = vec![PathSegment::Field("items".into()), PathSegment::Index(1)];
582 let resolved = resolve_path(&val, &path);
583 assert_eq!(resolved, vec![&json!("b")]);
584 }
585
586 #[test]
587 fn resolve_path_index_out_of_bounds() {
588 let val = json!({"items": ["a"]});
589 let path = vec![PathSegment::Field("items".into()), PathSegment::Index(99)];
590 assert!(resolve_path(&val, &path).is_empty());
591 }
592
593 #[test]
594 fn resolve_path_index_on_non_array() {
595 let val = json!({"items": "not_array"});
596 let path = vec![PathSegment::Field("items".into()), PathSegment::Index(0)];
597 assert!(resolve_path(&val, &path).is_empty());
598 }
599
600 #[test]
601 fn resolve_path_any_index_on_non_array() {
602 let val = json!({"items": "not_array"});
603 let path = vec![PathSegment::Field("items".into()), PathSegment::AnyIndex];
604 assert!(resolve_path(&val, &path).is_empty());
605 }
606
607 #[test]
608 fn resolve_path_wildcard() {
609 let val = json!({"a": {"x": 1}, "b": {"x": 2}});
610 let path = vec![PathSegment::Wildcard, PathSegment::Field("x".into())];
611 let resolved = resolve_path(&val, &path);
612 assert_eq!(resolved.len(), 2);
613 }
614
615 #[test]
616 fn resolve_path_wildcard_on_non_object() {
617 let val = json!("string");
618 let path = vec![PathSegment::Wildcard];
619 assert!(resolve_path(&val, &path).is_empty());
620 }
621
622 #[test]
623 fn resolve_path_empty() {
624 let val = json!({"a": 1});
625 let resolved = resolve_path(&val, &[]);
626 assert_eq!(resolved, vec![&json!({"a": 1})]);
627 }
628
629 #[test]
630 fn resolve_path_missing_field() {
631 let val = json!({"a": 1});
632 let path = vec![PathSegment::Field("b".into())];
633 assert!(resolve_path(&val, &path).is_empty());
634 }
635
636 #[test]
639 fn normalize_single_star_adjacent_to_globstar() {
640 assert!(wildcard_match("*/**/*.rs", "src/sub/lib.rs"));
642 }
643
644 #[test]
645 fn normalize_preserves_triple_stars() {
646 assert!(wildcard_match("***", "anything"));
648 }
649
650 #[test]
653 fn schema_has_path_additional_properties() {
654 let schema = json!({
655 "type": "object",
656 "additionalProperties": {
657 "type": "string"
658 }
659 });
660 assert!(schema_has_path(
661 &schema,
662 &[PathSegment::Field("anything".into())]
663 ));
664 }
665
666 #[test]
667 fn schema_has_path_additional_properties_false() {
668 let schema = json!({
670 "type": "object",
671 "additionalProperties": false
672 });
673 assert!(!schema_has_path(
674 &schema,
675 &[PathSegment::Field("missing".into())]
676 ));
677 }
678
679 #[test]
680 fn schema_has_path_array_items() {
681 let schema = json!({
682 "type": "object",
683 "properties": {
684 "list": {
685 "type": "array",
686 "items": {
687 "type": "object",
688 "properties": {
689 "name": { "type": "string" }
690 }
691 }
692 }
693 }
694 });
695 assert!(schema_has_path(
696 &schema,
697 &[
698 PathSegment::Field("list".into()),
699 PathSegment::AnyIndex,
700 PathSegment::Field("name".into()),
701 ]
702 ));
703 assert!(schema_has_path(
704 &schema,
705 &[
706 PathSegment::Field("list".into()),
707 PathSegment::Index(0),
708 PathSegment::Field("name".into()),
709 ]
710 ));
711 }
712
713 #[test]
714 fn schema_has_path_no_items() {
715 let schema = json!({
716 "type": "object",
717 "properties": {
718 "list": { "type": "string" }
719 }
720 });
721 assert!(!schema_has_path(
722 &schema,
723 &[PathSegment::Field("list".into()), PathSegment::AnyIndex,]
724 ));
725 }
726
727 #[test]
728 fn schema_has_path_wildcard() {
729 let schema = json!({
730 "type": "object",
731 "properties": {
732 "a": {
733 "type": "object",
734 "properties": {
735 "id": { "type": "string" }
736 }
737 },
738 "b": {
739 "type": "object",
740 "properties": {
741 "id": { "type": "string" }
742 }
743 }
744 }
745 });
746 assert!(schema_has_path(
747 &schema,
748 &[PathSegment::Wildcard, PathSegment::Field("id".into())]
749 ));
750 }
751
752 #[test]
753 fn schema_has_path_wildcard_no_properties() {
754 let schema = json!({
755 "type": "object",
756 "additionalProperties": {
757 "type": "object",
758 "properties": {
759 "name": { "type": "string" }
760 }
761 }
762 });
763 assert!(schema_has_path(
765 &schema,
766 &[PathSegment::Wildcard, PathSegment::Field("name".into())]
767 ));
768 }
769
770 #[test]
771 fn schema_has_path_wildcard_no_props_no_additional() {
772 let schema = json!({"type": "object"});
773 assert!(!schema_has_path(
774 &schema,
775 &[PathSegment::Wildcard, PathSegment::Field("x".into())]
776 ));
777 }
778
779 #[test]
782 fn validate_pattern_fields_any_args() {
783 let schema = json!({"type": "object"});
784 let p = exact("Bash");
785 assert!(validate_pattern_fields(&p, &schema).is_empty());
786 }
787
788 #[test]
789 fn validate_pattern_fields_primary_args() {
790 let schema = json!({"type": "object"});
791 let p = primary("Bash", "npm *");
792 assert!(validate_pattern_fields(&p, &schema).is_empty());
793 }
794
795 #[test]
798 fn op_precision_values() {
799 assert_eq!(op_precision(&MatchOp::Exact), 3);
800 assert_eq!(op_precision(&MatchOp::NotExact), 3);
801 assert_eq!(op_precision(&MatchOp::Glob), 2);
802 assert_eq!(op_precision(&MatchOp::NotGlob), 2);
803 assert_eq!(op_precision(&MatchOp::Regex), 1);
804 assert_eq!(op_precision(&MatchOp::NotRegex), 1);
805 }
806
807 #[test]
810 fn multiple_field_conditions_all_must_match() {
811 let p = ToolCallPattern {
812 tool: ToolMatcher::Exact("Tool".into()),
813 args: ArgMatcher::Fields(vec![
814 FieldCondition {
815 path: vec![PathSegment::Field("a".into())],
816 op: MatchOp::Exact,
817 value: "1".into(),
818 },
819 FieldCondition {
820 path: vec![PathSegment::Field("b".into())],
821 op: MatchOp::Exact,
822 value: "2".into(),
823 },
824 ]),
825 };
826 assert!(pattern_matches(&p, "Tool", &json!({"a": "1", "b": "2"})).is_match());
827 assert!(!pattern_matches(&p, "Tool", &json!({"a": "1", "b": "3"})).is_match());
828 assert!(!pattern_matches(&p, "Tool", &json!({"a": "1"})).is_match());
829 }
830
831 #[test]
834 fn named_field_not_exact() {
835 let p = field_rule("Bash", "command", MatchOp::NotExact, "rm");
836 assert!(pattern_matches(&p, "Bash", &json!({"command": "ls"})).is_match());
837 assert!(!pattern_matches(&p, "Bash", &json!({"command": "rm"})).is_match());
838 }
839
840 #[test]
841 fn named_field_not_regex() {
842 let p = field_rule("Bash", "command", MatchOp::NotRegex, "^rm");
843 assert!(pattern_matches(&p, "Bash", &json!({"command": "ls"})).is_match());
844 assert!(!pattern_matches(&p, "Bash", &json!({"command": "rm -rf"})).is_match());
845 }
846
847 #[test]
850 fn infer_primary_from_non_object() {
851 let p = primary("Tool", "*hello*");
852 assert!(pattern_matches(&p, "Tool", &json!("hello world")).is_match());
853 }
854
855 #[test]
856 fn infer_primary_from_multi_key_object() {
857 let p = primary("Tool", "*a*b*");
858 assert!(pattern_matches(&p, "Tool", &json!({"a": 1, "b": 2})).is_match());
860 }
861}