ferrum_interfaces/vnext/operation/
attribute.rs1use 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#[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}