@@ -54,6 +54,102 @@ fn extractHeaderName(key: []const u8) ![]const u8 {
5454 return key [start_quote + 1 .. end_quote ];
5555}
5656
57+ fn matchesRegex (text : []const u8 , pattern : []const u8 ) bool {
58+ if (pattern .len == 0 ) return text .len == 0 ;
59+
60+ // Handle anchors
61+ const starts_with_anchor = pattern [0 ] == '^' ;
62+ const ends_with_anchor = pattern .len > 0 and pattern [pattern .len - 1 ] == '$' ;
63+
64+ var actual_pattern = pattern ;
65+ if (starts_with_anchor ) actual_pattern = pattern [1.. ];
66+ if (ends_with_anchor and actual_pattern .len > 0 ) actual_pattern = actual_pattern [0.. actual_pattern .len - 1 ];
67+
68+ if (starts_with_anchor and ends_with_anchor ) {
69+ return matchesRegexAt (text , actual_pattern , 0 ) == text .len ;
70+ } else if (starts_with_anchor ) {
71+ return matchesRegexAt (text , actual_pattern , 0 ) != null ;
72+ } else if (ends_with_anchor ) {
73+ var i : usize = 0 ;
74+ while (i <= text .len ) : (i += 1 ) {
75+ if (matchesRegexAt (text [i .. ], actual_pattern , 0 )) | end_pos | {
76+ if (i + end_pos == text .len ) return true ;
77+ }
78+ }
79+ return false ;
80+ } else {
81+ var i : usize = 0 ;
82+ while (i <= text .len ) : (i += 1 ) {
83+ if (matchesRegexAt (text [i .. ], actual_pattern , 0 ) != null ) return true ;
84+ }
85+ return false ;
86+ }
87+ }
88+
89+ fn matchesRegexAt (text : []const u8 , pattern : []const u8 , text_pos : usize ) ? usize {
90+ var p_pos : usize = 0 ;
91+ var t_pos = text_pos ;
92+
93+ while (p_pos < pattern .len and t_pos < text .len ) {
94+ if (p_pos + 1 < pattern .len and pattern [p_pos + 1 ] == '*' ) {
95+ // Handle .* or character*
96+ const match_char = pattern [p_pos ];
97+ p_pos += 2 ; // Skip char and *
98+
99+ // Try matching zero occurrences first
100+ if (matchesRegexAt (text , pattern [p_pos .. ], t_pos )) | end_pos | {
101+ return t_pos + end_pos ;
102+ }
103+
104+ // Try matching one or more occurrences
105+ while (t_pos < text .len ) {
106+ if (match_char == '.' or text [t_pos ] == match_char ) {
107+ t_pos += 1 ;
108+ if (matchesRegexAt (text , pattern [p_pos .. ], t_pos )) | end_pos | {
109+ return t_pos + end_pos ;
110+ }
111+ } else {
112+ break ;
113+ }
114+ }
115+ return null ;
116+ } else if (pattern [p_pos ] == '.' ) {
117+ // Match any single character
118+ t_pos += 1 ;
119+ p_pos += 1 ;
120+ } else if (pattern [p_pos ] == '[' ) {
121+ // Character class
122+ const close_bracket = std .mem .indexOfScalarPos (u8 , pattern , p_pos + 1 , ']' ) orelse return null ;
123+ const char_class = pattern [p_pos + 1.. close_bracket ];
124+ var matched = false ;
125+ for (char_class ) | c | {
126+ if (text [t_pos ] == c ) {
127+ matched = true ;
128+ break ;
129+ }
130+ }
131+ if (! matched ) return null ;
132+ t_pos += 1 ;
133+ p_pos = close_bracket + 1 ;
134+ } else {
135+ // Literal character match
136+ if (text [t_pos ] != pattern [p_pos ]) return null ;
137+ t_pos += 1 ;
138+ p_pos += 1 ;
139+ }
140+ }
141+
142+ // Handle remaining .* patterns at end
143+ while (p_pos + 1 < pattern .len and pattern [p_pos + 1 ] == '*' ) {
144+ p_pos += 2 ;
145+ }
146+
147+ if (p_pos == pattern .len ) {
148+ return t_pos - text_pos ;
149+ }
150+ return null ;
151+ }
152+
57153pub fn check (request : * HttpParser.HttpRequest , response : Client.HttpResponse ) ! void {
58154 const stderr = std .io .getStdErr ().writer ();
59155 for (request .assertions .items ) | assertion | {
@@ -184,6 +280,58 @@ pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !v
184280 return error .InvalidAssertionKey ;
185281 }
186282 },
283+ .matches_regex = > {
284+ if (std .ascii .eqlIgnoreCase (assertion .key , "status" )) {
285+ var status_buf : [3 ]u8 = undefined ;
286+ const status_code = @intFromEnum (response .status .? );
287+ const status_str = std .fmt .bufPrint (& status_buf , "{}" , .{status_code }) catch return error .StatusCodeFormat ;
288+ if (! matchesRegex (status_str , assertion .value )) {
289+ stderr .print ("[Fail] Expected status code to match regex \" {s}\" , got \" {s}\" \n " , .{ assertion .value , status_str }) catch {};
290+ return error .StatusCodeNotMatchesRegex ;
291+ }
292+ } else if (std .ascii .eqlIgnoreCase (assertion .key , "body" )) {
293+ if (! matchesRegex (response .body , assertion .value )) {
294+ stderr .print ("[Fail] Expected body content to match regex \" {s}\" , got \" {s}\" \n " , .{ assertion .value , response .body }) catch {};
295+ return error .BodyContentNotMatchesRegex ;
296+ }
297+ } else if (std .mem .startsWith (u8 , assertion .key , "header[\" " )) {
298+ const header_name = try extractHeaderName (assertion .key );
299+ const actual_value = response .headers .get (header_name );
300+ if (actual_value == null or ! matchesRegex (actual_value .? , assertion .value )) {
301+ stderr .print ("[Fail] Expected header \" {s}\" to match regex \" {s}\" , got \" {s}\" \n " , .{ header_name , assertion .value , actual_value orelse "null" }) catch {};
302+ return error .HeaderNotMatchesRegex ;
303+ }
304+ } else {
305+ stderr .print ("[Fail] Invalid assertion key for matches_regex: {s}\n " , .{assertion .key }) catch {};
306+ return error .InvalidAssertionKey ;
307+ }
308+ },
309+ .not_matches_regex = > {
310+ if (std .ascii .eqlIgnoreCase (assertion .key , "status" )) {
311+ var status_buf : [3 ]u8 = undefined ;
312+ const status_code = @intFromEnum (response .status .? );
313+ const status_str = std .fmt .bufPrint (& status_buf , "{}" , .{status_code }) catch return error .StatusCodeFormat ;
314+ if (matchesRegex (status_str , assertion .value )) {
315+ stderr .print ("[Fail] Expected status code to NOT match regex \" {s}\" , got \" {s}\" \n " , .{ assertion .value , status_str }) catch {};
316+ return error .StatusCodeMatchesRegexButShouldnt ;
317+ }
318+ } else if (std .ascii .eqlIgnoreCase (assertion .key , "body" )) {
319+ if (matchesRegex (response .body , assertion .value )) {
320+ stderr .print ("[Fail] Expected body content to NOT match regex \" {s}\" , got \" {s}\" \n " , .{ assertion .value , response .body }) catch {};
321+ return error .BodyContentMatchesRegexButShouldnt ;
322+ }
323+ } else if (std .mem .startsWith (u8 , assertion .key , "header[\" " )) {
324+ const header_name = try extractHeaderName (assertion .key );
325+ const actual_value = response .headers .get (header_name );
326+ if (actual_value != null and matchesRegex (actual_value .? , assertion .value )) {
327+ stderr .print ("[Fail] Expected header \" {s}\" to NOT match regex \" {s}\" , got \" {s}\" \n " , .{ header_name , assertion .value , actual_value orelse "null" }) catch {};
328+ return error .HeaderMatchesRegexButShouldnt ;
329+ }
330+ } else {
331+ stderr .print ("[Fail] Invalid assertion key for not_matches_regex: {s}\n " , .{assertion .key }) catch {};
332+ return error .InvalidAssertionKey ;
333+ }
334+ },
187335 else = > {},
188336 }
189337 }
@@ -340,3 +488,61 @@ test "HttpParser supports starts_with for status, body, and header" {
340488
341489 try check (& request , response );
342490}
491+
492+ test "HttpParser supports matches_regex and not_matches_regex for status, body, and headers" {
493+ const allocator = std .testing .allocator ;
494+
495+ var assertions = std .ArrayList (HttpParser .Assertion ).init (allocator );
496+ defer assertions .deinit ();
497+
498+ // Should pass: status matches regex for 2xx codes
499+ try assertions .append (HttpParser.Assertion {
500+ .key = "status" ,
501+ .value = "^2.*" ,
502+ .assertion_type = .matches_regex ,
503+ });
504+
505+ // Should pass: body matches regex for JSON-like content
506+ try assertions .append (HttpParser.Assertion {
507+ .key = "body" ,
508+ .value = ".*success.*" ,
509+ .assertion_type = .matches_regex ,
510+ });
511+
512+ // Should pass: header matches regex for application/* content types
513+ try assertions .append (HttpParser.Assertion {
514+ .key = "header[\" content-type\" ]" ,
515+ .value = "application/.*" ,
516+ .assertion_type = .matches_regex ,
517+ });
518+
519+ // Should pass: status does not match regex for error codes
520+ try assertions .append (HttpParser.Assertion {
521+ .key = "status" ,
522+ .value = "^[45].*" ,
523+ .assertion_type = .not_matches_regex ,
524+ });
525+
526+ var request = HttpParser.HttpRequest {
527+ .method = .GET ,
528+ .url = "https://api.example.com" ,
529+ .headers = std .ArrayList (http .Header ).init (allocator ),
530+ .assertions = assertions ,
531+ .body = null ,
532+ };
533+
534+ var response_headers = std .StringHashMap ([]const u8 ).init (allocator );
535+ try response_headers .put ("content-type" , "application/json" );
536+ defer response_headers .deinit ();
537+
538+ const body = try allocator .dupe (u8 , "Operation success completed" );
539+ defer allocator .free (body );
540+ const response = Client.HttpResponse {
541+ .status = http .Status .ok ,
542+ .headers = response_headers ,
543+ .body = body ,
544+ .allocator = allocator ,
545+ };
546+
547+ try check (& request , response );
548+ }
0 commit comments