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