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    /// Inclusive lower bound for one exact numeric kind.
22    NumericMinimumInclusive {
23        /// Exact lower-bound literal.
24        value: ScalarLiteral,
25    },
26    /// Inclusive lower and upper bounds for one exact numeric kind.
27    NumericRangeInclusive {
28        /// Exact lower-bound literal.
29        min: ScalarLiteral,
30        /// Exact upper-bound literal.
31        max: ScalarLiteral,
32    },
33}
34
35impl SourceRuleOperation {
36    /// Validate the bounded operation independently of its eventual target.
37    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
38        match self {
39            Self::LengthRangeInclusive { min, max } if min <= max => Ok(()),
40            Self::NumericMinimumInclusive { value } if numeric_literal(value) => value.validate(),
41            Self::NumericRangeInclusive { min, max }
42                if numeric_literal(min)
43                    && min.kind() == max.kind()
44                    && scalar_literal_le(min, max) =>
45            {
46                min.validate()?;
47                max.validate()
48            }
49            Self::LengthRangeInclusive { .. }
50            | Self::NumericMinimumInclusive { .. }
51            | Self::NumericRangeInclusive { .. } => Err(SchemaContractError::InvalidRuleOperation),
52        }
53    }
54}
55
56const fn numeric_literal(literal: &ScalarLiteral) -> bool {
57    matches!(
58        literal.kind(),
59        ScalarKind::Decimal
60            | ScalarKind::Float32
61            | ScalarKind::Float64
62            | ScalarKind::Int128
63            | ScalarKind::IntBig
64            | ScalarKind::Nat128
65            | ScalarKind::NatBig
66    )
67}
68
69fn scalar_literal_le(left: &ScalarLiteral, right: &ScalarLiteral) -> bool {
70    match (left, right) {
71        (ScalarLiteral::Decimal(left), ScalarLiteral::Decimal(right)) => left <= right,
72        (ScalarLiteral::Float32(left), ScalarLiteral::Float32(right)) => left <= right,
73        (ScalarLiteral::Float64(left), ScalarLiteral::Float64(right)) => left <= right,
74        (ScalarLiteral::Int(left), ScalarLiteral::Int(right)) => left <= right,
75        (ScalarLiteral::IntBig(left), ScalarLiteral::IntBig(right)) => left <= right,
76        (ScalarLiteral::Nat(left), ScalarLiteral::Nat(right)) => left <= right,
77        (ScalarLiteral::NatBig(left), ScalarLiteral::NatBig(right)) => left <= right,
78        _ => false,
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::SourceRuleOperation;
85    use crate::{ScalarLiteral, SchemaContractError};
86
87    #[test]
88    fn source_rule_operation_rejects_reversed_and_mixed_ranges() {
89        assert_eq!(
90            SourceRuleOperation::LengthRangeInclusive { min: 2, max: 1 }.validate(),
91            Err(SchemaContractError::InvalidRuleOperation),
92        );
93        assert_eq!(
94            SourceRuleOperation::NumericRangeInclusive {
95                min: ScalarLiteral::Nat(2),
96                max: ScalarLiteral::Nat(1),
97            }
98            .validate(),
99            Err(SchemaContractError::InvalidRuleOperation),
100        );
101        assert_eq!(
102            SourceRuleOperation::NumericRangeInclusive {
103                min: ScalarLiteral::Int(0),
104                max: ScalarLiteral::Nat(1),
105            }
106            .validate(),
107            Err(SchemaContractError::InvalidRuleOperation),
108        );
109    }
110
111    #[test]
112    fn source_rule_operation_accepts_ordered_exact_ranges() {
113        assert!(
114            SourceRuleOperation::LengthRangeInclusive { min: 1, max: 2 }
115                .validate()
116                .is_ok()
117        );
118        assert!(
119            SourceRuleOperation::NumericRangeInclusive {
120                min: ScalarLiteral::Int(-1),
121                max: ScalarLiteral::Int(1),
122            }
123            .validate()
124            .is_ok()
125        );
126    }
127}