Skip to main content

ferrum_interfaces/vnext/operation/
attribute.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5use super::super::VNextError;
6use super::foundation::invalid_operation;
7use super::{AttributeId, AttributeValueKind, CanonicalRational, SemanticValue};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(deny_unknown_fields)]
11pub struct AttributeSpec {
12    pub value_kind: AttributeValueKind,
13    pub required: bool,
14    pub constraint: AttributeConstraint,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum AttributeConstraint {
20    None,
21    BoolEquals(bool),
22    IntegerRange {
23        minimum: i64,
24        maximum: i64,
25    },
26    UnsignedRange {
27        minimum: u64,
28        maximum: u64,
29    },
30    RationalRange {
31        minimum: CanonicalRational,
32        maximum: CanonicalRational,
33    },
34    TextChoices {
35        values: BTreeSet<String>,
36    },
37    IntegerListLength {
38        minimum: u32,
39        maximum: u32,
40    },
41}
42
43/// Closed attribute vocabulary for one operation contract.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45pub struct AttributeSchema {
46    entries: BTreeMap<AttributeId, AttributeSpec>,
47}
48
49#[derive(Deserialize)]
50#[serde(deny_unknown_fields)]
51struct AttributeSchemaWire {
52    entries: BTreeMap<AttributeId, AttributeSpec>,
53}
54
55impl<'de> Deserialize<'de> for AttributeSchema {
56    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57    where
58        D: Deserializer<'de>,
59    {
60        let wire = AttributeSchemaWire::deserialize(deserializer)?;
61        Self::new(wire.entries).map_err(serde::de::Error::custom)
62    }
63}
64
65impl AttributeSchema {
66    pub fn new(entries: BTreeMap<AttributeId, AttributeSpec>) -> Result<Self, VNextError> {
67        for (attribute_id, spec) in &entries {
68            spec.validate(attribute_id)?;
69        }
70        Ok(Self { entries })
71    }
72
73    pub fn empty() -> Self {
74        Self {
75            entries: BTreeMap::new(),
76        }
77    }
78
79    pub fn entries(&self) -> &BTreeMap<AttributeId, AttributeSpec> {
80        &self.entries
81    }
82
83    pub fn validate_values(
84        &self,
85        values: &BTreeMap<AttributeId, SemanticValue>,
86        context: &str,
87    ) -> Result<(), VNextError> {
88        for (attribute_id, value) in values {
89            let spec = self.entries.get(attribute_id).ok_or_else(|| {
90                invalid_operation(format!(
91                    "{context} contains unknown attribute `{attribute_id}`"
92                ))
93            })?;
94            value.validate(context)?;
95            if value.kind() != spec.value_kind {
96                return Err(invalid_operation(format!(
97                    "{context} attribute `{attribute_id}` has the wrong value kind"
98                )));
99            }
100            spec.validate_value(attribute_id, value)?;
101        }
102        if let Some(attribute_id) = self.entries.iter().find_map(|(attribute_id, spec)| {
103            (spec.required && !values.contains_key(attribute_id)).then_some(attribute_id)
104        }) {
105            return Err(invalid_operation(format!(
106                "{context} is missing required attribute `{attribute_id}`"
107            )));
108        }
109        Ok(())
110    }
111}
112
113impl AttributeSpec {
114    fn validate(&self, attribute_id: &AttributeId) -> Result<(), VNextError> {
115        let compatible = match (&self.value_kind, &self.constraint) {
116            (_, AttributeConstraint::None) => true,
117            (AttributeValueKind::Bool, AttributeConstraint::BoolEquals(_)) => true,
118            (
119                AttributeValueKind::Integer,
120                AttributeConstraint::IntegerRange { minimum, maximum },
121            ) => minimum <= maximum,
122            (
123                AttributeValueKind::Unsigned,
124                AttributeConstraint::UnsignedRange { minimum, maximum },
125            ) => minimum <= maximum,
126            (AttributeValueKind::Text, AttributeConstraint::TextChoices { values }) => {
127                !values.is_empty() && values.iter().all(|value| !value.is_empty())
128            }
129            (
130                AttributeValueKind::Integers,
131                AttributeConstraint::IntegerListLength { minimum, maximum },
132            ) => minimum <= maximum,
133            (
134                AttributeValueKind::Rational,
135                AttributeConstraint::RationalRange { minimum, maximum },
136            ) => {
137                (minimum.numerator() as i128) * (maximum.denominator() as i128)
138                    <= (maximum.numerator() as i128) * (minimum.denominator() as i128)
139            }
140            _ => false,
141        };
142        if !compatible {
143            return Err(invalid_operation(format!(
144                "attribute `{attribute_id}` has an incompatible or invalid constraint"
145            )));
146        }
147        Ok(())
148    }
149
150    fn validate_value(
151        &self,
152        attribute_id: &AttributeId,
153        value: &SemanticValue,
154    ) -> Result<(), VNextError> {
155        let accepted = match (&self.constraint, value) {
156            (AttributeConstraint::None, _) => true,
157            (AttributeConstraint::BoolEquals(expected), SemanticValue::Bool(actual)) => {
158                expected == actual
159            }
160            (
161                AttributeConstraint::IntegerRange { minimum, maximum },
162                SemanticValue::Integer(actual),
163            ) => minimum <= actual && actual <= maximum,
164            (
165                AttributeConstraint::UnsignedRange { minimum, maximum },
166                SemanticValue::Unsigned(actual),
167            ) => minimum <= actual && actual <= maximum,
168            (
169                AttributeConstraint::RationalRange { minimum, maximum },
170                SemanticValue::Rational(actual),
171            ) => {
172                (actual.numerator() as i128) * (minimum.denominator() as i128)
173                    >= (minimum.numerator() as i128) * (actual.denominator() as i128)
174                    && (actual.numerator() as i128) * (maximum.denominator() as i128)
175                        <= (maximum.numerator() as i128) * (actual.denominator() as i128)
176            }
177            (AttributeConstraint::TextChoices { values }, SemanticValue::Text(actual)) => {
178                values.contains(actual)
179            }
180            (
181                AttributeConstraint::IntegerListLength { minimum, maximum },
182                SemanticValue::Integers(actual),
183            ) => (*minimum as usize) <= actual.len() && actual.len() <= (*maximum as usize),
184            _ => false,
185        };
186        if !accepted {
187            return Err(invalid_operation(format!(
188                "attribute `{attribute_id}` violates its declared constraint"
189            )));
190        }
191        Ok(())
192    }
193}