Skip to main content

ferrum_interfaces/vnext/operation/
semantic.rs

1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5use super::super::VNextError;
6
7/// Stable semantic attribute identity. Attribute names are data, not ad-hoc
8/// strings interpreted by an individual provider.
9#[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    /// Parses a finite base-10 decimal or scientific-notation value without
114    /// routing through binary floating point.
115    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}