Skip to main content

icydb_model/
application.rs

1//! Explicit application-owned normalization and validation composition.
2//!
3//! This module does not perform database admission, persistence, retries, or
4//! accepted-schema constraint evaluation.
5
6use crate::{normalize, validate, visitor::Visitable, visitor::VisitorError};
7
8/// Consuming normalize-then-validate convenience for application values.
9///
10/// The value is normalized first. Validation runs only if normalization
11/// succeeds, and the normalized value is returned only after both traversals
12/// succeed. The method consumes `self`, so it neither clones the value nor
13/// creates a second callback owner.
14pub trait NormalizeAndValidate: Visitable + Sized {
15    /// Normalize and then validate this application-owned value.
16    ///
17    /// # Errors
18    ///
19    /// Returns a typed [`VisitorError`] from the first failing stage. Use
20    /// [`VisitorError::operation`] to distinguish normalization from
21    /// validation without inspecting diagnostic prose.
22    fn normalize_and_validate(mut self) -> Result<Self, VisitorError> {
23        normalize(&mut self)?;
24        validate(&self)?;
25        Ok(self)
26    }
27}
28
29impl<T> NormalizeAndValidate for T where T: Visitable {}
30
31#[cfg(test)]
32mod tests {
33    use std::{cell::Cell, rc::Rc};
34
35    use crate::visitor::{
36        ApplicationOperation, NormalizeAuto, NormalizeCustom, ValidateAuto, ValidateCustom,
37        Visitable, VisitorContext,
38    };
39
40    use super::NormalizeAndValidate as _;
41
42    #[derive(Debug)]
43    struct Probe {
44        value: String,
45        validation_calls: Rc<Cell<u32>>,
46        normalization_fails: bool,
47    }
48
49    impl Visitable for Probe {}
50
51    impl NormalizeAuto for Probe {
52        fn normalize_self(&mut self, ctx: &mut dyn VisitorContext) {
53            self.value.make_ascii_lowercase();
54            if self.normalization_fails {
55                ctx.issue("normalization rejected");
56            }
57        }
58    }
59
60    impl NormalizeCustom for Probe {}
61
62    impl ValidateAuto for Probe {
63        fn validate_self(&self, ctx: &mut dyn VisitorContext) {
64            self.validation_calls
65                .set(self.validation_calls.get().saturating_add(1));
66            if self.value != "accepted" {
67                ctx.issue("validation rejected");
68            }
69        }
70    }
71
72    impl ValidateCustom for Probe {}
73
74    #[test]
75    fn composition_normalizes_before_validation_without_cloning() {
76        let validation_calls = Rc::new(Cell::new(0));
77        let probe = Probe {
78            value: "ACCEPTED".to_string(),
79            validation_calls: Rc::clone(&validation_calls),
80            normalization_fails: false,
81        };
82
83        let normalized = probe
84            .normalize_and_validate()
85            .expect("normalized value should validate");
86
87        assert_eq!(normalized.value, "accepted");
88        assert_eq!(validation_calls.get(), 1);
89    }
90
91    #[test]
92    fn composition_stops_before_validation_when_normalization_fails() {
93        let validation_calls = Rc::new(Cell::new(0));
94        let probe = Probe {
95            value: "ACCEPTED".to_string(),
96            validation_calls: Rc::clone(&validation_calls),
97            normalization_fails: true,
98        };
99
100        let error = probe
101            .normalize_and_validate()
102            .expect_err("normalization issue should stop composition");
103
104        assert_eq!(error.operation(), ApplicationOperation::Normalize);
105        assert_eq!(validation_calls.get(), 0);
106    }
107
108    #[test]
109    fn composition_reports_validation_as_the_failing_stage() {
110        let validation_calls = Rc::new(Cell::new(0));
111        let probe = Probe {
112            value: "REJECTED".to_string(),
113            validation_calls: Rc::clone(&validation_calls),
114            normalization_fails: false,
115        };
116
117        let error = probe
118            .normalize_and_validate()
119            .expect_err("normalized rejected value should fail validation");
120
121        assert_eq!(error.operation(), ApplicationOperation::Validate);
122        assert_eq!(validation_calls.get(), 1);
123    }
124}