Skip to main content

weavatrix_graph/
attribute.rs

1use crate::{String, Vec};
2use alloc::collections::BTreeMap;
3use core::hash::{Hash, Hasher};
4use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
5
6/// Deterministic JSON-like attribute value for graph extensions.
7///
8/// The graph core keeps attributes typed enough to preserve booleans, numeric
9/// counters, nested ranges, and arrays without taking a JSON runtime dependency.
10#[derive(Debug, Clone, Copy, Serialize)]
11pub struct FiniteF64(f64);
12
13impl FiniteF64 {
14    /// Creates a finite floating-point attribute value.
15    ///
16    /// # Errors
17    ///
18    /// Returns an error for NaN and infinity, which are not valid JSON numbers.
19    pub fn new(value: f64) -> Result<Self, String> {
20        if value.is_finite() {
21            Ok(Self(value))
22        } else {
23            Err(format!("float attribute must be finite: {value}"))
24        }
25    }
26
27    #[must_use]
28    pub const fn get(self) -> f64 {
29        self.0
30    }
31}
32
33impl PartialEq for FiniteF64 {
34    fn eq(&self, other: &Self) -> bool {
35        self.0.to_bits() == other.0.to_bits()
36    }
37}
38
39impl Eq for FiniteF64 {}
40
41impl PartialOrd for FiniteF64 {
42    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
43        Some(self.cmp(other))
44    }
45}
46
47impl Ord for FiniteF64 {
48    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
49        self.0.total_cmp(&other.0)
50    }
51}
52
53impl Hash for FiniteF64 {
54    fn hash<H: Hasher>(&self, state: &mut H) {
55        self.0.to_bits().hash(state);
56    }
57}
58
59impl<'de> Deserialize<'de> for FiniteF64 {
60    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
61    where
62        D: Deserializer<'de>,
63    {
64        Self::new(f64::deserialize(deserializer)?).map_err(D::Error::custom)
65    }
66}
67
68/// Deterministic JSON-like attribute value for graph extensions.
69#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
70#[serde(untagged)]
71#[non_exhaustive]
72pub enum AttributeValue {
73    Null,
74    Bool(bool),
75    Integer(i64),
76    Unsigned(u64),
77    Float(FiniteF64),
78    String(String),
79    List(Vec<AttributeValue>),
80    Object(BTreeMap<String, AttributeValue>),
81}
82
83impl From<bool> for AttributeValue {
84    fn from(value: bool) -> Self {
85        Self::Bool(value)
86    }
87}
88
89impl From<i64> for AttributeValue {
90    fn from(value: i64) -> Self {
91        Self::Integer(value)
92    }
93}
94
95impl From<u64> for AttributeValue {
96    fn from(value: u64) -> Self {
97        Self::Unsigned(value)
98    }
99}
100
101impl From<i32> for AttributeValue {
102    fn from(value: i32) -> Self {
103        Self::Integer(i64::from(value))
104    }
105}
106
107impl From<u32> for AttributeValue {
108    fn from(value: u32) -> Self {
109        Self::Unsigned(u64::from(value))
110    }
111}
112
113impl TryFrom<f64> for AttributeValue {
114    type Error = String;
115
116    fn try_from(value: f64) -> Result<Self, Self::Error> {
117        Ok(Self::Float(FiniteF64::new(value)?))
118    }
119}
120
121impl From<String> for AttributeValue {
122    fn from(value: String) -> Self {
123        Self::String(value)
124    }
125}
126
127impl From<&str> for AttributeValue {
128    fn from(value: &str) -> Self {
129        Self::String(String::from(value))
130    }
131}
132
133impl From<Vec<AttributeValue>> for AttributeValue {
134    fn from(value: Vec<AttributeValue>) -> Self {
135        Self::List(value)
136    }
137}
138
139impl From<BTreeMap<String, AttributeValue>> for AttributeValue {
140    fn from(value: BTreeMap<String, AttributeValue>) -> Self {
141        Self::Object(value)
142    }
143}