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