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 candid::CandidType;
7use serde::{Deserialize, Serialize};
8
9use crate::{ScalarKind, ScalarLiteral, SchemaContractError};
10
11/// One closed durable operation applied to every selected nominal value.
12#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
13pub enum SourceRuleOperation {
14    /// Inclusive Unicode-scalar, octet, or collection-cardinality range.
15    LengthRangeInclusive {
16        /// Inclusive minimum length.
17        min: u64,
18        /// Inclusive maximum length.
19        max: u64,
20    },
21    /// Exact nonzero integer or fixed-scale decimal divisor.
22    MultipleOf {
23        /// Exact divisor admitted against the target kind.
24        divisor: ScalarLiteral,
25    },
26    /// Inclusive upper bound for one exact numeric kind.
27    NumericMaximumInclusive {
28        /// Exact upper-bound literal.
29        value: ScalarLiteral,
30    },
31    /// Inclusive lower bound for one exact numeric kind.
32    NumericMinimumInclusive {
33        /// Exact lower-bound literal.
34        value: ScalarLiteral,
35    },
36    /// Inclusive lower and upper bounds for one exact numeric kind.
37    NumericRangeInclusive {
38        /// Exact lower-bound literal.
39        min: ScalarLiteral,
40        /// Exact upper-bound literal.
41        max: ScalarLiteral,
42    },
43}
44
45impl SourceRuleOperation {
46    /// Validate the bounded operation independently of its eventual target.
47    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
48        match self {
49            Self::LengthRangeInclusive { min, max } if min <= max => Ok(()),
50            Self::MultipleOf { divisor }
51                if exact_multiple_literal(divisor) && !scalar_literal_is_zero(divisor) =>
52            {
53                divisor.validate()
54            }
55            Self::NumericMaximumInclusive { value } | Self::NumericMinimumInclusive { value }
56                if numeric_literal(value) =>
57            {
58                value.validate()
59            }
60            Self::NumericRangeInclusive { min, max }
61                if numeric_literal(min)
62                    && min.kind() == max.kind()
63                    && scalar_literal_le(min, max) =>
64            {
65                min.validate()?;
66                max.validate()
67            }
68            Self::LengthRangeInclusive { .. }
69            | Self::MultipleOf { .. }
70            | Self::NumericMaximumInclusive { .. }
71            | Self::NumericMinimumInclusive { .. }
72            | Self::NumericRangeInclusive { .. } => Err(SchemaContractError::InvalidRuleOperation),
73        }
74    }
75}
76
77pub(crate) const fn exact_multiple_literal(literal: &ScalarLiteral) -> bool {
78    matches!(
79        literal.kind(),
80        ScalarKind::Decimal
81            | ScalarKind::Int128
82            | ScalarKind::IntBig
83            | ScalarKind::Nat128
84            | ScalarKind::NatBig
85    )
86}
87
88const fn numeric_literal(literal: &ScalarLiteral) -> bool {
89    matches!(
90        literal.kind(),
91        ScalarKind::Decimal
92            | ScalarKind::Float32
93            | ScalarKind::Float64
94            | ScalarKind::Int128
95            | ScalarKind::IntBig
96            | ScalarKind::Nat128
97            | ScalarKind::NatBig
98    )
99}
100
101fn scalar_literal_le(left: &ScalarLiteral, right: &ScalarLiteral) -> bool {
102    match (left, right) {
103        (ScalarLiteral::Decimal(left), ScalarLiteral::Decimal(right)) => left <= right,
104        (ScalarLiteral::Float32(left), ScalarLiteral::Float32(right)) => left <= right,
105        (ScalarLiteral::Float64(left), ScalarLiteral::Float64(right)) => left <= right,
106        (ScalarLiteral::Int(left), ScalarLiteral::Int(right)) => left <= right,
107        (ScalarLiteral::IntBig(left), ScalarLiteral::IntBig(right)) => left <= right,
108        (ScalarLiteral::Nat(left), ScalarLiteral::Nat(right)) => left <= right,
109        (ScalarLiteral::NatBig(left), ScalarLiteral::NatBig(right)) => left <= right,
110        _ => false,
111    }
112}
113
114fn scalar_literal_is_zero(literal: &ScalarLiteral) -> bool {
115    match literal {
116        ScalarLiteral::Decimal(value) => value.is_zero(),
117        ScalarLiteral::Int(value) => *value == 0,
118        ScalarLiteral::IntBig(value) => value == &crate::IntBig::default(),
119        ScalarLiteral::Nat(value) => *value == 0,
120        ScalarLiteral::NatBig(value) => value == &crate::NatBig::default(),
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}