Skip to main content

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}