@@ -7,10 +7,10 @@ use serde_json::Value;
77use std:: collections:: HashMap ;
88
99use crate :: assert:: engine:: AssertionResult ;
10- use crate :: plugins:: { PluginContext , PluginManager , PluginResult } ;
10+ use crate :: plugins:: { PluginContext , PluginManager , PluginResult , normalize_plugin_name } ;
1111
12- /// Evaluate legacy assertion (plugins and operators)
13- pub fn evaluate_legacy (
12+ /// Evaluate assertion expression (plugins and operators)
13+ pub fn evaluate_assertion (
1414 plugin_manager : & PluginManager ,
1515 assertion : & str ,
1616 response : & Value ,
@@ -85,31 +85,18 @@ fn evaluate_boolean_function(
8585) -> Result < AssertionResult > {
8686 if let ( Some ( start_paren) , Some ( end_paren) ) = ( expr. find ( '(' ) , expr. rfind ( ')' ) ) {
8787 let func_name = & expr[ 0 ..start_paren] ;
88- let plugin_name = func_name. strip_prefix ( '@' ) . unwrap_or ( func_name) ;
8988 let arg_str = & expr[ start_paren + 1 ..end_paren] ;
9089
91- if let Some ( plugin) = plugin_manager. get ( plugin_name) {
90+ let resolved_name = normalize_plugin_name ( func_name) ;
91+
92+ if let Some ( plugin) = plugin_manager. get ( resolved_name) {
9293 let context = PluginContext {
9394 response,
9495 headers,
9596 trailers,
9697 } ;
9798
98- // Special handling for @header, @has_header, and @trailer arguments (raw string)
99- let args = if plugin_name == "header"
100- || plugin_name == "has_header"
101- || plugin_name == "trailer"
102- {
103- vec ! [ Value :: String ( arg_str. trim( ) . trim_matches( '"' ) . to_string( ) ) ]
104- } else {
105- vec ! [ evaluate_expression(
106- plugin_manager,
107- arg_str,
108- response,
109- headers,
110- trailers,
111- ) ]
112- } ;
99+ let args = parse_plugin_arguments ( plugin_manager, arg_str, response, headers, trailers) ;
113100
114101 return match plugin. execute ( & args, & context) {
115102 Ok ( PluginResult :: Assertion ( res) ) => Ok ( res) ,
@@ -119,7 +106,7 @@ fn evaluate_boolean_function(
119106 } else {
120107 Ok ( AssertionResult :: fail ( format ! (
121108 "Plugin {} returned falsy value: {:?}" ,
122- plugin_name , val
109+ resolved_name , val
123110 ) ) )
124111 }
125112 }
@@ -144,31 +131,18 @@ fn evaluate_expression(
144131 && let ( Some ( start_paren) , Some ( end_paren) ) = ( expr. find ( '(' ) , expr. rfind ( ')' ) )
145132 {
146133 let func_name = & expr[ 0 ..start_paren] ;
147- let plugin_name = func_name. strip_prefix ( '@' ) . unwrap_or ( func_name) ;
148134 let arg_str = & expr[ start_paren + 1 ..end_paren] ;
149135
150- if let Some ( plugin) = plugin_manager. get ( plugin_name) {
136+ let resolved_name = normalize_plugin_name ( func_name) ;
137+
138+ if let Some ( plugin) = plugin_manager. get ( resolved_name) {
151139 let context = PluginContext {
152140 response,
153141 headers,
154142 trailers,
155143 } ;
156144
157- // Special handling for @header, @has_header, and @trailer arguments (raw string)
158- let args = if plugin_name == "header"
159- || plugin_name == "has_header"
160- || plugin_name == "trailer"
161- {
162- vec ! [ Value :: String ( arg_str. trim( ) . trim_matches( '"' ) . to_string( ) ) ]
163- } else {
164- vec ! [ evaluate_expression(
165- plugin_manager,
166- arg_str,
167- response,
168- headers,
169- trailers,
170- ) ]
171- } ;
145+ let args = parse_plugin_arguments ( plugin_manager, arg_str, response, headers, trailers) ;
172146
173147 match plugin. execute ( & args, & context) {
174148 Ok ( PluginResult :: Value ( v) ) => return v,
@@ -196,6 +170,99 @@ fn parse_value(s: &str) -> Value {
196170 }
197171}
198172
173+ fn parse_plugin_arguments (
174+ plugin_manager : & PluginManager ,
175+ arg_str : & str ,
176+ response : & Value ,
177+ headers : Option < & HashMap < String , String > > ,
178+ trailers : Option < & HashMap < String , String > > ,
179+ ) -> Vec < Value > {
180+ split_arguments ( arg_str)
181+ . into_iter ( )
182+ . map ( |token| parse_argument_value ( plugin_manager, token, response, headers, trailers) )
183+ . collect ( )
184+ }
185+
186+ fn split_arguments ( input : & str ) -> Vec < & str > {
187+ let trimmed = input. trim ( ) ;
188+ if trimmed. is_empty ( ) {
189+ return Vec :: new ( ) ;
190+ }
191+
192+ let mut out = Vec :: new ( ) ;
193+ let mut start = 0 ;
194+ let mut depth = 0 ;
195+ let mut in_string = false ;
196+ let mut escaped = false ;
197+
198+ for ( idx, ch) in trimmed. char_indices ( ) {
199+ if in_string {
200+ if escaped {
201+ escaped = false ;
202+ continue ;
203+ }
204+ if ch == '\\' {
205+ escaped = true ;
206+ continue ;
207+ }
208+ if ch == '"' {
209+ in_string = false ;
210+ }
211+ continue ;
212+ }
213+
214+ match ch {
215+ '"' => in_string = true ,
216+ '(' | '[' | '{' => depth += 1 ,
217+ ')' | ']' | '}' => {
218+ if depth > 0 {
219+ depth -= 1 ;
220+ }
221+ }
222+ ',' if depth == 0 => {
223+ out. push ( trimmed[ start..idx] . trim ( ) ) ;
224+ start = idx + 1 ;
225+ }
226+ _ => { }
227+ }
228+ }
229+
230+ out. push ( trimmed[ start..] . trim ( ) ) ;
231+ out
232+ }
233+
234+ fn parse_argument_value (
235+ plugin_manager : & PluginManager ,
236+ token : & str ,
237+ response : & Value ,
238+ headers : Option < & HashMap < String , String > > ,
239+ trailers : Option < & HashMap < String , String > > ,
240+ ) -> Value {
241+ let t = token. trim ( ) ;
242+ if t. is_empty ( ) {
243+ return Value :: Null ;
244+ }
245+
246+ if t. starts_with ( '@' ) && t. contains ( '(' ) && t. ends_with ( ')' ) {
247+ return evaluate_expression ( plugin_manager, t, response, headers, trailers) ;
248+ }
249+
250+ if t == "." || t. starts_with ( '.' ) {
251+ return resolve_path ( t, response) ;
252+ }
253+
254+ if ( t. starts_with ( '"' ) && t. ends_with ( '"' ) && t. len ( ) >= 2 )
255+ || t == "true"
256+ || t == "false"
257+ || t == "null"
258+ || t. parse :: < f64 > ( ) . is_ok ( )
259+ {
260+ return parse_value ( t) ;
261+ }
262+
263+ Value :: String ( t. to_string ( ) )
264+ }
265+
199266fn compare (
200267 lhs : Value ,
201268 op : & str ,
@@ -343,34 +410,75 @@ mod tests {
343410 }
344411
345412 #[ test]
346- fn test_evaluate_legacy_equality ( ) {
413+ fn test_evaluate_assertion_equality ( ) {
347414 let pm = create_plugin_manager ( ) ;
348415 let response = json ! ( { "status" : "success" } ) ;
349- let result = evaluate_legacy ( & pm, ".status == \" success\" " , & response, None , None ) . unwrap ( ) ;
416+ let result =
417+ evaluate_assertion ( & pm, ".status == \" success\" " , & response, None , None ) . unwrap ( ) ;
350418 assert ! ( matches!( result, AssertionResult :: Pass ) ) ;
351419 }
352420
353421 #[ test]
354- fn test_evaluate_legacy_inequality ( ) {
422+ fn test_evaluate_assertion_inequality ( ) {
355423 let pm = create_plugin_manager ( ) ;
356424 let response = json ! ( { "status" : "success" } ) ;
357- let result = evaluate_legacy ( & pm, ".status == \" error\" " , & response, None , None ) . unwrap ( ) ;
425+ let result =
426+ evaluate_assertion ( & pm, ".status == \" error\" " , & response, None , None ) . unwrap ( ) ;
358427 assert ! ( matches!( result, AssertionResult :: Fail { .. } ) ) ;
359428 }
360429
361430 #[ test]
362- fn test_evaluate_legacy_contains ( ) {
431+ fn test_evaluate_assertion_contains ( ) {
363432 let pm = create_plugin_manager ( ) ;
364433 let response = json ! ( { "name" : "test" } ) ;
365- let result = evaluate_legacy ( & pm, ".name contains \" te\" " , & response, None , None ) . unwrap ( ) ;
434+ let result =
435+ evaluate_assertion ( & pm, ".name contains \" te\" " , & response, None , None ) . unwrap ( ) ;
366436 assert ! ( matches!( result, AssertionResult :: Pass ) ) ;
367437 }
368438
369439 #[ test]
370- fn test_evaluate_legacy_plugin ( ) {
440+ fn test_evaluate_assertion_plugin ( ) {
371441 let pm = create_plugin_manager ( ) ;
372442 let response = json ! ( { "id" : "550e8400-e29b-41d4-a716-446655440000" } ) ;
373- let result = evaluate_legacy ( & pm, "@uuid(.id)" , & response, None , None ) . unwrap ( ) ;
443+ let result = evaluate_assertion ( & pm, "@uuid(.id)" , & response, None , None ) . unwrap ( ) ;
444+ assert ! ( matches!( result, AssertionResult :: Pass ) ) ;
445+ }
446+
447+ #[ test]
448+ fn test_evaluate_assertion_has_header_unquoted_argument ( ) {
449+ let pm = create_plugin_manager ( ) ;
450+ let response = json ! ( { } ) ;
451+ let mut headers = HashMap :: new ( ) ;
452+ headers. insert ( "content-type" . to_string ( ) , "application/json" . to_string ( ) ) ;
453+
454+ let result = evaluate_assertion (
455+ & pm,
456+ "@has_header(content-type) == true" ,
457+ & response,
458+ Some ( & headers) ,
459+ None ,
460+ )
461+ . unwrap ( ) ;
462+
463+ assert ! ( matches!( result, AssertionResult :: Pass ) ) ;
464+ }
465+
466+ #[ test]
467+ fn test_evaluate_assertion_trailer_value_plugin ( ) {
468+ let pm = create_plugin_manager ( ) ;
469+ let response = json ! ( { } ) ;
470+ let mut trailers = HashMap :: new ( ) ;
471+ trailers. insert ( "grpc-status" . to_string ( ) , "0" . to_string ( ) ) ;
472+
473+ let result = evaluate_assertion (
474+ & pm,
475+ "@trailer(\" grpc-status\" ) == \" 0\" " ,
476+ & response,
477+ None ,
478+ Some ( & trailers) ,
479+ )
480+ . unwrap ( ) ;
481+
374482 assert ! ( matches!( result, AssertionResult :: Pass ) ) ;
375483 }
376484
0 commit comments