Skip to main content

domain_key/
proptest_impls.rs

1//! Proptest [`Strategy`] impls and the [`ProptestKeyDomain`] companion trait.
2//!
3//! Enabled by the `proptest` feature flag (implies `std`). Provides property-based
4//! testing support for [`Key<D>`](crate::Key), [`Id<D>`](crate::Id),
5//! [`Uuid<D>`](crate::Uuid) (requires `uuid` feature), and
6//! [`Ulid<D>`](crate::Ulid) (requires `ulid` feature).
7//!
8//! # Using `Key<D>` in proptest
9//!
10//! To use `Key<D>` as a proptest input, implement [`ProptestKeyDomain`] for your
11//! domain type. For most domains, an empty impl is sufficient — the constructive
12//! strategy handles generation automatically:
13//!
14//! ```ignore
15//! use domain_key::{KeyDomain, ProptestKeyDomain};
16//!
17//! #[derive(Debug)]
18//! struct MyDomain;
19//! // ... KeyDomain impl ...
20//!
21//! impl ProptestKeyDomain for MyDomain {}  // empty impl — uses constructive default
22//! ```
23//!
24//! For domains with complex custom validation that the constructive path cannot
25//! fully capture, override [`ProptestKeyDomain::proptest_strategy`] to supply a
26//! complete strategy:
27//!
28//! ```ignore
29//! use domain_key::ProptestKeyDomain;
30//! use proptest::prelude::*;
31//!
32//! impl ProptestKeyDomain for MyCustomDomain {
33//!     fn proptest_strategy() -> Option<proptest::strategy::BoxedStrategy<domain_key::Key<Self>>> {
34//!         let known_valid = vec!["foo", "bar", "baz"];
35//!         Some(proptest::sample::select(known_valid)
36//!             .prop_map(|s| domain_key::Key::new(s).unwrap())
37//!             .boxed())
38//!     }
39//! }
40//! ```
41
42use core::num::NonZeroU64;
43use std::sync::Arc;
44
45use proptest::prelude::*;
46use proptest::strategy::{BoxedStrategy, Just, Strategy};
47use smartstring::alias::String as SmartString;
48
49use crate::domain::{IdDomain, KeyDomain};
50use crate::id::Id;
51use crate::key::Key;
52
53/// Companion trait for using [`Key<D>`] in proptest property tests.
54///
55/// Implement this trait for your domain type to enable proptest `Strategy`
56/// generation of `Key<D>` values. For most domains, an **empty impl** is
57/// sufficient — the default `None` return causes the three-path constructive
58/// strategy to activate automatically.
59///
60/// # When to override
61///
62/// Override [`proptest_strategy`](ProptestKeyDomain::proptest_strategy) only
63/// when your domain has complex custom validation (i.e.,
64/// [`KeyDomain::HAS_CUSTOM_VALIDATION`] is `true`) that the constructive path
65/// cannot satisfy, and [`KeyDomain::examples`] is also empty or insufficient.
66///
67/// # Example — default constructive path
68///
69/// ```ignore
70/// impl ProptestKeyDomain for MyDomain {}  // one line; zero boilerplate
71/// ```
72///
73/// # Example — custom override
74///
75/// ```ignore
76/// impl ProptestKeyDomain for MyDomain {
77///     fn proptest_strategy() -> Option<proptest::strategy::BoxedStrategy<Key<Self>>> {
78///         Some(proptest::sample::select(Self::examples())
79///             .prop_map(|s| Key::new(s).unwrap())
80///             .boxed())
81///     }
82/// }
83/// ```
84///
85/// # Discovering this trait
86///
87/// If your domain implements [`KeyDomain`] with `HAS_CUSTOM_VALIDATION = true`,
88/// you must either provide non-empty [`KeyDomain::examples()`] or implement this
89/// trait with a custom [`proptest_strategy()`](ProptestKeyDomain::proptest_strategy)
90/// override to guarantee that proptest generates valid keys.
91pub trait ProptestKeyDomain: KeyDomain {
92    /// Returns a custom proptest `Strategy` for generating `Key<Self>` values,
93    /// or `None` to use the default three-path constructive strategy.
94    ///
95    /// The default implementation returns `None`, which activates the
96    /// constructive path in the `Strategy for Key<D>` impl.
97    ///
98    /// Override this method when:
99    /// - [`KeyDomain::HAS_CUSTOM_VALIDATION`] is `true`, **and**
100    /// - [`KeyDomain::examples`] is empty or insufficient to cover your domain,
101    ///   **and**
102    /// - You can provide a strategy that produces only valid keys.
103    fn proptest_strategy() -> Option<BoxedStrategy<Key<Self>>>
104    where
105        Self: Sized,
106    {
107        None
108    }
109}
110
111// ── Id<D> ────────────────────────────────────────────────────────────────────
112
113impl<D: IdDomain> proptest::arbitrary::Arbitrary for Id<D> {
114    type Parameters = ();
115    type Strategy = BoxedStrategy<Self>;
116
117    fn arbitrary_with(_: ()) -> BoxedStrategy<Self> {
118        any::<u64>()
119            .prop_map(|raw| Id::from_non_zero(NonZeroU64::new(raw | 1).expect("raw | 1 != 0")))
120            .boxed()
121    }
122}
123
124// ── Uuid<D> ──────────────────────────────────────────────────────────────────
125
126/// Proptest `Arbitrary` for [`Uuid<D>`](crate::Uuid).
127///
128/// Generates a uniformly random 16-byte UUID. Requires both `uuid` and `proptest` features.
129#[cfg(feature = "uuid")]
130impl<D: crate::domain::UuidDomain> proptest::arbitrary::Arbitrary
131    for crate::uuid::Uuid<D>
132{
133    type Parameters = ();
134    type Strategy = BoxedStrategy<Self>;
135
136    fn arbitrary_with(_: ()) -> BoxedStrategy<Self> {
137        any::<[u8; 16]>()
138            .prop_map(|bytes| Self::from_bytes(bytes))
139            .boxed()
140    }
141}
142
143// ── Ulid<D> ──────────────────────────────────────────────────────────────────
144
145/// Proptest `Arbitrary` for [`Ulid<D>`](crate::Ulid).
146///
147/// Generates a uniformly random 16-byte ULID. Requires both `ulid` and `proptest` features.
148#[cfg(feature = "ulid")]
149impl<D: crate::domain::UlidDomain> proptest::arbitrary::Arbitrary
150    for crate::ulid::Ulid<D>
151{
152    type Parameters = ();
153    type Strategy = BoxedStrategy<Self>;
154
155    fn arbitrary_with(_: ()) -> BoxedStrategy<Self> {
156        any::<[u8; 16]>()
157            .prop_map(|bytes| Self::from_bytes(bytes))
158            .boxed()
159    }
160}
161
162// ── Key<D> ───────────────────────────────────────────────────────────────────
163
164/// Builds a [`Key<D>`] from a slice of `usize` decision indices, one per character
165/// position. Used by the proptest strategy to turn a `Vec<usize>` into a concrete
166/// key value in a deterministic, shrink-friendly way.
167fn build_key_from_indices<D: KeyDomain>(
168    alphabet: &[char],
169    start_chars: &[char],
170    indices: &[usize],
171) -> Option<Key<D>> {
172    if indices.is_empty() {
173        return None;
174    }
175    let length = indices.len();
176    let mut result = SmartString::new();
177    let mut prev: Option<char> = None;
178
179    for (pos, &idx) in indices.iter().enumerate() {
180        let is_last = pos == length - 1;
181
182        let candidates: Vec<char> = match pos {
183            0 if is_last => start_chars
184                .iter()
185                .copied()
186                .filter(|&c| D::allowed_end_character(c))
187                .collect(),
188            0 => start_chars.to_vec(),
189            _ => {
190                let p = prev.expect("prev is set after position 0");
191                alphabet
192                    .iter()
193                    .copied()
194                    .filter(|&c| D::allowed_consecutive_characters(p, c))
195                    .filter(|&c| !is_last || D::allowed_end_character(c))
196                    .collect()
197            }
198        };
199
200        if candidates.is_empty() {
201            return None;
202        }
203
204        let c = candidates[idx % candidates.len()];
205        result.push(c);
206        prev = Some(c);
207    }
208
209    Key::new(result.as_str()).ok()
210}
211
212/// Returns the constructive proptest strategy for `Key<D: ProptestKeyDomain>`.
213///
214/// This is the innermost "Path 3" strategy used when no override is provided
215/// and the domain has no examples.
216fn key_constructive_strategy<D: ProptestKeyDomain>() -> BoxedStrategy<Key<D>>
217where
218    Key<D>: core::fmt::Debug,
219{
220    let min_len = D::min_length().max(1);
221    let max_len = D::MAX_LENGTH.max(min_len);
222
223    let alphabet: Arc<Vec<char>> = Arc::new(
224        (' '..='~')
225            .filter(|&c| D::allowed_characters(c))
226            .collect(),
227    );
228    let start_chars: Arc<Vec<char>> = Arc::new(
229        alphabet
230            .iter()
231            .copied()
232            .filter(|&c| D::allowed_start_character(c))
233            .collect(),
234    );
235
236    (min_len..=max_len)
237        .prop_flat_map(move |len| {
238            let a = Arc::clone(&alphabet);
239            let s = Arc::clone(&start_chars);
240            proptest::collection::vec(any::<usize>(), len).prop_map(move |indices| {
241                build_key_from_indices::<D>(&a, &s, &indices)
242            })
243        })
244        .prop_filter_map("non-empty candidate set", |opt| opt)
245        .boxed()
246}
247
248impl<D: ProptestKeyDomain> proptest::arbitrary::Arbitrary for Key<D>
249where
250    Key<D>: core::fmt::Debug,
251{
252    type Parameters = ();
253    type Strategy = BoxedStrategy<Key<D>>;
254
255    fn arbitrary_with(_: ()) -> BoxedStrategy<Key<D>> {
256        // Path 1: domain-supplied override strategy.
257        if let Some(s) = D::proptest_strategy() {
258            return s;
259        }
260
261        // Path 2: examples-weighted strategy for custom-validation domains.
262        if D::HAS_CUSTOM_VALIDATION {
263            let examples = D::examples();
264            if !examples.is_empty() {
265                let mut all_strats: Vec<BoxedStrategy<Key<D>>> = examples
266                    .iter()
267                    .map(|&s| {
268                        let key = Key::from(SmartString::from(s));
269                        Just(key).boxed()
270                    })
271                    .collect();
272                // Include the constructive path for coverage beyond the examples list.
273                all_strats.push(key_constructive_strategy::<D>());
274                return proptest::strategy::Union::new(all_strats).boxed();
275            }
276        }
277
278        // Path 3: pure constructive strategy.
279        key_constructive_strategy::<D>()
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use proptest::prelude::*;
286
287    use crate::domain::{Domain, KeyDomain};
288    use crate::key::Key;
289
290    use super::ProptestKeyDomain;
291
292    #[derive(Debug, Clone)]
293    struct AlphaLowerDomain;
294
295    impl Domain for AlphaLowerDomain {
296        const DOMAIN_NAME: &'static str = "alpha_lower";
297    }
298
299    impl KeyDomain for AlphaLowerDomain {
300        const MAX_LENGTH: usize = 12;
301
302        fn allowed_characters(c: char) -> bool {
303            c.is_ascii_lowercase()
304        }
305    }
306
307    impl ProptestKeyDomain for AlphaLowerDomain {}
308
309    proptest! {
310        #[test]
311        fn key_strategy_produces_valid_keys(key in any::<Key<AlphaLowerDomain>>()) {
312            prop_assert!(Key::<AlphaLowerDomain>::new(key.as_str()).is_ok());
313        }
314
315        #[test]
316        fn key_strategy_respects_length_bounds(key in any::<Key<AlphaLowerDomain>>()) {
317            let len = key.len();
318            prop_assert!(
319                len >= AlphaLowerDomain::min_length() && len <= AlphaLowerDomain::MAX_LENGTH,
320                "key length {len} out of bounds"
321            );
322        }
323    }
324
325    #[test]
326    fn examples_domain_path2_runs_without_panic() {
327        // Verify Path 2 (examples-weighted) doesn't panic and produces valid keys.
328        #[derive(Debug, Clone)]
329        struct ExDomain;
330        impl Domain for ExDomain {
331            const DOMAIN_NAME: &'static str = "ex";
332        }
333        impl KeyDomain for ExDomain {
334            const MAX_LENGTH: usize = 16;
335            const HAS_CUSTOM_VALIDATION: bool = true;
336            fn examples() -> &'static [&'static str] {
337                &["alpha", "beta", "gamma"]
338            }
339            fn allowed_characters(c: char) -> bool {
340                c.is_ascii_lowercase()
341            }
342        }
343        impl ProptestKeyDomain for ExDomain {}
344
345        let mut runner = proptest::test_runner::TestRunner::default();
346        let strategy = any::<Key<ExDomain>>();
347        let valid_examples: &[&str] = ExDomain::examples();
348        let mut from_examples = 0usize;
349        let mut from_constructive = 0usize;
350        for _ in 0..50 {
351            let tree = strategy.new_tree(&mut runner).unwrap();
352            let key = tree.current();
353            // All keys must pass re-validation for this simple domain.
354            assert!(
355                Key::<ExDomain>::new(key.as_str()).is_ok(),
356                "generated key failed validation: {key:?}"
357            );
358            if valid_examples.contains(&key.as_str()) {
359                from_examples += 1;
360            } else {
361                from_constructive += 1;
362            }
363        }
364        // Both paths should contribute.
365        assert!(from_examples > 0, "no keys from examples");
366        let _ = from_constructive; // constructive may also produce valid keys here
367    }
368}
369