Skip to main content

apollo_smith/
random.rs

1use arbitrary::Unstructured;
2use rand::RngExt;
3
4const ALPHANUM_CHARS: &[char; 62] = &[
5    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
6    'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
7    'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
8    '5', '6', '7', '8', '9',
9];
10
11/// Error type for response generation.
12#[derive(Debug, thiserror::Error)]
13pub enum ResponseError {
14    /// The randomness source attempted to choose from an empty range.
15    #[error("randomness source attempted to choose from an empty range")]
16    EmptyChoose,
17    /// The randomness source was exhausted or produced invalid data.
18    #[error("randomness source exhausted or produced invalid data")]
19    Exhausted,
20    /// The randomness source produced data that could not be converted to the expected format.
21    #[error("invalid format: {0}")]
22    InvalidFormat(String),
23}
24
25/// Abstraction over a source of randomness for response generation.
26///
27/// Implementations are provided for [`Unstructured`] (for fuzz testing) and
28/// for any type implementing [`rand::Rng`] via the [`RandProvider`] newtype.
29pub trait RandomProvider {
30    /// Generate a random boolean.
31    fn gen_bool(&mut self) -> Result<bool, ResponseError>;
32
33    /// Generate a random `i32` within the inclusive range `[min, max]`.
34    fn gen_i32_range(&mut self, min: i32, max: i32) -> Result<i32, ResponseError>;
35
36    /// Generate a random `usize` within the inclusive range `[min, max]`.
37    fn gen_usize_range(&mut self, min: usize, max: usize) -> Result<usize, ResponseError>;
38
39    /// Generate a random `f64` within the inclusive range `[min, max]`.
40    fn gen_f64_range(&mut self, min: f64, max: f64) -> Result<f64, ResponseError>;
41
42    /// Generate a random alphanumeric character (`[0-9a-zA-Z]`).
43    fn gen_alphanumeric_char(&mut self) -> Result<char, ResponseError>;
44
45    /// Choose a random index in `0..len`. Returns an error if `len == 0`.
46    fn choose_index(&mut self, len: usize) -> Result<usize, ResponseError>;
47
48    /// Return `true` with probability `numerator / denominator`. Panics if `numerator == 0` or `numerator > denominator`.
49    fn ratio(&mut self, numerator: u32, denominator: u32) -> Result<bool, ResponseError>;
50}
51
52impl RandomProvider for Unstructured<'_> {
53    fn gen_bool(&mut self) -> Result<bool, ResponseError> {
54        self.arbitrary::<bool>()
55            .map_err(|_| ResponseError::Exhausted)
56    }
57
58    fn gen_i32_range(&mut self, min: i32, max: i32) -> Result<i32, ResponseError> {
59        self.int_in_range(min..=max)
60            .map_err(|_| ResponseError::Exhausted)
61    }
62
63    fn gen_usize_range(&mut self, min: usize, max: usize) -> Result<usize, ResponseError> {
64        self.int_in_range(min..=max)
65            .map_err(|_| ResponseError::Exhausted)
66    }
67
68    fn gen_f64_range(&mut self, min: f64, max: f64) -> Result<f64, ResponseError> {
69        // Unstructured doesn't support float ranges, so we generate a raw f64
70        // and map it into [min, max].
71        let raw: u32 = self.arbitrary().map_err(|_| ResponseError::Exhausted)?;
72        let fraction = (raw as f64) / (u32::MAX as f64); // [0.0, 1.0]
73        Ok(min + fraction * (max - min))
74    }
75
76    fn gen_alphanumeric_char(&mut self) -> Result<char, ResponseError> {
77        self.choose(ALPHANUM_CHARS)
78            .map_err(|_| ResponseError::Exhausted)
79            .copied()
80    }
81
82    fn choose_index(&mut self, len: usize) -> Result<usize, ResponseError> {
83        self.choose_index(len)
84            .map_err(|_| ResponseError::EmptyChoose)
85    }
86
87    fn ratio(&mut self, numerator: u32, denominator: u32) -> Result<bool, ResponseError> {
88        self.ratio(numerator, denominator)
89            .map_err(|_| ResponseError::Exhausted)
90    }
91}
92
93/// Newtype wrapper that implements [`RandomProvider`] for any [`rand::Rng`].
94///
95/// # Example
96///
97/// ```ignore
98/// use apollo_smith::RandProvider;
99///
100/// let mut rng = RandProvider(rand::rng());
101/// let response = ResponseBuilder::new(&mut rng, &doc, &schema).build()?;
102/// ```
103pub struct RandProvider<R>(pub R);
104
105impl<R: rand::Rng> RandomProvider for RandProvider<R> {
106    fn gen_bool(&mut self) -> Result<bool, ResponseError> {
107        Ok(self.0.random_bool(0.5))
108    }
109
110    fn gen_i32_range(&mut self, min: i32, max: i32) -> Result<i32, ResponseError> {
111        Ok(self.0.random_range(min..=max))
112    }
113
114    fn gen_usize_range(&mut self, min: usize, max: usize) -> Result<usize, ResponseError> {
115        Ok(self.0.random_range(min..=max))
116    }
117
118    fn gen_f64_range(&mut self, min: f64, max: f64) -> Result<f64, ResponseError> {
119        Ok(self.0.random_range(min..=max))
120    }
121
122    fn gen_alphanumeric_char(&mut self) -> Result<char, ResponseError> {
123        Ok(self.0.sample(rand::distr::Alphanumeric) as char)
124    }
125
126    fn choose_index(&mut self, len: usize) -> Result<usize, ResponseError> {
127        if len == 0 {
128            return Err(ResponseError::EmptyChoose);
129        }
130        Ok(self.0.random_range(0..len))
131    }
132
133    fn ratio(&mut self, numerator: u32, denominator: u32) -> Result<bool, ResponseError> {
134        Ok(self.0.random_ratio(numerator, denominator))
135    }
136}