Skip to main content

backbone_core/
specification.rs

1//! Specification Pattern - Business Rules as Objects
2//!
3//! The Specification pattern encapsulates business rules that can be combined
4//! and reused. This is a core DDD tactical pattern.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use backbone_core::specification::{Specification, AndSpecification};
10//!
11//! // Define a specification
12//! struct UserIsActive;
13//!
14//! impl Specification<User> for UserIsActive {
15//!     type Error = String;
16//!
17//!     fn is_satisfied_by(&self, user: &User) -> Result<bool, Self::Error> {
18//!         Ok(user.status == UserStatus::Active)
19//!     }
20//! }
21//!
22//! // Combine specifications
23//! let spec = UserIsActive.and(UserHasVerifiedEmail);
24//! let is_valid = spec.is_satisfied_by(&user)?;
25//! ```
26
27use std::collections::HashMap;
28use std::fmt::Debug;
29use std::marker::PhantomData;
30
31/// Core Specification trait
32///
33/// Specifications encapsulate business rules that can be:
34/// - Combined with AND, OR, NOT operators
35/// - Reused across different parts of the application
36/// - Tested in isolation
37///
38/// # Type Parameters
39///
40/// - `T`: The type of entity being validated
41pub trait Specification<T>: Send + Sync {
42    /// Error type for validation failures
43    type Error: Debug + Send;
44
45    /// Check if the candidate satisfies this specification
46    fn is_satisfied_by(&self, candidate: &T) -> Result<bool, Self::Error>;
47
48    /// Combine with another specification using AND
49    fn and<S>(self, other: S) -> AndSpecification<Self, S, T>
50    where
51        Self: Sized,
52        S: Specification<T>,
53    {
54        AndSpecification::new(self, other)
55    }
56
57    /// Combine with another specification using OR
58    fn or<S>(self, other: S) -> OrSpecification<Self, S, T>
59    where
60        Self: Sized,
61        S: Specification<T>,
62    {
63        OrSpecification::new(self, other)
64    }
65
66    /// Negate this specification
67    fn not(self) -> NotSpecification<Self, T>
68    where
69        Self: Sized,
70    {
71        NotSpecification::new(self)
72    }
73}
74
75// ============================================================================
76// Composite Specifications
77// ============================================================================
78
79/// AND specification - both left and right must be satisfied
80#[derive(Debug, Clone)]
81pub struct AndSpecification<L, R, T> {
82    left: L,
83    right: R,
84    _marker: PhantomData<T>,
85}
86
87impl<L, R, T> AndSpecification<L, R, T> {
88    pub fn new(left: L, right: R) -> Self {
89        Self {
90            left,
91            right,
92            _marker: PhantomData,
93        }
94    }
95}
96
97impl<L, R, T> Specification<T> for AndSpecification<L, R, T>
98where
99    L: Specification<T>,
100    R: Specification<T>,
101    T: Send + Sync,
102{
103    type Error = String;
104
105    fn is_satisfied_by(&self, candidate: &T) -> Result<bool, Self::Error> {
106        let left_result = self
107            .left
108            .is_satisfied_by(candidate)
109            .map_err(|e| format!("Left specification failed: {:?}", e))?;
110
111        if !left_result {
112            return Ok(false);
113        }
114
115        self.right
116            .is_satisfied_by(candidate)
117            .map_err(|e| format!("Right specification failed: {:?}", e))
118    }
119}
120
121/// OR specification - either left or right must be satisfied
122#[derive(Debug, Clone)]
123pub struct OrSpecification<L, R, T> {
124    left: L,
125    right: R,
126    _marker: PhantomData<T>,
127}
128
129impl<L, R, T> OrSpecification<L, R, T> {
130    pub fn new(left: L, right: R) -> Self {
131        Self {
132            left,
133            right,
134            _marker: PhantomData,
135        }
136    }
137}
138
139impl<L, R, T> Specification<T> for OrSpecification<L, R, T>
140where
141    L: Specification<T>,
142    R: Specification<T>,
143    T: Send + Sync,
144{
145    type Error = String;
146
147    fn is_satisfied_by(&self, candidate: &T) -> Result<bool, Self::Error> {
148        let left_result = self
149            .left
150            .is_satisfied_by(candidate)
151            .map_err(|e| format!("Left specification failed: {:?}", e))?;
152
153        if left_result {
154            return Ok(true);
155        }
156
157        self.right
158            .is_satisfied_by(candidate)
159            .map_err(|e| format!("Right specification failed: {:?}", e))
160    }
161}
162
163/// NOT specification - negates the inner specification
164#[derive(Debug, Clone)]
165pub struct NotSpecification<S, T> {
166    spec: S,
167    _marker: PhantomData<T>,
168}
169
170impl<S, T> NotSpecification<S, T> {
171    pub fn new(spec: S) -> Self {
172        Self {
173            spec,
174            _marker: PhantomData,
175        }
176    }
177}
178
179impl<S, T> Specification<T> for NotSpecification<S, T>
180where
181    S: Specification<T>,
182    T: Send + Sync,
183{
184    type Error = String;
185
186    fn is_satisfied_by(&self, candidate: &T) -> Result<bool, Self::Error> {
187        let result = self
188            .spec
189            .is_satisfied_by(candidate)
190            .map_err(|e| format!("Inner specification failed: {:?}", e))?;
191        Ok(!result)
192    }
193}
194
195// ============================================================================
196// Specification Result
197// ============================================================================
198
199/// Result of evaluating a specification with detailed information
200#[derive(Debug, Clone)]
201pub struct SpecificationResult {
202    /// Whether the specification was satisfied
203    pub satisfied: bool,
204    /// Name of the specification
205    pub specification_name: String,
206    /// Human-readable message
207    pub message: String,
208    /// Additional details
209    pub details: HashMap<String, String>,
210}
211
212impl SpecificationResult {
213    /// Create a satisfied result
214    pub fn satisfied(name: impl Into<String>, message: impl Into<String>) -> Self {
215        Self {
216            satisfied: true,
217            specification_name: name.into(),
218            message: message.into(),
219            details: HashMap::new(),
220        }
221    }
222
223    /// Create an unsatisfied result
224    pub fn unsatisfied(name: impl Into<String>, message: impl Into<String>) -> Self {
225        Self {
226            satisfied: false,
227            specification_name: name.into(),
228            message: message.into(),
229            details: HashMap::new(),
230        }
231    }
232
233    /// Add a detail
234    pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
235        self.details.insert(key.into(), value.into());
236        self
237    }
238}
239
240// ============================================================================
241// Specification Evaluator
242// ============================================================================
243
244/// Evaluates multiple specifications and collects results
245pub struct SpecificationEvaluator<T> {
246    _marker: PhantomData<T>,
247}
248
249impl<T: Send + Sync> SpecificationEvaluator<T> {
250    /// Evaluate all specifications and return all results
251    pub fn evaluate_all<S>(
252        specifications: &[&S],
253        candidate: &T,
254    ) -> Vec<Result<bool, String>>
255    where
256        S: Specification<T, Error = String> + ?Sized,
257    {
258        specifications
259            .iter()
260            .map(|spec| spec.is_satisfied_by(candidate))
261            .collect()
262    }
263
264    /// Check if all specifications are satisfied
265    pub fn all_satisfied<S>(specifications: &[&S], candidate: &T) -> Result<bool, String>
266    where
267        S: Specification<T, Error = String> + ?Sized,
268    {
269        for spec in specifications {
270            if !spec.is_satisfied_by(candidate)? {
271                return Ok(false);
272            }
273        }
274        Ok(true)
275    }
276
277    /// Check if any specification is satisfied
278    pub fn any_satisfied<S>(specifications: &[&S], candidate: &T) -> Result<bool, String>
279    where
280        S: Specification<T, Error = String> + ?Sized,
281    {
282        for spec in specifications {
283            if spec.is_satisfied_by(candidate)? {
284                return Ok(true);
285            }
286        }
287        Ok(false)
288    }
289}
290
291// ============================================================================
292// Common Specifications
293// ============================================================================
294
295/// Always returns true
296#[derive(Debug, Clone, Default)]
297pub struct AlwaysTrue<T>(PhantomData<T>);
298
299impl<T> AlwaysTrue<T> {
300    pub fn new() -> Self {
301        Self(PhantomData)
302    }
303}
304
305impl<T: Send + Sync> Specification<T> for AlwaysTrue<T> {
306    type Error = std::convert::Infallible;
307
308    fn is_satisfied_by(&self, _candidate: &T) -> Result<bool, Self::Error> {
309        Ok(true)
310    }
311}
312
313/// Always returns false
314#[derive(Debug, Clone, Default)]
315pub struct AlwaysFalse<T>(PhantomData<T>);
316
317impl<T> AlwaysFalse<T> {
318    pub fn new() -> Self {
319        Self(PhantomData)
320    }
321}
322
323impl<T: Send + Sync> Specification<T> for AlwaysFalse<T> {
324    type Error = std::convert::Infallible;
325
326    fn is_satisfied_by(&self, _candidate: &T) -> Result<bool, Self::Error> {
327        Ok(false)
328    }
329}
330
331/// Specification using a closure
332pub struct PredicateSpecification<T, F>
333where
334    F: Fn(&T) -> bool + Send + Sync,
335{
336    predicate: F,
337    _marker: PhantomData<T>,
338}
339
340impl<T, F> PredicateSpecification<T, F>
341where
342    F: Fn(&T) -> bool + Send + Sync,
343{
344    pub fn new(predicate: F) -> Self {
345        Self {
346            predicate,
347            _marker: PhantomData,
348        }
349    }
350}
351
352impl<T, F> Specification<T> for PredicateSpecification<T, F>
353where
354    T: Send + Sync,
355    F: Fn(&T) -> bool + Send + Sync,
356{
357    type Error = std::convert::Infallible;
358
359    fn is_satisfied_by(&self, candidate: &T) -> Result<bool, Self::Error> {
360        Ok((self.predicate)(candidate))
361    }
362}
363
364/// Helper function to create a predicate specification
365pub fn predicate<T, F>(f: F) -> PredicateSpecification<T, F>
366where
367    F: Fn(&T) -> bool + Send + Sync,
368{
369    PredicateSpecification::new(f)
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[derive(Debug)]
377    struct TestEntity {
378        value: i32,
379        active: bool,
380    }
381
382    struct PositiveValue;
383    impl Specification<TestEntity> for PositiveValue {
384        type Error = String;
385        fn is_satisfied_by(&self, e: &TestEntity) -> Result<bool, Self::Error> {
386            Ok(e.value > 0)
387        }
388    }
389
390    struct IsActive;
391    impl Specification<TestEntity> for IsActive {
392        type Error = String;
393        fn is_satisfied_by(&self, e: &TestEntity) -> Result<bool, Self::Error> {
394            Ok(e.active)
395        }
396    }
397
398    #[test]
399    fn test_and_specification() {
400        let spec = PositiveValue.and(IsActive);
401
402        let active_positive = TestEntity { value: 10, active: true };
403        assert!(spec.is_satisfied_by(&active_positive).unwrap());
404
405        let inactive_positive = TestEntity { value: 10, active: false };
406        assert!(!spec.is_satisfied_by(&inactive_positive).unwrap());
407
408        let active_negative = TestEntity { value: -5, active: true };
409        assert!(!spec.is_satisfied_by(&active_negative).unwrap());
410    }
411
412    #[test]
413    fn test_or_specification() {
414        let spec = PositiveValue.or(IsActive);
415
416        let inactive_positive = TestEntity { value: 10, active: false };
417        assert!(spec.is_satisfied_by(&inactive_positive).unwrap());
418
419        let active_negative = TestEntity { value: -5, active: true };
420        assert!(spec.is_satisfied_by(&active_negative).unwrap());
421
422        let inactive_negative = TestEntity { value: -5, active: false };
423        assert!(!spec.is_satisfied_by(&inactive_negative).unwrap());
424    }
425
426    #[test]
427    fn test_not_specification() {
428        let spec = PositiveValue.not();
429
430        let positive = TestEntity { value: 10, active: false };
431        assert!(!spec.is_satisfied_by(&positive).unwrap());
432
433        let negative = TestEntity { value: -5, active: false };
434        assert!(spec.is_satisfied_by(&negative).unwrap());
435    }
436
437    #[test]
438    fn test_predicate_specification() {
439        let spec = predicate(|e: &TestEntity| e.value > 5);
440
441        let high = TestEntity { value: 10, active: false };
442        assert!(spec.is_satisfied_by(&high).unwrap());
443
444        let low = TestEntity { value: 3, active: false };
445        assert!(!spec.is_satisfied_by(&low).unwrap());
446    }
447
448    #[test]
449    fn test_complex_composition() {
450        // (PositiveValue AND IsActive) OR (value > 100)
451        let spec = PositiveValue
452            .and(IsActive)
453            .or(predicate(|e: &TestEntity| e.value > 100));
454
455        let active_positive = TestEntity { value: 10, active: true };
456        assert!(spec.is_satisfied_by(&active_positive).unwrap());
457
458        let inactive_very_high = TestEntity { value: 200, active: false };
459        assert!(spec.is_satisfied_by(&inactive_very_high).unwrap());
460
461        let inactive_low = TestEntity { value: 5, active: false };
462        assert!(!spec.is_satisfied_by(&inactive_low).unwrap());
463    }
464}