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
78            | ScalarKind::Int128
79            | ScalarKind::IntBig
80            | ScalarKind::Nat128
81            | ScalarKind::NatBig
82    )
83}
84
85const fn numeric_literal(literal: &ScalarLiteral) -> bool {
86    matches!(
87        literal.kind(),
88        ScalarKind::Decimal
89            | ScalarKind::Float32
90            | ScalarKind::Float64
91            | ScalarKind::Int128
92            | ScalarKind::IntBig
93            | ScalarKind::Nat128
94            | ScalarKind::NatBig
95    )
96}
97
98fn scalar_literal_le(left: &ScalarLiteral, right: &ScalarLiteral) -> bool {
99    match (left, right) {
100        (ScalarLiteral::Decimal(left), ScalarLiteral::Decimal(right)) => left <= right,
101        (ScalarLiteral::Float32(left), ScalarLiteral::Float32(right)) => left <= right,
102        (ScalarLiteral::Float64(left), ScalarLiteral::Float64(right)) => left <= right,
103        (ScalarLiteral::Int(left), ScalarLiteral::Int(right)) => left <= right,
104        (ScalarLiteral::IntBig(left), ScalarLiteral::IntBig(right)) => left <= right,
105        (ScalarLiteral::Nat(left), ScalarLiteral::Nat(right)) => left <= right,
106        (ScalarLiteral::NatBig(left), ScalarLiteral::NatBig(right)) => left <= right,
107        _ => false,
108    }
109}
110
111fn scalar_literal_is_zero(literal: &ScalarLiteral) -> bool {
112    match literal {
113        ScalarLiteral::Decimal(value) => value.is_zero(),
114        ScalarLiteral::Int(value) => *value == 0,
115        ScalarLiteral::IntBig(value) => value == &crate::IntBig::default(),
116        ScalarLiteral::Nat(value) => *value == 0,
117        ScalarLiteral::NatBig(value) => value == &crate::NatBig::default(),
118        ScalarLiteral::Account(_)
119        | ScalarLiteral::Blob(_)
120        | ScalarLiteral::Bool(_)
121        | ScalarLiteral::Date(_)
122        | ScalarLiteral::Duration(_)
123        | ScalarLiteral::EnumUnit { .. }
124        | ScalarLiteral::Float32(_)
125        | ScalarLiteral::Float64(_)
126        | ScalarLiteral::Principal(_)
127        | ScalarLiteral::Subaccount(_)
128        | ScalarLiteral::Text(_)
129        | ScalarLiteral::Timestamp(_)
130        | ScalarLiteral::Ulid(_)
131        | ScalarLiteral::Unit(_) => false,
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::SourceRuleOperation;
138    use crate::{ScalarLiteral, SchemaContractError};
139
140    #[test]
141    fn source_rule_operation_rejects_reversed_and_mixed_ranges() {
142        assert_eq!(
143            SourceRuleOperation::LengthRangeInclusive { min: 2, max: 1 }.validate(),
144            Err(SchemaContractError::InvalidRuleOperation),
145        );
146        assert_eq!(
147            SourceRuleOperation::NumericRangeInclusive {
148                min: ScalarLiteral::Nat(2),
149                max: ScalarLiteral::Nat(1),
150            }
151            .validate(),
152            Err(SchemaContractError::InvalidRuleOperation),
153        );
154        assert_eq!(
155            SourceRuleOperation::NumericRangeInclusive {
156                min: ScalarLiteral::Int(0),
157                max: ScalarLiteral::Nat(1),
158            }
159            .validate(),
160            Err(SchemaContractError::InvalidRuleOperation),
161        );
162    }
163
164    #[test]
165    fn source_rule_operation_accepts_ordered_exact_ranges() {
166        assert!(
167            SourceRuleOperation::LengthRangeInclusive { min: 1, max: 2 }
168                .validate()
169                .is_ok()
170        );
171        assert!(
172            SourceRuleOperation::NumericRangeInclusive {
173                min: ScalarLiteral::Int(-1),
174                max: ScalarLiteral::Int(1),
175            }
176            .validate()
177            .is_ok()
178        );
179    }
180
181    #[test]
182    fn source_rule_operation_accepts_exact_nonzero_multiple_and_maximum() {
183        assert!(
184            SourceRuleOperation::NumericMaximumInclusive {
185                value: ScalarLiteral::Nat(10),
186            }
187            .validate()
188            .is_ok()
189        );
190        assert!(
191            SourceRuleOperation::MultipleOf {
192                divisor: ScalarLiteral::Int(-5),
193            }
194            .validate()
195            .is_ok()
196        );
197    }
198
199    #[test]
200    fn source_rule_operation_rejects_zero_and_float_multiple() {
201        for divisor in [
202            ScalarLiteral::Nat(0),
203            ScalarLiteral::Float64(crate::Float64::try_new(1.0).expect("finite float")),
204        ] {
205            assert_eq!(
206                SourceRuleOperation::MultipleOf { divisor }.validate(),
207                Err(SchemaContractError::InvalidRuleOperation),
208            );
209        }
210    }
211}