http_client_vcr/
matcher.rs1use crate::serializable::SerializableRequest;
2use http_client::Request;
3use std::fmt::Debug;
4
5pub trait RequestMatcher: Debug + Send + Sync {
6 fn matches(&self, request: &Request, recorded_request: &SerializableRequest) -> bool;
7
8 fn matches_serializable(
9 &self,
10 request: &SerializableRequest,
11 recorded_request: &SerializableRequest,
12 ) -> bool {
13 request.method == recorded_request.method && request.url == recorded_request.url
15 }
16}
17
18#[derive(Debug)]
19pub struct DefaultMatcher {
20 match_method: bool,
21 match_url: bool,
22 match_headers: Vec<String>,
23 match_body: bool,
24}
25
26impl DefaultMatcher {
27 pub fn new() -> Self {
28 Self {
29 match_method: true,
30 match_url: true,
31 match_headers: vec![
33 "authorization".to_string(),
34 "cookie".to_string(),
35 "content-type".to_string(),
36 "user-agent".to_string(),
37 ],
38 match_body: false,
39 }
40 }
41
42 pub fn without_cookies() -> Self {
44 Self {
45 match_method: true,
46 match_url: true,
47 match_headers: vec![
48 "authorization".to_string(),
49 "content-type".to_string(),
50 "user-agent".to_string(),
51 ],
52 match_body: false,
53 }
54 }
55
56 pub fn with_method(mut self, match_method: bool) -> Self {
57 self.match_method = match_method;
58 self
59 }
60
61 pub fn with_url(mut self, match_url: bool) -> Self {
62 self.match_url = match_url;
63 self
64 }
65
66 pub fn with_headers(mut self, headers: Vec<String>) -> Self {
67 self.match_headers = headers;
68 self
69 }
70
71 pub fn with_body(mut self, match_body: bool) -> Self {
72 self.match_body = match_body;
73 self
74 }
75}
76
77impl RequestMatcher for DefaultMatcher {
78 fn matches(&self, request: &Request, recorded_request: &SerializableRequest) -> bool {
79 log::debug!(
80 "Matching request: {} {} against recorded: {} {}",
81 request.method(),
82 request.url(),
83 recorded_request.method,
84 recorded_request.url
85 );
86
87 if self.match_method && request.method().to_string() != recorded_request.method {
88 log::debug!(
89 "Method mismatch: {} != {}",
90 request.method(),
91 recorded_request.method
92 );
93 return false;
94 }
95
96 if self.match_url && request.url().to_string() != recorded_request.url {
97 log::debug!(
98 "URL mismatch: {} != {}",
99 request.url(),
100 recorded_request.url
101 );
102 return false;
103 }
104
105 if !self.match_headers.is_empty() {
106 log::debug!("Checking {} headers for matching", self.match_headers.len());
107 for header_name in &self.match_headers {
108 let request_header = request.header(header_name.as_str());
109 let recorded_header = recorded_request.headers.get(header_name);
110
111 log::debug!(
112 "Comparing header '{}': request={:?}, recorded={:?}",
113 header_name,
114 request_header.map(|v| v.iter().map(|h| h.as_str()).collect::<Vec<_>>()),
115 recorded_header
116 );
117
118 match (request_header, recorded_header) {
119 (Some(req_val), Some(rec_val)) => {
120 let req_values: Vec<String> =
121 req_val.iter().map(|v| v.as_str().to_string()).collect();
122 if &req_values != rec_val {
123 log::debug!(
124 "Header '{header_name}' values mismatch: request={req_values:?} != recorded={rec_val:?}"
125 );
126 return false;
127 } else {
128 log::debug!("Header '{header_name}' matched: {req_values:?}");
129 }
130 }
131 (None, None) => {
132 log::debug!("Header '{header_name}' both absent (matched)");
133 }
134 _ => {
135 log::debug!("Header '{}' presence mismatch: request present={}, recorded present={}",
136 header_name, request_header.is_some(), recorded_header.is_some());
137 return false;
138 }
139 }
140 }
141 }
142
143 log::debug!("Request matched successfully");
144 true
145 }
146
147 fn matches_serializable(
148 &self,
149 request: &SerializableRequest,
150 recorded_request: &SerializableRequest,
151 ) -> bool {
152 if self.match_method && request.method != recorded_request.method {
153 return false;
154 }
155
156 if self.match_url && request.url != recorded_request.url {
157 return false;
158 }
159
160 if !self.match_headers.is_empty() {
161 for header_name in &self.match_headers {
162 let request_header = request.headers.get(header_name);
163 let recorded_header = recorded_request.headers.get(header_name);
164
165 match (request_header, recorded_header) {
166 (Some(req_val), Some(rec_val)) => {
167 if req_val != rec_val {
168 return false;
169 }
170 }
171 (None, None) => {}
172 _ => return false,
173 }
174 }
175 }
176
177 true
178 }
179}
180
181impl Default for DefaultMatcher {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187#[derive(Debug)]
188pub struct ExactMatcher;
189
190impl RequestMatcher for ExactMatcher {
191 fn matches(&self, request: &Request, recorded_request: &SerializableRequest) -> bool {
192 if request.method().to_string() != recorded_request.method {
193 return false;
194 }
195
196 if request.url().to_string() != recorded_request.url {
197 return false;
198 }
199
200 let mut request_headers = std::collections::HashMap::new();
201 for (name, values) in request.iter() {
202 let header_values: Vec<String> =
203 values.iter().map(|v| v.as_str().to_string()).collect();
204 request_headers.insert(name.as_str().to_string(), header_values);
205 }
206
207 if request_headers != recorded_request.headers {
208 return false;
209 }
210
211 true
212 }
213
214 fn matches_serializable(
215 &self,
216 request: &SerializableRequest,
217 recorded_request: &SerializableRequest,
218 ) -> bool {
219 request.method == recorded_request.method
220 && request.url == recorded_request.url
221 && request.headers == recorded_request.headers
222 }
223}