1use serde::{Deserialize, Serialize};
2use std::{
3 collections::BTreeMap,
4 fmt::{self, Display, Formatter},
5 time::Duration,
6};
7
8use crate::{HttpMethod, RuleMatch};
9
10#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
12pub struct CustomHttpConfigV2 {
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub match_pairing: Option<MatchPairingConfig>,
16
17 pub calls: Vec<HttpCallConfig>,
19}
20
21impl CustomHttpConfigV2 {
22 pub fn with_call(mut self, call: HttpCallConfig) -> Self {
23 self.calls.push(call);
24 self
25 }
26}
27
28#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
30pub struct MatchPairingConfig {
31 pub kind: String,
33
34 #[serde(flatten)]
36 pub parameters: BTreeMap<String, String>,
37}
38
39#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
41pub struct HttpCallConfig {
42 pub request: HttpRequestConfig,
43 pub response: HttpResponseConfig,
44}
45
46#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
48pub struct HttpRequestConfig {
49 pub endpoint: TemplatedMatchString,
52
53 #[serde(default = "default_http_method")]
54 pub method: HttpMethod,
55
56 #[serde(default)]
59 pub hosts: Vec<TemplatedMatchString>,
60
61 #[serde(default)]
64 pub headers: BTreeMap<String, TemplatedMatchString>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub body: Option<TemplatedMatchString>,
69
70 #[serde(default = "default_timeout")]
71 pub timeout: Duration,
72}
73
74fn default_http_method() -> HttpMethod {
75 HttpMethod::Get
76}
77
78fn default_timeout() -> Duration {
79 Duration::from_secs(3)
80}
81
82#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
84pub struct HttpResponseConfig {
85 pub conditions: Vec<ResponseCondition>,
88}
89
90#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
92pub struct ResponseCondition {
93 #[serde(rename = "type")]
95 pub condition_type: ResponseConditionType,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub status_code: Option<StatusCodeMatcher>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub raw_body: Option<BodyMatcher>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
109 pub body: Option<BTreeMap<String, BodyMatcher>>,
110}
111
112impl ResponseCondition {
113 pub fn matches(&self, status_code: u16, body: &str) -> ResponseConditionResult {
114 if let Some(status_code_matcher) = self.status_code.as_ref()
115 && status_code_matcher.matches(status_code)
116 {
117 return self.condition_type.clone().into();
118 }
119 if let Some(raw_body_matcher) = self.raw_body.as_ref()
120 && raw_body_matcher.matches(body)
121 {
122 return self.condition_type.clone().into();
123 }
124 ResponseConditionResult::NotChecked
125 }
126}
127
128#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
130#[serde(rename_all = "lowercase")]
131pub enum ResponseConditionType {
132 Valid,
133 Invalid,
134}
135
136#[derive(Debug, PartialEq)]
137pub enum ResponseConditionResult {
138 Valid,
139 Invalid,
140 NotChecked,
141}
142
143impl From<ResponseConditionType> for ResponseConditionResult {
144 fn from(condition_type: ResponseConditionType) -> Self {
145 match condition_type {
146 ResponseConditionType::Valid => ResponseConditionResult::Valid,
147 ResponseConditionType::Invalid => ResponseConditionResult::Invalid,
148 }
149 }
150}
151
152#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
154#[serde(untagged)]
155pub enum StatusCodeMatcher {
156 Single(u16),
158
159 List(Vec<u16>),
161
162 Range { start: u16, end: u16 },
164}
165
166impl StatusCodeMatcher {
167 pub fn matches(&self, status_code: u16) -> bool {
169 match self {
170 StatusCodeMatcher::Single(code) => status_code == *code,
171 StatusCodeMatcher::List(codes) => codes.contains(&status_code),
172 StatusCodeMatcher::Range { start, end } => status_code >= *start && status_code < *end,
173 }
174 }
175}
176
177#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
179#[serde(tag = "type", content = "config")]
180pub enum BodyMatcher {
181 Present,
183
184 ExactMatch(String),
186
187 Regex(String),
189}
190
191impl BodyMatcher {
192 pub fn matches(&self, body: &str) -> bool {
193 match self {
194 BodyMatcher::Present => !body.is_empty(),
195 BodyMatcher::ExactMatch(value) => body == *value,
196 BodyMatcher::Regex(pattern) => regex::Regex::new(pattern).unwrap().is_match(body),
197 }
198 }
199}
200
201#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
203pub struct PairedValidatorConfig {
204 pub kind: String,
206
207 pub name: String,
210}
211
212#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
213pub struct TemplatedMatchString(pub String);
214
215impl TemplatedMatchString {
216 pub fn with_rule_match(&self, rule_match: &RuleMatch) -> Self {
217 self.render("$MATCH", rule_match.match_value.as_ref().unwrap())
218 }
219
220 pub fn with_host(&self, host: &str) -> Self {
221 self.render("$HOST", host)
222 }
223
224 fn render(&self, tag: &str, value: &str) -> Self {
225 TemplatedMatchString(self.0.replace(tag, value))
226 }
227}
228
229impl Display for TemplatedMatchString {
230 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
231 write!(f, "{}", self.0)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_status_code_matcher_single() {
241 let matcher = StatusCodeMatcher::Single(200);
242 assert!(matcher.matches(200));
243 assert!(!matcher.matches(201));
244 assert!(!matcher.matches(404));
245 }
246
247 #[test]
248 fn test_status_code_matcher_list() {
249 let matcher = StatusCodeMatcher::List(vec![200, 201, 204]);
250 assert!(matcher.matches(200));
251 assert!(matcher.matches(201));
252 assert!(matcher.matches(204));
253 assert!(!matcher.matches(202));
254 assert!(!matcher.matches(404));
255 }
256
257 #[test]
258 fn test_status_code_matcher_range() {
259 let matcher = StatusCodeMatcher::Range {
260 start: 200,
261 end: 300,
262 };
263 assert!(matcher.matches(200));
264 assert!(matcher.matches(250));
265 assert!(matcher.matches(299));
266 assert!(!matcher.matches(199));
267 assert!(!matcher.matches(300));
268 assert!(!matcher.matches(404));
269 }
270
271 #[test]
272 fn test_body_matcher_present() {
273 let matcher = BodyMatcher::Present;
274 assert!(matcher.matches("test"));
275 assert!(!matcher.matches(""));
276 }
277
278 #[test]
279 fn test_body_matcher_exact_match() {
280 let matcher = BodyMatcher::ExactMatch("test".to_string());
281 assert!(matcher.matches("test"));
282 assert!(!matcher.matches("test1"));
283 assert!(!matcher.matches(""));
284 }
285
286 #[test]
287 fn test_body_matcher_regex() {
288 let matcher = BodyMatcher::Regex("test".to_string());
289 assert!(matcher.matches("test"));
290 assert!(matcher.matches("test1"));
291 assert!(!matcher.matches("different"));
292 }
293}