108108//! There is a grammar for the definitions in [ANTLR4 format](https://github.com/pact-foundation/pact-plugins/blob/main/docs/matching-rule-definition.g4).
109109//!
110110
111+ use std:: collections:: HashMap ;
111112use std:: fmt:: { Display , Formatter } ;
112113use std:: char:: REPLACEMENT_CHARACTER ;
113114use std:: str:: from_utf8;
@@ -123,8 +124,9 @@ use tracing::{instrument, trace, warn};
123124use crate :: expression_parser:: DataType ;
124125use crate :: generators:: Generator ;
125126use crate :: generators:: Generator :: ProviderStateGenerator ;
126- use crate :: matchingrules:: MatchingRule ;
127+ use crate :: matchingrules:: { MatchingRule , MatchingRuleCategory , RuleLogic } ;
127128use crate :: matchingrules:: MatchingRule :: { MaxType , MinType , NotEmpty } ;
129+ use crate :: path_exp:: DocPath ;
128130
129131/// Type to associate with an expression element
130132#[ derive( Debug , Copy , Clone , PartialEq , Eq ) ]
@@ -288,6 +290,9 @@ enum MatcherDefinitionToken {
288290 #[ token( "atMost" ) ]
289291 AtMost ,
290292
293+ #[ token( "arrayContains" ) ]
294+ ArrayContains ,
295+
291296 #[ token( "(" ) ]
292297 LeftBracket ,
293298
@@ -352,7 +357,8 @@ pub fn is_matcher_def(v: &str) -> bool {
352357 if let Some ( Ok ( token) ) = next {
353358 if token == MatcherDefinitionToken :: Matching || token == MatcherDefinitionToken :: NotEmpty ||
354359 token == MatcherDefinitionToken :: EachKey || token == MatcherDefinitionToken :: EachValue ||
355- token == MatcherDefinitionToken :: AtLeast || token == MatcherDefinitionToken :: AtMost {
360+ token == MatcherDefinitionToken :: AtLeast || token == MatcherDefinitionToken :: AtMost ||
361+ token == MatcherDefinitionToken :: ArrayContains {
356362 true
357363 } else {
358364 false
@@ -378,10 +384,10 @@ fn matching_definition(lex: &mut Lexer<MatcherDefinitionToken>, v: &str) -> anyh
378384
379385 let remainder = lex. remainder ( ) ;
380386 if !remainder. is_empty ( ) {
381- Err ( anyhow ! ( "expected not more tokens, got '{}' with '{}' remaining" , lex. slice( ) , remainder) )
382- } else {
383- Ok ( value)
387+ return Err ( anyhow ! ( "expected not more tokens, got '{}' with '{}' remaining" , lex. slice( ) , remainder) ) ;
384388 }
389+
390+ Ok ( value)
385391}
386392
387393// matchingDefinitionExp returns [ MatchingRuleDefinition value ] :
@@ -449,14 +455,17 @@ fn matching_definition_exp(lex: &mut Lexer<MatcherDefinitionToken>, v: &str) ->
449455 generator : None ,
450456 expression : v. to_string ( )
451457 } )
458+ } else if token == & MatcherDefinitionToken :: ArrayContains {
459+ let definition = parse_array_contains ( lex, v) ?;
460+ Ok ( definition)
452461 } else {
453462 let mut buffer = BytesMut :: new ( ) . writer ( ) ;
454463 let span = lex. span ( ) ;
455464 let report = Report :: build ( ReportKind :: Error , ( "expression" , span. start ..span. start ) )
456465 . with_config ( Config :: default ( ) . with_color ( false ) )
457466 . with_message ( format ! ( "Expected a type of matching rule definition, but got '{}'" , lex. slice( ) ) )
458467 . with_label ( Label :: new ( ( "expression" , span) ) . with_message ( "Expected a matching rule definition here" ) )
459- . with_note ( "valid matching rule definitions are: matching, notEmpty, eachKey, eachValue, atLeast, atMost" )
468+ . with_note ( "valid matching rule definitions are: matching, notEmpty, eachKey, eachValue, atLeast, atMost, arrayContains " )
460469 . finish ( ) ;
461470 report. write ( ( "expression" , Source :: from ( v) ) , & mut buffer) ?;
462471 let message = from_utf8 ( & * buffer. get_ref ( ) ) ?. to_string ( ) ;
@@ -469,7 +478,7 @@ fn matching_definition_exp(lex: &mut Lexer<MatcherDefinitionToken>, v: &str) ->
469478 . with_config ( Config :: default ( ) . with_color ( false ) )
470479 . with_message ( format ! ( "Expected a type of matching rule definition but got the end of the expression" ) )
471480 . with_label ( Label :: new ( ( "expression" , span) ) . with_message ( "Expected a matching rule definition here" ) )
472- . with_note ( "valid matching rule definitions are: matching, notEmpty, eachKey, eachValue, atLeast, atMost" )
481+ . with_note ( "valid matching rule definitions are: matching, notEmpty, eachKey, eachValue, atLeast, atMost, arrayContains " )
473482 . finish ( ) ;
474483 report. write ( ( "expression" , Source :: from ( v) ) , & mut buffer) ?;
475484 let message = from_utf8 ( & * buffer. get_ref ( ) ) ?. to_string ( ) ;
@@ -513,6 +522,88 @@ fn parse_each_value(lex: &mut Lexer<MatcherDefinitionToken>, v: &str) -> anyhow:
513522 }
514523}
515524
525+ // LEFT_BRACKET matchingDefinitionExp ( COMMA matchingDefinitionExp )* RIGHT_BRACKET
526+ fn parse_array_contains ( lex : & mut Lexer < MatcherDefinitionToken > , v : & str ) -> anyhow:: Result < MatchingRuleDefinition > {
527+ let next = lex. next ( )
528+ . ok_or_else ( || end_of_expression ( v, "an opening bracket" ) ) ?;
529+ if let Ok ( MatcherDefinitionToken :: LeftBracket ) = next {
530+ let first = matching_definition_exp ( lex, v) ?;
531+ let mut inner_results = vec ! [ first] ;
532+
533+ // Parse comma-separated additional expressions
534+ loop {
535+ let next = lex. next ( ) . ok_or_else ( || end_of_expression ( v, "a closing bracket or comma" ) ) ?;
536+ match next {
537+ Ok ( MatcherDefinitionToken :: RightBracket ) => break ,
538+ Ok ( MatcherDefinitionToken :: Comma ) => {
539+ let next_exp = matching_definition_exp ( lex, v) ?;
540+ inner_results. push ( next_exp) ;
541+ }
542+ _ => {
543+ return Err ( anyhow ! ( error_message( lex, v, "Expected a closing bracket or comma" , "Expected a closing bracket or comma before this" ) ?) ) ;
544+ }
545+ }
546+ }
547+
548+ // Each inner expression becomes one entry in the rules vec:
549+ // - Inline matchers (Left): resolved into an ArrayContains variant
550+ // - References (Right): kept as-is for plugin-level resolution
551+ // Mixed is allowed: arrayContains(matching(equalTo, 'X'), matching($'ref'))
552+ let mut rules: Vec < Either < MatchingRule , MatchingReference > > = Vec :: new ( ) ;
553+ let mut inline_variants = Vec :: new ( ) ;
554+ let mut first_value = None ;
555+ let mut variant_index = 0usize ;
556+
557+ for result in inner_results {
558+ if first_value. is_none ( ) && !result. value . is_empty ( ) {
559+ first_value = Some ( result. value . clone ( ) ) ;
560+ }
561+ match result. rules . first ( ) {
562+ Some ( Either :: Left ( matching_rule) ) => {
563+ let mut category = MatchingRuleCategory :: empty ( "body" ) ;
564+ category. add_rule ( DocPath :: root ( ) , matching_rule. clone ( ) , RuleLogic :: And ) ;
565+ let generators: HashMap < DocPath , Generator > = result. generator
566+ . map ( |g| {
567+ let mut map = HashMap :: new ( ) ;
568+ map. insert ( DocPath :: root ( ) , g) ;
569+ map
570+ } )
571+ . unwrap_or_default ( ) ;
572+ inline_variants. push ( ( variant_index, category, generators) ) ;
573+ }
574+ Some ( Either :: Right ( reference) ) => {
575+ rules. push ( Either :: Right ( reference. clone ( ) ) ) ;
576+ }
577+ None => { }
578+ }
579+ variant_index += 1 ;
580+ }
581+
582+ if !inline_variants. is_empty ( ) {
583+ rules. insert ( 0 , Either :: Left ( MatchingRule :: ArrayContains ( inline_variants) ) ) ;
584+ }
585+
586+ Ok ( MatchingRuleDefinition {
587+ value : first_value. unwrap_or_default ( ) ,
588+ value_type : ValueType :: Unknown ,
589+ rules,
590+ generator : None ,
591+ expression : v. to_string ( ) ,
592+ } )
593+ } else {
594+ let mut buffer = BytesMut :: new ( ) . writer ( ) ;
595+ let span = lex. span ( ) ;
596+ let report = Report :: build ( ReportKind :: Error , ( "expression" , span. start ..span. start ) )
597+ . with_config ( Config :: default ( ) . with_color ( false ) )
598+ . with_message ( format ! ( "Expected an opening bracket, got '{}'" , lex. slice( ) ) )
599+ . with_label ( Label :: new ( ( "expression" , span) ) . with_message ( "Expected an opening bracket before this" ) )
600+ . finish ( ) ;
601+ report. write ( ( "expression" , Source :: from ( v) ) , & mut buffer) ?;
602+ let message = from_utf8 ( & * buffer. get_ref ( ) ) ?. to_string ( ) ;
603+ Err ( anyhow ! ( message) )
604+ }
605+ }
606+
516607fn error_message ( lex : & mut Lexer < MatcherDefinitionToken > , v : & str , error : & str , additional : & str ) -> Result < String , Error > {
517608 let mut buffer = BytesMut :: new ( ) . writer ( ) ;
518609 let span = lex. span ( ) ;
@@ -1128,6 +1219,7 @@ mod test {
11281219 use crate :: generators:: Generator :: { Date , DateTime , Time } ;
11291220 use crate :: matchingrules:: MatchingRule ;
11301221 use crate :: matchingrules:: MatchingRule :: { Regex , Type } ;
1222+ use crate :: path_exp:: DocPath ;
11311223
11321224 use super :: * ;
11331225
@@ -1587,7 +1679,7 @@ mod test {
15871679 | │ │\u{0020}
15881680 | │ ╰─ Expected a matching rule definition here
15891681 | │\u{0020}
1590- | │ Note: valid matching rule definitions are: matching, notEmpty, eachKey, eachValue, atLeast, atMost
1682+ | │ Note: valid matching rule definitions are: matching, notEmpty, eachKey, eachValue, atLeast, atMost, arrayContains
15911683 |───╯
15921684 |
15931685 " . trim_margin ( ) . unwrap ( ) ) ) ;
@@ -1602,7 +1694,7 @@ mod test {
16021694 | │ ──────┬────── \u{0020}
16031695 | │ ╰──────── Expected a matching rule definition here
16041696 | │\u{0020}
1605- | │ Note: valid matching rule definitions are: matching, notEmpty, eachKey, eachValue, atLeast, atMost
1697+ | │ Note: valid matching rule definitions are: matching, notEmpty, eachKey, eachValue, atLeast, atMost, arrayContains
16061698 |───╯
16071699 |
16081700 " . trim_margin ( ) . unwrap ( ) ) ) ;
@@ -2117,4 +2209,116 @@ mod test {
21172209 fn is_matcher_def_test ( #[ case] expression : & str , #[ case] expected : bool ) {
21182210 expect ! ( is_matcher_def( expression) ) . to ( be_equal_to ( expected) ) ;
21192211 }
2212+
2213+ #[ test]
2214+ fn is_matcher_def_array_contains ( ) {
2215+ assert ! ( is_matcher_def( "arrayContains(matching(equalTo, 'PUBLIC'))" ) ) ;
2216+ assert ! ( is_matcher_def( "arrayContains(matching(equalTo, 'A'), matching(equalTo, 'B'))" ) ) ;
2217+ assert ! ( !is_matcher_def( "notArrayContains" ) ) ;
2218+ }
2219+
2220+ #[ test]
2221+ fn parse_array_contains_single_variant ( ) {
2222+ let result = parse_matcher_def ( "arrayContains(matching(equalTo, 'PUBLIC'))" ) . unwrap ( ) ;
2223+ assert_eq ! ( result. rules. len( ) , 1 ) ;
2224+ match & result. rules [ 0 ] {
2225+ Either :: Left ( MatchingRule :: ArrayContains ( variants) ) => {
2226+ assert_eq ! ( variants. len( ) , 1 ) ;
2227+ let ( index, rules, _generators) = & variants[ 0 ] ;
2228+ assert_eq ! ( * index, 0 ) ;
2229+ let rule_list = rules. rules . get ( & DocPath :: root ( ) ) . expect ( "should have root path rule" ) ;
2230+ assert_eq ! ( rule_list. rules. len( ) , 1 ) ;
2231+ assert_eq ! ( rule_list. rules[ 0 ] , MatchingRule :: Equality ) ;
2232+ }
2233+ _ => panic ! ( "Expected ArrayContains rule, got {:?}" , result. rules[ 0 ] ) ,
2234+ }
2235+ assert_eq ! ( result. value, "PUBLIC" ) ;
2236+ }
2237+
2238+ #[ test]
2239+ fn parse_array_contains_multi_variant ( ) {
2240+ let result = parse_matcher_def ( "arrayContains(matching(equalTo, 'PUBLIC'), matching(equalTo, 'PRIVATE_LINK'))" ) . unwrap ( ) ;
2241+ assert_eq ! ( result. rules. len( ) , 1 ) ;
2242+ match & result. rules [ 0 ] {
2243+ Either :: Left ( MatchingRule :: ArrayContains ( variants) ) => {
2244+ assert_eq ! ( variants. len( ) , 2 ) ;
2245+ assert_eq ! ( variants[ 0 ] . 0 , 0 ) ;
2246+ assert_eq ! ( variants[ 1 ] . 0 , 1 ) ;
2247+ }
2248+ _ => panic ! ( "Expected ArrayContains rule" ) ,
2249+ }
2250+ assert_eq ! ( result. value, "PUBLIC" ) ;
2251+ }
2252+
2253+ #[ test]
2254+ fn parse_array_contains_with_reference ( ) {
2255+ let result = parse_matcher_def ( "arrayContains(matching($'publicEntry'))" ) . unwrap ( ) ;
2256+ assert_eq ! ( result. rules. len( ) , 1 ) ;
2257+ match & result. rules [ 0 ] {
2258+ Either :: Right ( reference) => {
2259+ assert_eq ! ( reference. name, "publicEntry" ) ;
2260+ }
2261+ _ => panic ! ( "Expected a reference, got {:?}" , result. rules[ 0 ] ) ,
2262+ }
2263+ }
2264+
2265+ #[ test]
2266+ fn parse_array_contains_with_multiple_references ( ) {
2267+ let result = parse_matcher_def ( "arrayContains(matching($'entry1'), matching($'entry2'))" ) . unwrap ( ) ;
2268+ assert_eq ! ( result. rules. len( ) , 2 ) ;
2269+ match ( & result. rules [ 0 ] , & result. rules [ 1 ] ) {
2270+ ( Either :: Right ( r1) , Either :: Right ( r2) ) => {
2271+ assert_eq ! ( r1. name, "entry1" ) ;
2272+ assert_eq ! ( r2. name, "entry2" ) ;
2273+ }
2274+ _ => panic ! ( "Expected two references" ) ,
2275+ }
2276+ }
2277+
2278+ #[ test]
2279+ fn parse_array_contains_with_regex ( ) {
2280+ let result = parse_matcher_def ( "arrayContains(matching(regex, 'PUBLIC|PRIVATE.*', 'PUBLIC'))" ) . unwrap ( ) ;
2281+ assert_eq ! ( result. rules. len( ) , 1 ) ;
2282+ match & result. rules [ 0 ] {
2283+ Either :: Left ( MatchingRule :: ArrayContains ( variants) ) => {
2284+ assert_eq ! ( variants. len( ) , 1 ) ;
2285+ let rule_list = variants[ 0 ] . 1 . rules . get ( & DocPath :: root ( ) ) . unwrap ( ) ;
2286+ match & rule_list. rules [ 0 ] {
2287+ MatchingRule :: Regex ( re) => assert_eq ! ( re, "PUBLIC|PRIVATE.*" ) ,
2288+ other => panic ! ( "Expected Regex, got {:?}" , other) ,
2289+ }
2290+ }
2291+ _ => panic ! ( "Expected ArrayContains" ) ,
2292+ }
2293+ assert_eq ! ( result. value, "PUBLIC" ) ;
2294+ }
2295+
2296+ #[ test]
2297+ fn parse_array_contains_empty_is_error ( ) {
2298+ let result = parse_matcher_def ( "arrayContains()" ) ;
2299+ assert ! ( result. is_err( ) ) ;
2300+ }
2301+
2302+ #[ test]
2303+ fn parse_array_contains_combined_with_at_least ( ) {
2304+ let result = parse_matcher_def ( "atLeast(2), arrayContains(matching(equalTo, 'PUBLIC'))" ) . unwrap ( ) ;
2305+ assert ! ( result. rules. iter( ) . any( |r| matches!( r, Either :: Left ( MatchingRule :: MinType ( 2 ) ) ) ) ) ;
2306+ assert ! ( result. rules. iter( ) . any( |r| matches!( r, Either :: Left ( MatchingRule :: ArrayContains ( _) ) ) ) ) ;
2307+ }
2308+
2309+ #[ test]
2310+ fn parse_array_contains_combined_with_each_value ( ) {
2311+ let result = parse_matcher_def ( "eachValue(matching(type, 'X')), arrayContains(matching(equalTo, 'PUBLIC'))" ) . unwrap ( ) ;
2312+ assert ! ( result. rules. iter( ) . any( |r| matches!( r, Either :: Left ( MatchingRule :: EachValue ( _) ) ) ) ) ;
2313+ assert ! ( result. rules. iter( ) . any( |r| matches!( r, Either :: Left ( MatchingRule :: ArrayContains ( _) ) ) ) ) ;
2314+ }
2315+
2316+ #[ test]
2317+ fn parse_array_contains_mixed_inline_and_reference ( ) {
2318+ let result = parse_matcher_def ( "arrayContains(matching(equalTo, 'X'), matching($'ref'))" ) . unwrap ( ) ;
2319+ // Should have ArrayContains with 1 inline variant + 1 reference
2320+ assert ! ( result. rules. iter( ) . any( |r| matches!( r, Either :: Left ( MatchingRule :: ArrayContains ( _) ) ) ) ) ;
2321+ assert ! ( result. rules. iter( ) . any( |r| matches!( r, Either :: Right ( _) ) ) ) ;
2322+ assert_eq ! ( result. value, "X" ) ;
2323+ }
21202324}
0 commit comments