1use serde::Deserialize;
2
3use std::{fs, path::Path};
4
5mod default_respond;
6mod guard;
7pub mod prefix;
8pub mod rule;
9
10use crate::{
11 error::{RoutingError, RoutingResult},
12 parsed_request::ParsedRequest,
13 strategy::Strategy,
14 util::http::normalize_url_path,
15};
16use default_respond::DefaultRespond;
17use guard::Guard;
18use prefix::Prefix;
19use rule::{Rule, respond::Respond};
20
21use std::sync::{
32 Arc,
33 atomic::{AtomicUsize, Ordering},
34};
35
36fn default_counter() -> Arc<AtomicUsize> {
37 Arc::new(AtomicUsize::new(0))
38}
39
40#[derive(Clone, Deserialize, Debug)]
41pub struct RuleSet {
42 pub prefix: Option<Prefix>,
43 pub default: Option<DefaultRespond>,
44 pub guard: Option<Guard>,
45 pub rules: Vec<Rule>,
46 #[serde(default)]
50 pub strategy: Option<Strategy>,
51 #[serde(skip)]
52 pub file_path: String,
53 #[serde(skip, default = "default_counter")]
55 pub round_robin_counter: Arc<AtomicUsize>,
56}
57
58impl RuleSet {
59 #[allow(clippy::result_large_err)]
74 pub fn new(
75 rule_set_file_path: &str,
76 current_dir_to_config_dir_relative_path: &str,
77 rule_set_idx: usize,
78 ) -> RoutingResult<Self> {
79 let path = Path::new(rule_set_file_path);
80 let toml_string =
81 fs::read_to_string(rule_set_file_path).map_err(|e| RoutingError::RuleSetRead {
82 path: path.to_path_buf(),
83 source: e,
84 })?;
85
86 let mut ret: Self =
87 toml::from_str(&toml_string).map_err(|e| RoutingError::RuleSetParse {
88 path: path.to_path_buf(),
89 canonical: path.canonicalize().ok(),
90 source: e,
91 })?;
92
93 let mut prefix = ret.prefix.clone().unwrap_or_default();
95
96 prefix.url_path_prefix = prefix
99 .url_path_prefix
100 .as_deref()
101 .map(|p| normalize_url_path(p, None));
102
103 let respond_dir_prefix = prefix.respond_dir_prefix.as_deref().unwrap_or(".");
107
108 let respond_dir_prefix =
109 Path::new(current_dir_to_config_dir_relative_path).join(respond_dir_prefix);
110 let respond_dir_prefix = respond_dir_prefix.to_str().ok_or_else(|| {
111 RoutingError::RuleSetRead {
112 path: path.to_path_buf(),
113 source: std::io::Error::new(
118 std::io::ErrorKind::InvalidData,
119 format!(
120 "respond_dir path contains non-UTF-8 bytes: {}",
121 respond_dir_prefix.to_string_lossy()
122 ),
123 ),
124 }
125 })?;
126
127 prefix.respond_dir_prefix = Some(respond_dir_prefix.to_owned());
128 ret.prefix = Some(prefix);
129
130 ret.rules = ret
134 .rules
135 .iter()
136 .enumerate()
137 .map(|(rule_idx, rule)| rule.compute_derived_fields(&ret, rule_idx, rule_set_idx))
138 .collect();
139
140 ret.file_path = rule_set_file_path.to_owned();
142 ret.round_robin_counter = Arc::new(AtomicUsize::new(0));
144
145 Ok(ret)
146 }
147
148 pub fn find_matched(
150 &self,
151 parsed_request: &ParsedRequest,
152 strategy: Option<&Strategy>,
153 rule_set_idx: usize,
154 ) -> Option<Respond> {
155 match self.prefix.as_ref() {
156 Some(prefix)
157 if prefix.url_path_prefix.is_some()
158 && !parsed_request
159 .url_path
160 .starts_with(prefix.url_path_prefix.as_ref().unwrap()) =>
161 {
162 return None;
163 }
164 _ => (),
165 }
166
167 let effective_strategy = self
170 .strategy
171 .as_ref()
172 .or(strategy)
173 .unwrap_or(&Strategy::FirstMatch);
174 let strategy = effective_strategy;
175
176 match strategy {
177 Strategy::FirstMatch => {
178 for (rule_idx, rule) in self.rules.iter().enumerate() {
179 if rule.when.is_match(parsed_request, rule_idx, rule_set_idx) {
180 return Some(rule.respond.clone());
181 }
182 }
183 None
184 }
185
186 Strategy::UniformRandom { seed } => {
187 let matches: Vec<&Rule> = self
189 .rules
190 .iter()
191 .enumerate()
192 .filter(|(idx, r)| r.when.is_match(parsed_request, *idx, rule_set_idx))
193 .map(|(_, r)| r)
194 .collect();
195
196 if matches.is_empty() {
197 return None;
198 }
199 let mut rng = crate::strategy::make_rng(*seed);
200 let idx = rng.next_index(matches.len());
201 Some(matches[idx].respond.clone())
202 }
203
204 Strategy::WeightedRandom { seed } => {
205 let candidates: Vec<(&Rule, u32)> = self
207 .rules
208 .iter()
209 .enumerate()
210 .filter(|(idx, r)| r.when.is_match(parsed_request, *idx, rule_set_idx))
211 .map(|(_, r)| (r, r.weight.unwrap_or(1)))
212 .filter(|(_, w)| *w > 0)
213 .collect();
214
215 if candidates.is_empty() {
216 return None;
217 }
218
219 let total: u32 = candidates.iter().map(|(_, w)| w).sum();
220 let mut rng = crate::strategy::make_rng(*seed);
221 let pick = (rng.next() % total as u64) as u32;
222 let mut acc = 0u32;
223 for (rule, weight) in &candidates {
224 acc += weight;
225 if pick < acc {
226 return Some(rule.respond.clone());
227 }
228 }
229 candidates.last().map(|(r, _)| r.respond.clone())
231 }
232
233 Strategy::Priority { tiebreaker } => {
234 let matches: Vec<(&Rule, i32)> = self
236 .rules
237 .iter()
238 .enumerate()
239 .filter(|(idx, r)| r.when.is_match(parsed_request, *idx, rule_set_idx))
240 .map(|(_, r)| (r, r.priority.unwrap_or(0)))
241 .collect();
242
243 if matches.is_empty() {
244 return None;
245 }
246
247 let max_priority = matches.iter().map(|(_, p)| *p).max().unwrap();
248 let top: Vec<&Rule> = matches
249 .into_iter()
250 .filter(|(_, p)| *p == max_priority)
251 .map(|(r, _)| r)
252 .collect();
253
254 match tiebreaker {
255 crate::strategy::PriorityTiebreaker::FirstMatch => {
256 top.into_iter().next().map(|r| r.respond.clone())
257 }
258 crate::strategy::PriorityTiebreaker::UniformRandom => {
259 let mut rng = crate::strategy::make_rng(None);
260 let idx = rng.next_index(top.len());
261 Some(top[idx].respond.clone())
262 }
263 }
264 }
265
266 Strategy::RoundRobin => {
267 let matches: Vec<&Rule> = self
268 .rules
269 .iter()
270 .enumerate()
271 .filter(|(idx, r)| r.when.is_match(parsed_request, *idx, rule_set_idx))
272 .map(|(_, r)| r)
273 .collect();
274
275 if matches.is_empty() {
276 return None;
277 }
278
279 let idx = self.round_robin_counter.fetch_add(1, Ordering::Relaxed) % matches.len();
283
284 Some(matches[idx].respond.clone())
285 }
286 }
287 }
288
289 pub fn validate(&self) -> bool {
291 true
292 }
293
294 pub fn dir_prefix(&self) -> String {
296 self.prefix
297 .clone()
298 .unwrap_or_default()
299 .respond_dir_prefix
300 .unwrap_or_default()
301 }
302}
303
304impl std::fmt::Display for RuleSet {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 if let Some(x) = self.prefix.as_ref() {
307 let _ = write!(f, "{}", x);
308 }
309 if let Some(x) = self.guard.as_ref() {
310 let _ = write!(f, "{}", x);
311 }
312 if let Some(x) = self.default.as_ref() {
313 let _ = write!(f, "{}", x);
314 }
315 for rule in self.rules.iter() {
316 let _ = write!(f, "{}", rule);
317 }
318 Ok(())
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::parsed_request::ParsedRequest;
326 use crate::rule_set::rule::{
327 Rule,
328 respond::Respond,
329 when::{When, request::Request},
330 };
331 use crate::strategy::Strategy;
332
333 fn get_req(url_path: &str) -> ParsedRequest {
335 let req = hyper::Request::builder()
336 .method("GET")
337 .uri(url_path)
338 .body(())
339 .unwrap();
340 let (parts, _) = req.into_parts();
341 ParsedRequest {
342 url_path: url_path.to_owned(),
343 component_parts: parts,
344 body_json: None,
345 }
346 }
347
348 fn make_round_robin_set(n: usize, url_path: &str) -> RuleSet {
351 use crate::rule_set::rule::when::request::url_path::{UrlPath, UrlPathConfig};
352
353 let rules = (0..n)
354 .map(|i| Rule {
355 when: When {
356 request: Request {
357 url_path_config: Some(UrlPathConfig::Simple(url_path.to_owned())),
358 url_path: Some(UrlPath {
359 value: url_path.to_owned(),
360 value_with_prefix: url_path.to_owned(),
361 op: None,
362 }),
363 http_method: None,
364 headers: None,
365 body: None,
366 },
367 },
368 respond: Respond {
369 text: Some(format!("response_{}", i)),
370 file_path: None,
371 csv_records_key: None,
372 status: None,
373 status_code: None,
374 headers: None,
375 delay_response_milliseconds: None,
376 },
377 weight: None,
378 priority: None,
379 })
380 .collect();
381
382 RuleSet {
383 prefix: None,
384 default: None,
385 guard: None,
386 rules,
387 strategy: None,
388 file_path: String::new(),
389 round_robin_counter: Arc::new(AtomicUsize::new(0)),
390 }
391 }
392
393 #[test]
394 fn round_robin_cycles_through_matching_rules() {
395 let rs = make_round_robin_set(2, "/api");
396 let req = get_req("/api");
397 let strategy = Strategy::RoundRobin;
398
399 let r0 = rs.find_matched(&req, Some(&strategy), 0).expect("match 0");
400 let r1 = rs.find_matched(&req, Some(&strategy), 0).expect("match 1");
401 let r2 = rs.find_matched(&req, Some(&strategy), 0).expect("match 2");
402
403 assert_eq!(r0.text.as_deref(), Some("response_0"));
404 assert_eq!(r1.text.as_deref(), Some("response_1"));
405 assert_eq!(r2.text.as_deref(), Some("response_0"), "cycle back");
406 }
407
408 #[test]
409 fn round_robin_three_rules_full_cycle() {
410 let rs = make_round_robin_set(3, "/api");
411 let req = get_req("/api");
412 let strategy = Strategy::RoundRobin;
413
414 let texts: Vec<String> = (0..6)
415 .map(|_| {
416 rs.find_matched(&req, Some(&strategy), 0)
417 .unwrap()
418 .text
419 .clone()
420 .unwrap()
421 })
422 .collect();
423
424 assert_eq!(
425 texts,
426 vec![
427 "response_0",
428 "response_1",
429 "response_2",
430 "response_0",
431 "response_1",
432 "response_2",
433 ]
434 );
435 }
436
437 #[test]
438 fn round_robin_no_match_does_not_advance_counter() {
439 let rs = make_round_robin_set(2, "/api");
440 let strategy = Strategy::RoundRobin;
441
442 let miss = rs.find_matched(&get_req("/other"), Some(&strategy), 0);
444 assert!(miss.is_none(), "non-matching path should miss");
445
446 let hit = rs
448 .find_matched(&get_req("/api"), Some(&strategy), 0)
449 .expect("should match");
450 assert_eq!(hit.text.as_deref(), Some("response_0"));
451 }
452
453 #[test]
454 fn round_robin_single_match_always_same() {
455 let rs = make_round_robin_set(1, "/api");
456 let req = get_req("/api");
457 let strategy = Strategy::RoundRobin;
458
459 for _ in 0..5 {
460 let r = rs.find_matched(&req, Some(&strategy), 0).expect("match");
461 assert_eq!(r.text.as_deref(), Some("response_0"));
462 }
463 }
464}