ferrum_interfaces/vnext/operation/
semantic.rs1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5use super::super::VNextError;
6
7#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10#[serde(try_from = "String", into = "String")]
11pub struct AttributeId(String);
12
13impl AttributeId {
14 pub fn new(value: impl Into<String>) -> Result<Self, VNextError> {
15 let value = value.into();
16 if value.is_empty() || value.len() > 160 {
17 return Err(VNextError::InvalidIdentity {
18 kind: "operation attribute",
19 value,
20 reason: "identity must contain between 1 and 160 bytes",
21 });
22 }
23 if !value.bytes().all(|byte| {
24 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b':' | b'/')
25 }) {
26 return Err(VNextError::InvalidIdentity {
27 kind: "operation attribute",
28 value,
29 reason: "identity contains a non-portable character",
30 });
31 }
32 Ok(Self(value))
33 }
34
35 pub fn as_str(&self) -> &str {
36 &self.0
37 }
38}
39
40impl fmt::Display for AttributeId {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 formatter.write_str(&self.0)
43 }
44}
45
46impl TryFrom<String> for AttributeId {
47 type Error = VNextError;
48
49 fn try_from(value: String) -> Result<Self, Self::Error> {
50 Self::new(value)
51 }
52}
53
54impl From<AttributeId> for String {
55 fn from(value: AttributeId) -> Self {
56 value.0
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum AttributeValueKind {
63 Bool,
64 Integer,
65 Unsigned,
66 Rational,
67 Text,
68 Integers,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
72pub struct CanonicalRational {
73 numerator: i64,
74 denominator: u64,
75}
76
77#[derive(Deserialize)]
78#[serde(deny_unknown_fields)]
79struct CanonicalRationalWire {
80 numerator: i64,
81 denominator: u64,
82}
83
84impl<'de> Deserialize<'de> for CanonicalRational {
85 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
86 where
87 D: Deserializer<'de>,
88 {
89 let wire = CanonicalRationalWire::deserialize(deserializer)?;
90 Self::new(wire.numerator, wire.denominator).map_err(serde::de::Error::custom)
91 }
92}
93
94impl CanonicalRational {
95 pub fn new(numerator: i64, denominator: u64) -> Result<Self, VNextError> {
96 if denominator == 0 {
97 return Err(VNextError::InvalidExecutionPlan {
98 reason: "rational denominator must be non-zero".to_owned(),
99 });
100 }
101 let divisor = gcd_u64(numerator.unsigned_abs(), denominator);
102 let denominator = denominator / divisor;
103 let reduced = i128::from(numerator) / i128::from(divisor);
104 let numerator = i64::try_from(reduced).map_err(|_| VNextError::InvalidExecutionPlan {
105 reason: "canonical rational numerator overflows i64".to_owned(),
106 })?;
107 Ok(Self {
108 numerator,
109 denominator,
110 })
111 }
112
113 pub fn from_decimal_str(raw: &str) -> Result<Self, VNextError> {
116 let normalized = raw.to_ascii_lowercase();
117 let (mantissa, exponent) = match normalized.split_once('e') {
118 Some((mantissa, exponent)) => (
119 mantissa,
120 exponent.parse::<i32>().map_err(|error| {
121 invalid_decimal_rational(format!("invalid decimal exponent: {error}"))
122 })?,
123 ),
124 None => (normalized.as_str(), 0),
125 };
126 let (negative, mantissa) = if let Some(unsigned) = mantissa.strip_prefix('-') {
127 (true, unsigned)
128 } else {
129 (false, mantissa.strip_prefix('+').unwrap_or(mantissa))
130 };
131 let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
132 if whole.is_empty()
133 || !whole.bytes().all(|byte| byte.is_ascii_digit())
134 || !fraction.bytes().all(|byte| byte.is_ascii_digit())
135 {
136 return Err(invalid_decimal_rational(format!(
137 "invalid decimal rational {raw:?}"
138 )));
139 }
140
141 let digits = format!("{whole}{fraction}");
142 let mut magnitude = digits.parse::<u128>().map_err(|error| {
143 invalid_decimal_rational(format!("decimal numerator overflows: {error}"))
144 })?;
145 let fractional_digits = i32::try_from(fraction.len()).map_err(|_| {
146 invalid_decimal_rational("decimal rational has too many fractional digits")
147 })?;
148 let scale = fractional_digits
149 .checked_sub(exponent)
150 .ok_or_else(|| invalid_decimal_rational("decimal rational exponent overflows"))?;
151 let denominator = if scale >= 0 {
152 10_u128
153 .checked_pow(scale as u32)
154 .ok_or_else(|| invalid_decimal_rational("decimal rational denominator overflows"))?
155 } else {
156 magnitude = magnitude
157 .checked_mul(10_u128.checked_pow(scale.unsigned_abs()).ok_or_else(|| {
158 invalid_decimal_rational("decimal rational numerator scale overflows")
159 })?)
160 .ok_or_else(|| invalid_decimal_rational("decimal rational numerator overflows"))?;
161 1
162 };
163 let signed = if negative {
164 -(i128::try_from(magnitude)
165 .map_err(|_| invalid_decimal_rational("decimal rational numerator exceeds i128"))?)
166 } else {
167 i128::try_from(magnitude)
168 .map_err(|_| invalid_decimal_rational("decimal rational numerator exceeds i128"))?
169 };
170 let numerator = i64::try_from(signed)
171 .map_err(|_| invalid_decimal_rational("decimal rational numerator exceeds i64"))?;
172 let denominator = u64::try_from(denominator)
173 .map_err(|_| invalid_decimal_rational("decimal rational denominator exceeds u64"))?;
174 Self::new(numerator, denominator)
175 }
176
177 pub const fn numerator(self) -> i64 {
178 self.numerator
179 }
180
181 pub const fn denominator(self) -> u64 {
182 self.denominator
183 }
184}
185
186fn invalid_decimal_rational(reason: impl Into<String>) -> VNextError {
187 VNextError::InvalidExecutionPlan {
188 reason: reason.into(),
189 }
190}
191
192fn gcd_u64(mut left: u64, mut right: u64) -> u64 {
193 while right != 0 {
194 let remainder = left % right;
195 left = right;
196 right = remainder;
197 }
198 left.max(1)
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "snake_case")]
203pub enum SemanticValue {
204 Bool(bool),
205 Integer(i64),
206 Unsigned(u64),
207 Rational(CanonicalRational),
208 Text(String),
209 Integers(Vec<i64>),
210}
211
212impl SemanticValue {
213 pub const fn kind(&self) -> AttributeValueKind {
214 match self {
215 Self::Bool(_) => AttributeValueKind::Bool,
216 Self::Integer(_) => AttributeValueKind::Integer,
217 Self::Unsigned(_) => AttributeValueKind::Unsigned,
218 Self::Rational(_) => AttributeValueKind::Rational,
219 Self::Text(_) => AttributeValueKind::Text,
220 Self::Integers(_) => AttributeValueKind::Integers,
221 }
222 }
223
224 pub fn validate(&self, context: &str) -> Result<(), VNextError> {
225 match self {
226 Self::Text(value) if value.is_empty() => Err(VNextError::InvalidExecutionPlan {
227 reason: format!("{context} contains an empty text attribute"),
228 }),
229 Self::Integers(values) if values.is_empty() => Err(VNextError::InvalidExecutionPlan {
230 reason: format!("{context} contains an empty integer-list attribute"),
231 }),
232 _ => Ok(()),
233 }
234 }
235}