Skip to main content

icydb_schema/
rule.rs

1//! Bounded source-side durable-rule operations.
2//!
3//! These values are proposal facts. Accepted schema resolves their nominal
4//! target and owns every runtime evaluator.
5
6use crate::{ScalarKind, ScalarLiteral, SchemaContractError};
7
8/// One closed durable operation applied to every selected nominal value.
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub enum SourceRuleOperation {
11    /// Inclusive Unicode-scalar, octet, or collection-cardinality range.
12    LengthRangeInclusive {
13        /// Inclusive minimum length.
14        min: u64,
15        /// Inclusive maximum length.
16        max: u64,
17    },
18    /// Exact nonzero integer or fixed-scale decimal divisor.
19    MultipleOf {
20        /// Exact divisor admitted against the target kind.
21        divisor: ScalarLiteral,
22    },
23    /// Inclusive upper bound for one exact numeric kind.
24    NumericMaximumInclusive {
25        /// Exact upper-bound literal.
26        value: ScalarLiteral,
27    },
28    /// Inclusive lower bound for one exact numeric kind.
29    NumericMinimumInclusive {
30        /// Exact lower-bound literal.
31        value: ScalarLiteral,
32    },
33    /// Inclusive lower and upper bounds for one exact numeric kind.
34    NumericRangeInclusive {
35        /// Exact lower-bound literal.
36        min: ScalarLiteral,
37        /// Exact upper-bound literal.
38        max: ScalarLiteral,
39    },
40}
41
42impl SourceRuleOperation {
43    /// Validate the bounded operation independently of its eventual target.
44    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
45        match self {
46            Self::LengthRangeInclusive { min, max } if min <= max => Ok(()),
47            Self::MultipleOf { divisor }
48                if exact_multiple_literal(divisor) && !scalar_literal_is_zero(divisor) =>
49            {
50                divisor.validate()
51            }
52            Self::NumericMaximumInclusive { value } | Self::NumericMinimumInclusive { value }
53                if numeric_literal(value) =>
54            {
55                value.validate()
56            }
57            Self::NumericRangeInclusive { min, max }
58                if numeric_literal(min)
59                    && min.kind() == max.kind()
60                    && scalar_literal_le(min, max) =>
61            {
62                min.validate()?;
63                max.validate()
64            }
65            Self::LengthRangeInclusive { .. }
66            | Self::MultipleOf { .. }
67            | Self::NumericMaximumInclusive { .. }
68            | Self::NumericMinimumInclusive { .. }
69            | Self::NumericRangeInclusive { .. } => Err(SchemaContractError::InvalidRuleOperation),
70        }
71    }
72}
73
74pub(crate) const fn exact_multiple_literal(literal: &ScalarLiteral) -> bool {
75    matches!(
76        literal.kind(),
77        ScalarKind::Decimal | ScalarKind::Int128 | ScalarKind::Nat128
78    )
79}
80
81const fn numeric_literal(literal: &ScalarLiteral) -> bool {
82    matches!(
83        literal.kind(),
84        ScalarKind::Decimal
85            | ScalarKind::Float32
86            | ScalarKind::Float64
87            | ScalarKind::Int128
88            | ScalarKind::Nat128
89            | ScalarKind::U256
90    )
91}
92
93fn scalar_literal_le(left: &ScalarLiteral, right: &ScalarLiteral) -> bool {
94    match (left, right) {
95        (ScalarLiteral::Decimal(left), ScalarLiteral::Decimal(right)) => left <= right,
96        (ScalarLiteral::Float32(left), ScalarLiteral::Float32(right)) => left <= right,
97        (ScalarLiteral::Float64(left), ScalarLiteral::Float64(right)) => left <= right,
98        (ScalarLiteral::Int(left), ScalarLiteral::Int(right)) => left <= right,
99        (ScalarLiteral::Nat(left), ScalarLiteral::Nat(right)) => left <= right,
100        (ScalarLiteral::U256(left), ScalarLiteral::U256(right)) => left <= right,
101        _ => false,
102    }
103}
104
105fn scalar_literal_is_zero(literal: &ScalarLiteral) -> bool {
106    match literal {
107        ScalarLiteral::Decimal(value) => value.is_zero(),
108        ScalarLiteral::Int(value) => *value == 0,
109        ScalarLiteral::Nat(value) => *value == 0,
110        ScalarLiteral::U256(value) => *value == crate::U256::ZERO,
111        ScalarLiteral::Account(_)
112        | ScalarLiteral::Blob(_)
113        | ScalarLiteral::Bool(_)
114        | ScalarLiteral::Date(_)
115        | ScalarLiteral::Duration(_)
116        | ScalarLiteral::EnumUnit { .. }
117        | ScalarLiteral::Float32(_)
118        | ScalarLiteral::Float64(_)
119        | ScalarLiteral::Principal(_)
120        | ScalarLiteral::Subaccount(_)
121        | ScalarLiteral::Text(_)
122        | ScalarLiteral::Timestamp(_)
123        | ScalarLiteral::Ulid(_)
124        | ScalarLiteral::Unit(_) => false,
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::SourceRuleOperation;
131    use crate::{ScalarLiteral, SchemaContractError};
132
133    #[test]
134    fn source_rule_operation_rejects_reversed_and_mixed_ranges() {
135        assert_eq!(
136            SourceRuleOperation::LengthRangeInclusive { min: 2, max: 1 }.validate(),
137            Err(SchemaContractError::InvalidRuleOperation),
138        );
139        assert_eq!(
140            SourceRuleOperation::NumericRangeInclusive {
141                min: ScalarLiteral::Nat(2),
142                max: ScalarLiteral::Nat(1),
143            }
144            .validate(),
145            Err(SchemaContractError::InvalidRuleOperation),
146        );
147        assert_eq!(
148            SourceRuleOperation::NumericRangeInclusive {
149                min: ScalarLiteral::Int(0),
150                max: ScalarLiteral::Nat(1),
151            }
152            .validate(),
153            Err(SchemaContractError::InvalidRuleOperation),
154        );
155    }
156
157    #[test]
158    fn source_rule_operation_accepts_ordered_exact_ranges() {
159        assert!(
160            SourceRuleOperation::LengthRangeInclusive { min: 1, max: 2 }
161                .validate()
162                .is_ok()
163        );
164        assert!(
165            SourceRuleOperation::NumericRangeInclusive {
166                min: ScalarLiteral::Int(-1),
167                max: ScalarLiteral::Int(1),
168            }
169            .validate()
170            .is_ok()
171        );
172    }
173
174    #[test]
175    fn source_rule_operation_accepts_exact_nonzero_multiple_and_maximum() {
176        assert!(
177            SourceRuleOperation::NumericMaximumInclusive {
178                value: ScalarLiteral::Nat(10),
179            }
180            .validate()
181            .is_ok()
182        );
183        assert!(
184            SourceRuleOperation::MultipleOf {
185                divisor: ScalarLiteral::Int(-5),
186            }
187            .validate()
188            .is_ok()
189        );
190    }
191
192    #[test]
193    fn source_rule_operation_rejects_zero_and_float_multiple() {
194        for divisor in [
195            ScalarLiteral::Nat(0),
196            ScalarLiteral::Float64(crate::Float64::try_new(1.0).expect("finite float")),
197        ] {
198            assert_eq!(
199                SourceRuleOperation::MultipleOf { divisor }.validate(),
200                Err(SchemaContractError::InvalidRuleOperation),
201            );
202        }
203    }
204}