1use std::collections::HashMap;
20use std::sync::atomic::{AtomicUsize, Ordering};
21use std::sync::Arc;
22
23use serde_json::Value;
24
25use crate::config::{Method, RequestMatch, ResponseConfig, Route};
26
27#[derive(Debug, Clone)]
29pub struct Router {
30 routes: Vec<CompiledRoute>,
31}
32
33#[derive(Debug, Clone)]
34struct CompiledRoute {
35 method: Method,
36 segments: Vec<Segment>,
37 when: Option<RequestMatch>,
38 responses: Vec<ResponseConfig>,
42 counter: Arc<AtomicUsize>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48enum Segment {
49 Literal(String),
50 Param(String),
51}
52
53#[derive(Debug, Clone)]
55pub struct Match {
56 pub path_params: HashMap<String, String>,
58 pub response: ResponseConfig,
60}
61
62impl Router {
63 pub fn new(routes: Vec<Route>) -> Result<Self, RouterError> {
68 let mut compiled = Vec::with_capacity(routes.len());
69 for (index, route) in routes.into_iter().enumerate() {
70 let segments = compile_path(&route.path).map_err(|e| RouterError::InvalidPath {
71 route_index: index,
72 source: e,
73 })?;
74 let responses = route.response.into_responses();
75 if responses.is_empty() {
76 return Err(RouterError::EmptySequence { route_index: index });
77 }
78 compiled.push(CompiledRoute {
79 method: route.method,
80 segments,
81 when: route.when,
82 responses,
83 counter: Arc::new(AtomicUsize::new(0)),
84 });
85 }
86 Ok(Router { routes: compiled })
87 }
88
89 pub fn len(&self) -> usize {
91 self.routes.len()
92 }
93
94 pub fn is_empty(&self) -> bool {
96 self.routes.is_empty()
97 }
98
99 pub fn resolve(
108 &self,
109 method: Method,
110 path: &str,
111 query: &HashMap<String, String>,
112 headers: &HashMap<String, String>,
113 body: &Value,
114 ) -> Option<Match> {
115 let request_segments: Vec<&str> = path_segments(path).collect();
116
117 for route in &self.routes {
118 if route.method != method {
119 continue;
120 }
121 if let Some(path_params) = match_path(&route.segments, &request_segments) {
122 if match_when(route.when.as_ref(), query, headers, body) {
123 let response = pick_response(route);
124 return Some(Match {
125 path_params,
126 response,
127 });
128 }
129 }
130 }
131 None
132 }
133}
134
135fn pick_response(route: &CompiledRoute) -> ResponseConfig {
141 let n = route.responses.len();
142 if n == 1 {
143 return route.responses[0].clone();
144 }
145 let idx = route.counter.fetch_add(1, Ordering::Relaxed);
146 let clamped = idx.min(n - 1);
148 route.responses[clamped].clone()
149}
150
151fn path_segments(path: &str) -> impl Iterator<Item = &str> {
153 path.trim_end_matches('/')
154 .split('/')
155 .filter(|s| !s.is_empty())
156}
157
158fn compile_path(pattern: &str) -> Result<Vec<Segment>, PathError> {
164 let mut segments = Vec::new();
165 for raw in pattern.split('/') {
166 if raw.is_empty() {
167 continue;
168 }
169 if let Some(name) = raw.strip_prefix('{').and_then(|r| r.strip_suffix('}')) {
170 if name.is_empty() || name.contains('{') || name.contains('}') {
171 return Err(PathError::InvalidParam(raw.to_string()));
172 }
173 segments.push(Segment::Param(name.to_string()));
174 } else if raw.contains('{') || raw.contains('}') {
175 return Err(PathError::UnbalancedBraces(raw.to_string()));
176 } else {
177 segments.push(Segment::Literal(raw.to_string()));
178 }
179 }
180 if segments.is_empty() {
181 return Err(PathError::EmptyPattern);
182 }
183 Ok(segments)
184}
185
186fn match_path(segments: &[Segment], request: &[&str]) -> Option<HashMap<String, String>> {
190 if segments.len() != request.len() {
191 return None;
192 }
193 let mut params = HashMap::with_capacity(segments.len());
194 for (seg, req) in segments.iter().zip(request.iter()) {
195 match seg {
196 Segment::Literal(lit) => {
197 if lit != req {
198 return None;
199 }
200 }
201 Segment::Param(name) => {
202 params.insert(name.clone(), (*req).to_string());
203 }
204 }
205 }
206 Some(params)
207}
208
209fn match_when(
211 when: Option<&RequestMatch>,
212 query: &HashMap<String, String>,
213 headers: &HashMap<String, String>,
214 body: &Value,
215) -> bool {
216 let Some(when) = when else {
217 return true;
218 };
219 when.query
220 .iter()
221 .all(|(k, v)| query.get(k).map(|actual| actual == v).unwrap_or(false))
222 && when.headers.iter().all(|(k, v)| {
223 let lower = k.to_ascii_lowercase();
224 headers
225 .get(&lower)
226 .map(|actual| actual.eq_ignore_ascii_case(v))
227 .unwrap_or(false)
228 })
229 && when
230 .body
231 .as_ref()
232 .map(|pattern| body_matches(pattern, body))
233 .unwrap_or(true)
234}
235
236fn body_matches(pattern: &Value, actual: &Value) -> bool {
242 match (pattern, actual) {
243 (Value::Object(pattern), Value::Object(actual)) => pattern.iter().all(|(key, value)| {
244 actual
245 .get(key)
246 .map(|a| body_matches(value, a))
247 .unwrap_or(false)
248 }),
249 (Value::Array(pattern), Value::Array(actual)) => {
250 pattern.len() == actual.len()
251 && pattern.iter().zip(actual).all(|(p, a)| body_matches(p, a))
252 }
253 _ => pattern == actual,
254 }
255}
256
257#[derive(Debug, thiserror::Error)]
259pub enum RouterError {
260 #[error("invalid path pattern in route {route_index}: {source}")]
262 InvalidPath {
263 route_index: usize,
264 #[source]
265 source: PathError,
266 },
267
268 #[error("empty `sequence` in route {route_index}; expected at least one item")]
270 EmptySequence { route_index: usize },
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
275pub enum PathError {
276 #[error("invalid path parameter `{0}`")]
278 InvalidParam(String),
279 #[error("unbalanced braces in segment `{0}`")]
281 UnbalancedBraces(String),
282 #[error("path pattern is empty")]
284 EmptyPattern,
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::config::{Method, RequestMatch, ResponseConfig, ResponseSpec};
291 use serde_json::json;
292
293 fn route(method: Method, path: &str) -> Route {
294 Route {
295 method,
296 path: path.to_string(),
297 when: None,
298 response: ResponseSpec::Single(ResponseConfig::default()),
299 }
300 }
301
302 fn route_with_status(method: Method, path: &str, status: u16) -> Route {
304 let mut r = route(method, path);
305 if let ResponseSpec::Single(resp) = &mut r.response {
306 resp.status = status;
307 }
308 r
309 }
310
311 fn empty_inputs() -> (HashMap<String, String>, HashMap<String, String>, Value) {
312 (HashMap::new(), HashMap::new(), Value::Null)
313 }
314
315 #[test]
316 fn literal_path_matches() {
317 let router = Router::new(vec![route(Method::Get, "/users")]).unwrap();
318 let (q, h, b) = empty_inputs();
319 let m = router.resolve(Method::Get, "/users", &q, &h, &b);
320 assert!(m.is_some());
321 }
322
323 #[test]
324 fn leading_slash_optional() {
325 let router = Router::new(vec![route(Method::Get, "/users")]).unwrap();
326 let (q, h, b) = empty_inputs();
327 assert!(router.resolve(Method::Get, "users", &q, &h, &b).is_some());
328 }
329
330 #[test]
331 fn method_must_match() {
332 let router = Router::new(vec![route(Method::Get, "/users")]).unwrap();
333 let (q, h, b) = empty_inputs();
334 assert!(router.resolve(Method::Post, "/users", &q, &h, &b).is_none());
335 }
336
337 #[test]
338 fn captures_path_params() {
339 let router = Router::new(vec![route(Method::Get, "/users/{id}/items/{itemId}")]).unwrap();
340 let (q, h, b) = empty_inputs();
341 let m = router
342 .resolve(Method::Get, "/users/42/items/7", &q, &h, &b)
343 .unwrap();
344 assert_eq!(m.path_params.get("id").unwrap(), "42");
345 assert_eq!(m.path_params.get("itemId").unwrap(), "7");
346 }
347
348 #[test]
349 fn segment_count_must_match() {
350 let router = Router::new(vec![route(Method::Get, "/users/{id}")]).unwrap();
351 let (q, h, b) = empty_inputs();
352 assert!(router
353 .resolve(Method::Get, "/users/42/items", &q, &h, &b)
354 .is_none());
355 }
356
357 #[test]
358 fn first_match_wins() {
359 let r1 = route_with_status(Method::Get, "/users/{id}", 200);
360 let r2 = route_with_status(Method::Get, "/users/{id}", 201);
361 let router = Router::new(vec![r1, r2]).unwrap();
362 let (q, h, b) = empty_inputs();
363 let m = router.resolve(Method::Get, "/users/1", &q, &h, &b).unwrap();
364 assert_eq!(m.response.status, 200);
365 }
366
367 #[test]
368 fn matches_query_param() {
369 let mut r = route(Method::Get, "/users");
370 r.when = Some(RequestMatch {
371 query: [("role".to_string(), "admin".to_string())].into(),
372 ..Default::default()
373 });
374 let router = Router::new(vec![r]).unwrap();
375 let (mut q, h, b) = empty_inputs();
376 assert!(router.resolve(Method::Get, "/users", &q, &h, &b).is_none());
377 q.insert("role".into(), "admin".into());
378 assert!(router.resolve(Method::Get, "/users", &q, &h, &b).is_some());
379 }
380
381 #[test]
382 fn matches_header_case_insensitively() {
383 let mut r = route(Method::Get, "/users");
384 r.when = Some(RequestMatch {
385 headers: [("X-Tenant-Id".to_string(), "tenant-a".to_string())].into(),
386 ..Default::default()
387 });
388 let router = Router::new(vec![r]).unwrap();
389 let (q, mut h, b) = empty_inputs();
390 h.insert("x-tenant-id".into(), "TENANT-A".into());
391 assert!(router.resolve(Method::Get, "/users", &q, &h, &b).is_some());
392 }
393
394 #[test]
395 fn matches_body_subset() {
396 let mut r = route(Method::Post, "/login");
397 r.when = Some(RequestMatch {
398 body: Some(json!({"username": "admin"})),
399 ..Default::default()
400 });
401 let router = Router::new(vec![r]).unwrap();
402 let (q, h, _) = empty_inputs();
403 let body = json!({"username": "admin", "password": "secret"});
404 assert!(router
405 .resolve(Method::Post, "/login", &q, &h, &body)
406 .is_some());
407 let other = json!({"username": "guest"});
408 assert!(router
409 .resolve(Method::Post, "/login", &q, &h, &other)
410 .is_none());
411 }
412
413 #[test]
414 fn when_block_can_disambiguate_same_path() {
415 let mut admin = route_with_status(Method::Get, "/users", 201);
419 admin.when = Some(RequestMatch {
420 query: [("role".to_string(), "admin".to_string())].into(),
421 ..Default::default()
422 });
423 let generic = route_with_status(Method::Get, "/users", 200);
424 let router = Router::new(vec![admin, generic]).unwrap();
425
426 let (mut q, h, b) = empty_inputs();
427 q.insert("role".into(), "admin".into());
428 let m = router.resolve(Method::Get, "/users", &q, &h, &b).unwrap();
429 assert_eq!(m.response.status, 201);
430
431 q.clear();
432 let m = router.resolve(Method::Get, "/users", &q, &h, &b).unwrap();
433 assert_eq!(m.response.status, 200);
434 }
435
436 #[test]
437 fn rejects_invalid_path_pattern_empty_param() {
438 let routes = vec![route(Method::Get, "/users/{}")];
439 assert!(Router::new(routes).is_err());
440 }
441
442 #[test]
443 fn rejects_invalid_path_pattern_unbalanced() {
444 let routes = vec![route(Method::Get, "/users/{id")];
445 assert!(Router::new(routes).is_err());
446 }
447
448 #[test]
449 fn rejects_empty_pattern() {
450 let routes = vec![route(Method::Get, "/")];
451 assert!(Router::new(routes).is_err());
452 }
453
454 fn sequence_route(method: Method, path: &str, statuses: Vec<u16>) -> Route {
459 let sequence = statuses
460 .into_iter()
461 .map(|status| ResponseConfig {
462 status,
463 ..ResponseConfig::default()
464 })
465 .collect();
466 Route {
467 method,
468 path: path.to_string(),
469 when: None,
470 response: ResponseSpec::Sequence { sequence },
471 }
472 }
473
474 #[test]
475 fn sequence_returns_responses_in_order() {
476 let router = Router::new(vec![sequence_route(
477 Method::Get,
478 "/flaky",
479 vec![500, 500, 200],
480 )])
481 .unwrap();
482 let (q, h, b) = empty_inputs();
483
484 assert_eq!(
485 router
486 .resolve(Method::Get, "/flaky", &q, &h, &b)
487 .unwrap()
488 .response
489 .status,
490 500
491 );
492 assert_eq!(
493 router
494 .resolve(Method::Get, "/flaky", &q, &h, &b)
495 .unwrap()
496 .response
497 .status,
498 500
499 );
500 assert_eq!(
501 router
502 .resolve(Method::Get, "/flaky", &q, &h, &b)
503 .unwrap()
504 .response
505 .status,
506 200
507 );
508 }
509
510 #[test]
511 fn sequence_sticks_on_last_response_after_exhausting() {
512 let router =
513 Router::new(vec![sequence_route(Method::Get, "/retry", vec![500, 200])]).unwrap();
514 let (q, h, b) = empty_inputs();
515
516 router.resolve(Method::Get, "/retry", &q, &h, &b);
518 router.resolve(Method::Get, "/retry", &q, &h, &b);
519
520 for _ in 0..5 {
522 assert_eq!(
523 router
524 .resolve(Method::Get, "/retry", &q, &h, &b)
525 .unwrap()
526 .response
527 .status,
528 200
529 );
530 }
531 }
532
533 #[test]
534 fn sequence_state_is_shared_between_router_clones() {
535 let router = Router::new(vec![sequence_route(Method::Get, "/x", vec![1, 2, 3])]).unwrap();
538 let cloned = router.clone();
539 let (q, h, b) = empty_inputs();
540
541 assert_eq!(
543 router
544 .resolve(Method::Get, "/x", &q, &h, &b)
545 .unwrap()
546 .response
547 .status,
548 1
549 );
550 assert_eq!(
551 cloned
552 .resolve(Method::Get, "/x", &q, &h, &b)
553 .unwrap()
554 .response
555 .status,
556 2
557 );
558 assert_eq!(
559 router
560 .resolve(Method::Get, "/x", &q, &h, &b)
561 .unwrap()
562 .response
563 .status,
564 3
565 );
566 assert_eq!(
568 cloned
569 .resolve(Method::Get, "/x", &q, &h, &b)
570 .unwrap()
571 .response
572 .status,
573 3
574 );
575 }
576
577 #[test]
578 fn empty_sequence_is_rejected_at_compile_time() {
579 let r = Route {
580 method: Method::Get,
581 path: "/x".to_string(),
582 when: None,
583 response: ResponseSpec::Sequence { sequence: vec![] },
584 };
585 let err = Router::new(vec![r]).unwrap_err();
586 assert!(matches!(err, RouterError::EmptySequence { route_index: 0 }));
587 }
588}