Skip to main content

hegel/generators/
combinators.rs

1use super::{BasicGenerator, BoxedGenerator, Generator, TestCase, integers, labels};
2use crate::cbor_utils::{cbor_array, cbor_map};
3use ciborium::Value;
4use std::borrow::Cow;
5use std::marker::PhantomData;
6
7/// Generator that picks from a fixed list of values. Created by [`sampled_from()`].
8pub struct SampledFromGenerator<'a, T: Clone> {
9    elements: Cow<'a, [T]>,
10}
11
12impl<'a, T: Clone + Send + Sync + 'a> Generator<T> for SampledFromGenerator<'a, T> {
13    fn do_draw(&self, tc: &TestCase) -> T {
14        if let Some(basic) = self.as_basic() {
15            return basic.do_draw(tc);
16        }
17
18        // nocov start
19        let indices = integers::<usize>()
20            .min_value(0)
21            .max_value(self.elements.len() - 1);
22        let index = indices.do_draw(tc);
23        self.elements[index].clone()
24        // nocov end
25    }
26
27    fn as_basic(&self) -> Option<BasicGenerator<'_, T>> {
28        if self.elements.is_empty() {
29            return None; // nocov
30        }
31
32        let schema = cbor_map! {
33            "type" => "integer",
34            "min_value" => 0u64,
35            "max_value" => (self.elements.len() - 1) as u64
36        };
37        let elements: &[T] = &self.elements;
38        Some(BasicGenerator::new(schema, move |raw| {
39            let index: usize = super::deserialize_value(raw);
40            elements[index].clone()
41        }))
42    }
43
44    fn enumerate_values(&self) -> Option<Vec<T>> {
45        Some(self.elements.to_vec())
46    }
47}
48
49/// Pick from a fixed list of values.
50///
51/// Accepts anything convertible into `Cow<[T]>`, including:
52/// - `Vec<T>` (consumed without re-allocation)
53/// - `&[T]` where `T: Clone` (borrowed, zero allocation)
54/// - `&Vec<T>` or `&[T; N]` (via coercion to `&[T]`)
55///
56/// Panics if `elements` is empty.
57pub fn sampled_from<'a, T, S>(elements: S) -> SampledFromGenerator<'a, T>
58where
59    T: Clone + Send + Sync,
60    S: Into<Cow<'a, [T]>>,
61{
62    let elements = elements.into();
63    assert!(
64        !elements.is_empty(),
65        "Collection passed to sampled_from cannot be empty"
66    );
67    SampledFromGenerator { elements }
68}
69
70/// Generator that chooses from multiple generators. Created by [`one_of()`] or [`one_of!`](crate::one_of).
71pub struct OneOfGenerator<'a, T> {
72    generators: Vec<BoxedGenerator<'a, T>>,
73}
74
75impl<T> Generator<T> for OneOfGenerator<'_, T> {
76    fn do_draw(&self, tc: &TestCase) -> T {
77        if let Some(basic) = self.as_basic() {
78            basic.do_draw(tc)
79        } else {
80            tc.start_span(labels::ONE_OF);
81            let index = integers::<usize>()
82                .min_value(0)
83                .max_value(self.generators.len() - 1)
84                .do_draw(tc);
85            let result = self.generators[index].do_draw(tc);
86            tc.stop_span(false);
87            result
88        }
89    }
90
91    fn as_basic(&self) -> Option<BasicGenerator<'_, T>> {
92        let basics: Vec<BasicGenerator<'_, T>> = self
93            .generators
94            .iter()
95            .map(|g| g.as_basic())
96            .collect::<Option<Vec<_>>>()?;
97
98        let schemas: Vec<Value> = basics.iter().map(|b| b.schema().clone()).collect();
99        let schema = cbor_map! {"type" => "one_of", "generators" => Value::Array(schemas)};
100
101        Some(BasicGenerator::new(schema, move |raw| {
102            // The server returns `[index, value]` for one_of schemas.
103            let [idx, value]: [Value; 2] = raw.into_array().unwrap().try_into().unwrap();
104            let index = i128::from(idx.into_integer().unwrap()) as usize;
105            basics[index].parse_raw(value)
106        }))
107    }
108}
109
110/// Choose from multiple generators of the same type.
111///
112/// Accepts any iterable of boxed generators (e.g. `Vec<BoxedGenerator<T>>`
113/// or an iterator chain). For a more convenient syntax, use the `one_of!`
114/// macro instead.
115pub fn one_of<'a, T, I>(generators: I) -> OneOfGenerator<'a, T>
116where
117    I: IntoIterator<Item = BoxedGenerator<'a, T>>,
118{
119    let generators: Vec<BoxedGenerator<'a, T>> = generators.into_iter().collect();
120    assert!(
121        !generators.is_empty(),
122        "one_of requires at least one generator"
123    );
124    OneOfGenerator { generators }
125}
126
127/// Choose from multiple generators of the same type.
128///
129/// This macro automatically boxes each generator, providing a more ergonomic
130/// syntax than calling [`one_of`] directly.
131///
132/// # Example
133///
134/// ```no_run
135/// use hegel::generators as gs;
136///
137/// #[hegel::test]
138/// fn my_test(tc: &hegel::TestCase) {
139///     let value: i32 = tc.draw(hegel::one_of!(
140///         gs::integers::<i32>().min_value(0).max_value(10),
141///         gs::integers::<i32>().min_value(100).max_value(110),
142///     ));
143/// }
144/// ```
145#[macro_export]
146macro_rules! one_of {
147    ($($generator:expr),+ $(,)?) => {
148        $crate::generators::one_of(vec![
149            $($crate::generators::Generator::boxed($generator)),+
150        ])
151    };
152}
153
154/// Generator that produces `Some(value)` or `None`. Created by [`optional()`].
155pub struct OptionalGenerator<G, T> {
156    inner: G,
157    _phantom: PhantomData<fn(T)>,
158}
159
160impl<T, G> Generator<Option<T>> for OptionalGenerator<G, T>
161where
162    G: Generator<T>,
163{
164    fn do_draw(&self, tc: &TestCase) -> Option<T> {
165        if let Some(basic) = self.as_basic() {
166            basic.do_draw(tc)
167        } else {
168            // nocov start
169            tc.start_span(labels::OPTIONAL);
170            let is_some: bool = super::generate_from_schema(tc, &cbor_map! {"type" => "boolean"});
171            let result = if is_some {
172                Some(self.inner.do_draw(tc))
173            } else {
174                None
175            };
176            tc.stop_span(false);
177            result
178            // nocov end
179        }
180    }
181
182    fn as_basic(&self) -> Option<BasicGenerator<'_, Option<T>>> {
183        let inner_basic = self.inner.as_basic()?;
184        let inner_schema = inner_basic.schema().clone();
185
186        let null_schema = cbor_map! {"type" => "constant", "value" => Value::Null};
187        let schema =
188            cbor_map! {"type" => "one_of", "generators" => cbor_array![null_schema, inner_schema]};
189
190        Some(BasicGenerator::new(schema, move |raw| {
191            // The server returns `[index, value]` for one_of schemas; index 0
192            // selects the null branch (None), 1 selects the inner generator (Some).
193            let [idx, value]: [Value; 2] = raw.into_array().unwrap().try_into().unwrap();
194            if i128::from(idx.into_integer().unwrap()) == 0 {
195                None
196            } else {
197                Some(inner_basic.parse_raw(value))
198            }
199        }))
200    }
201}
202
203/// Generate `Option<T>` values: either `Some(value)` from the inner generator, or `None`.
204pub fn optional<T, G: Generator<T>>(inner: G) -> OptionalGenerator<G, T> {
205    OptionalGenerator {
206        inner,
207        _phantom: PhantomData,
208    }
209}