apimock_routing/strategy.rs
1use serde::Deserialize;
2
3/// Rule-evaluation strategy: decides which rule wins when multiple
4/// rules match the same request.
5///
6/// # RFC 007 — Strategy variants
7///
8/// The original `FirstMatch` strategy is unchanged and remains the
9/// default. Three new strategies are added:
10///
11/// - [`UniformRandom`] — pick uniformly at random from all matching rules.
12/// - [`WeightedRandom`] — pick randomly, weighted by each rule's `weight`.
13/// - [`Priority`] — group by priority, apply a tiebreaker within the group.
14#[derive(Clone, Deserialize, Debug, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum Strategy {
17 /// Walk rules in order; return the first that matches. Default.
18 #[default]
19 FirstMatch,
20
21 /// Pick uniformly at random from all matching rules.
22 /// `seed = Some(n)` for reproducible test runs.
23 UniformRandom {
24 #[serde(default)]
25 seed: Option<u64>,
26 },
27
28 /// Pick randomly, weighted by each rule's `weight` field (default 1).
29 WeightedRandom {
30 #[serde(default)]
31 seed: Option<u64>,
32 },
33
34 /// Group matching rules by `priority` (higher wins). Within the
35 /// top-priority group, apply `tiebreaker` (default: `first_match`).
36 Priority {
37 #[serde(default)]
38 tiebreaker: PriorityTiebreaker,
39 },
40 /// Cycle through matching rules in order, one per request.
41 /// State is kept in an `Arc<AtomicUsize>` on the parent `RuleSet`.
42 RoundRobin,
43}
44
45/// Tiebreaker applied within a priority group by [`Strategy::Priority`].
46#[derive(Clone, Deserialize, Debug, Default)]
47#[serde(rename_all = "snake_case")]
48pub enum PriorityTiebreaker {
49 #[default]
50 FirstMatch,
51 UniformRandom,
52}
53
54impl std::fmt::Display for Strategy {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 Self::FirstMatch => write!(f, "first_match"),
58 Self::UniformRandom { .. } => write!(f, "uniform_random"),
59 Self::WeightedRandom { .. } => write!(f, "weighted_random"),
60 Self::Priority { .. } => write!(f, "priority"),
61 Self::RoundRobin => write!(f, "round_robin"),
62 }
63 }
64}
65
66// ── minimal PRNG (no external dep) ───────────────────────────────────
67
68/// xorshift64 PRNG — fast, no-alloc, no external dependency.
69pub struct Xorshift64(u64);
70
71impl Xorshift64 {
72 pub fn new(seed: u64) -> Self {
73 Self(if seed == 0 { 0xdeadbeef_cafebabe } else { seed })
74 }
75
76 // clippy: renaming `next` would change apimock_routing::strategy::
77 // Xorshift64's public API surface; this PRNG helper intentionally does
78 // not implement std::iter::Iterator.
79 #[allow(clippy::should_implement_trait)]
80 pub fn next(&mut self) -> u64 {
81 let mut x = self.0;
82 x ^= x << 13;
83 x ^= x >> 7;
84 x ^= x << 17;
85 self.0 = x;
86 x
87 }
88
89 /// Uniform index in `0..len`.
90 pub fn next_index(&mut self, len: usize) -> usize {
91 (self.next() % len as u64) as usize
92 }
93}
94
95pub fn make_rng(seed: Option<u64>) -> Xorshift64 {
96 let s = seed.unwrap_or_else(|| {
97 std::time::SystemTime::now()
98 .duration_since(std::time::UNIX_EPOCH)
99 .map(|d| d.as_nanos() as u64)
100 .unwrap_or(0xc0ffee)
101 });
102 Xorshift64::new(s)
103}