Skip to main content

icydb_model/node/
type.rs

1use crate::prelude::*;
2
3///
4/// Type
5///
6/// Canonical runtime type descriptor for one schema node's attached normalizers
7/// and validators.
8///
9
10#[derive(Clone, Debug, Serialize)]
11pub struct Type {
12    #[serde(skip_serializing_if = "<[_]>::is_empty")]
13    normalizers: &'static [TypeNormalizer],
14
15    #[serde(skip_serializing_if = "<[_]>::is_empty")]
16    validators: &'static [TypeValidator],
17
18    #[serde(skip_serializing_if = "<[_]>::is_empty")]
19    rules: &'static [SourceRule],
20}
21
22impl Type {
23    #[must_use]
24    pub const fn new(
25        normalizers: &'static [TypeNormalizer],
26        validators: &'static [TypeValidator],
27        rules: &'static [SourceRule],
28    ) -> Self {
29        Self {
30            normalizers,
31            validators,
32            rules,
33        }
34    }
35
36    #[must_use]
37    pub const fn normalizers(&self) -> &'static [TypeNormalizer] {
38        self.normalizers
39    }
40
41    #[must_use]
42    pub const fn validators(&self) -> &'static [TypeValidator] {
43        self.validators
44    }
45
46    /// Borrow explicitly declared durable rules.
47    #[must_use]
48    pub const fn rules(&self) -> &'static [SourceRule] {
49        self.rules
50    }
51}
52
53impl ValidateNode for Type {}
54
55impl VisitableNode for Type {
56    fn drive<V: Visitor>(&self, v: &mut V) {
57        for node in self.normalizers() {
58            node.accept(v);
59        }
60        for node in self.validators() {
61            node.accept(v);
62        }
63        for node in self.rules() {
64            node.accept(v);
65        }
66    }
67}
68
69///
70/// SourceRule
71///
72/// Compiler-authored durable rule template carried by one reusable type.
73/// Fragment lowering instantiates it for each persisted field use; it is not
74/// an application callback or a database runtime evaluator.
75///
76
77#[derive(Clone, Debug, Serialize)]
78pub struct SourceRule {
79    name: &'static str,
80    operation: SourceRuleAuthoringOperation,
81}
82
83impl SourceRule {
84    /// Construct one explicit reusable rule template.
85    #[must_use]
86    pub const fn new(name: &'static str, operation: SourceRuleAuthoringOperation) -> Self {
87        Self { name, operation }
88    }
89
90    /// Return the current declared rule name.
91    #[must_use]
92    pub const fn name(&self) -> &'static str {
93        self.name
94    }
95
96    /// Borrow the frozen rule operation and its named operands.
97    #[must_use]
98    pub const fn operation(&self) -> &SourceRuleAuthoringOperation {
99        &self.operation
100    }
101}
102
103impl ValidateNode for SourceRule {
104    fn validate(&self) -> Result<(), ErrorTree> {
105        let mut errs = ErrorTree::new();
106        validate_source_name(
107            &mut errs,
108            "rule",
109            self.name(),
110            icydb_schema::RuleSourceKey::try_new,
111        );
112        if let Err(message) = self.operation().validate_shape() {
113            err!(errs, "rule '{}': {message}", self.name());
114        }
115        errs.result()
116    }
117}
118
119impl VisitableNode for SourceRule {}
120
121///
122/// SourceRuleAuthoringOperation
123///
124/// Closed authoring vocabulary with operation-specific named operands.
125/// Accepted schema owns runtime evaluation after fragment lowering.
126///
127
128#[derive(Clone, Debug, Serialize)]
129pub enum SourceRuleAuthoringOperation {
130    /// Inclusive character/octet/collection length range.
131    LengthRangeInclusive {
132        /// Inclusive minimum logical length.
133        min: RuleNumber,
134        /// Inclusive maximum logical length.
135        max: RuleNumber,
136    },
137    /// Exact integer or decimal multiple-of divisor.
138    MultipleOf {
139        /// Nonzero exact divisor admitted against the target kind during lowering.
140        divisor: RuleNumber,
141    },
142    /// Inclusive numeric maximum.
143    NumericMaximumInclusive {
144        /// Exact upper-bound literal admitted against the target kind during lowering.
145        value: RuleNumber,
146    },
147    /// Inclusive numeric minimum.
148    NumericMinimumInclusive {
149        /// Exact lower-bound literal admitted against the target kind during lowering.
150        value: RuleNumber,
151    },
152    /// Inclusive numeric range.
153    NumericRangeInclusive {
154        /// Exact lower-bound literal admitted against the target kind during lowering.
155        min: RuleNumber,
156        /// Exact upper-bound literal admitted against the target kind during lowering.
157        max: RuleNumber,
158    },
159}
160
161impl SourceRuleAuthoringOperation {
162    fn validate_shape(&self) -> Result<(), &'static str> {
163        match self {
164            Self::LengthRangeInclusive { min, max } => {
165                let min = rule_length_bound(min).ok_or(
166                    "length_range_inclusive operands must be nonnegative integers within u64",
167                )?;
168                let max = rule_length_bound(max).ok_or(
169                    "length_range_inclusive operands must be nonnegative integers within u64",
170                )?;
171                if min > max {
172                    return Err("length_range_inclusive requires min <= max");
173                }
174            }
175            Self::MultipleOf { divisor } => {
176                if !rule_number_is_valid(divisor) {
177                    return Err("multiple_of divisor must be a valid numeric literal");
178                }
179                if rule_number_is_zero(divisor) {
180                    return Err("multiple_of divisor must be nonzero");
181                }
182            }
183            Self::NumericMaximumInclusive { value } | Self::NumericMinimumInclusive { value } => {
184                if !rule_number_is_valid(value) {
185                    return Err("numeric rule value must be a valid numeric literal");
186                }
187            }
188            Self::NumericRangeInclusive { min, max } => {
189                if !rule_number_is_valid(min) || !rule_number_is_valid(max) {
190                    return Err("numeric range operands must be valid numeric literals");
191                }
192            }
193        }
194        Ok(())
195    }
196}
197
198/// One exact, operation-owned numeric literal emitted by the derive parser.
199///
200/// Unsuffixed decimal text stays textual until it is bound to the declared
201/// primitive. This prevents decimal rules from passing through binary float
202/// conversion before accepted-schema admission.
203#[derive(Clone, Debug, Serialize)]
204pub enum RuleNumber {
205    /// Canonical signed or unsigned integer text.
206    Integer(&'static str),
207    /// Exact unsuffixed base-10 decimal text.
208    Decimal(&'static str),
209    /// Explicit `f32` literal.
210    Float32(f32),
211    /// Explicit `f64` literal.
212    Float64(f64),
213}
214
215fn rule_length_bound(value: &RuleNumber) -> Option<u64> {
216    match value {
217        RuleNumber::Integer(value) => value.parse().ok(),
218        RuleNumber::Decimal(_) | RuleNumber::Float32(_) | RuleNumber::Float64(_) => None,
219    }
220}
221
222fn rule_number_is_zero(value: &RuleNumber) -> bool {
223    match value {
224        RuleNumber::Integer(value) => {
225            value.parse::<i128>().is_ok_and(|value| value == 0)
226                || value.parse::<u128>().is_ok_and(|value| value == 0)
227        }
228        RuleNumber::Decimal(value) => value
229            .parse::<icydb_schema::Decimal>()
230            .is_ok_and(|value| value.is_zero()),
231        RuleNumber::Float32(value) => *value == 0.0,
232        RuleNumber::Float64(value) => *value == 0.0,
233    }
234}
235
236fn rule_number_is_valid(value: &RuleNumber) -> bool {
237    match value {
238        RuleNumber::Integer(value) => {
239            value.parse::<i128>().is_ok() || value.parse::<u128>().is_ok()
240        }
241        RuleNumber::Decimal(value) => value.parse::<icydb_schema::Decimal>().is_ok(),
242        RuleNumber::Float32(value) => value.is_finite(),
243        RuleNumber::Float64(value) => value.is_finite(),
244    }
245}
246
247///
248/// TypeNormalizer
249///
250/// Reference to one normalizer node plus its bound argument list.
251///
252
253#[derive(Clone, Debug, Serialize)]
254pub struct TypeNormalizer {
255    path: &'static str,
256    args: Args,
257}
258
259impl TypeNormalizer {
260    #[must_use]
261    pub const fn new(path: &'static str, args: Args) -> Self {
262        Self { path, args }
263    }
264
265    #[must_use]
266    pub const fn path(&self) -> &'static str {
267        self.path
268    }
269
270    #[must_use]
271    pub const fn args(&self) -> &Args {
272        &self.args
273    }
274}
275
276impl ValidateNode for TypeNormalizer {
277    fn validate(&self) -> Result<(), ErrorTree> {
278        let mut errs = ErrorTree::new();
279
280        // Resolve the referenced normalizer path against the schema graph.
281        let res = schema_read().check_node_as::<Normalizer>(self.path());
282        if let Err(e) = res {
283            errs.add(e.to_string());
284        }
285
286        errs.result()
287    }
288}
289
290impl VisitableNode for TypeNormalizer {}
291
292///
293/// TypeValidator
294///
295/// Reference to one validator node plus its bound argument list.
296///
297
298#[derive(Clone, Debug, Serialize)]
299pub struct TypeValidator {
300    path: &'static str,
301    args: Args,
302}
303
304impl TypeValidator {
305    #[must_use]
306    pub const fn new(path: &'static str, args: Args) -> Self {
307        Self { path, args }
308    }
309
310    #[must_use]
311    pub const fn path(&self) -> &'static str {
312        self.path
313    }
314
315    #[must_use]
316    pub const fn args(&self) -> &Args {
317        &self.args
318    }
319}
320
321impl ValidateNode for TypeValidator {
322    fn validate(&self) -> Result<(), ErrorTree> {
323        let mut errs = ErrorTree::new();
324
325        // Resolve the referenced validator path against the schema graph.
326        let res = schema_read().check_node_as::<Validator>(self.path());
327        if let Err(e) = res {
328            errs.add(e.to_string());
329        }
330
331        errs.result()
332    }
333}
334
335impl VisitableNode for TypeValidator {}
336
337#[cfg(test)]
338mod tests {
339    use super::{RuleNumber, SourceRuleAuthoringOperation};
340
341    #[test]
342    fn directly_constructed_rule_numbers_validate_before_lowering() {
343        assert_eq!(
344            SourceRuleAuthoringOperation::MultipleOf {
345                divisor: RuleNumber::Decimal("not-a-decimal"),
346            }
347            .validate_shape(),
348            Err("multiple_of divisor must be a valid numeric literal"),
349        );
350        assert_eq!(
351            SourceRuleAuthoringOperation::NumericMaximumInclusive {
352                value: RuleNumber::Float64(f64::NAN),
353            }
354            .validate_shape(),
355            Err("numeric rule value must be a valid numeric literal"),
356        );
357    }
358}