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