tsg_core/graph/
attr.rs

1use std::fmt;
2use std::str::FromStr;
3
4use anyhow::{Result, anyhow};
5use bon::Builder;
6use bstr::{BStr, BString, ByteSlice};
7
8/// Represents different types of attribute values in the graph
9///
10/// This enum contains various data types that can be stored as attribute values:
11/// * `Int` - Integer values (stored as `isize`)
12/// * `Float` - Floating-point values (stored as `f32`)
13/// * `String` - UTF-8 string values (stored as `BString`)
14/// * `Json` - Structured JSON data (stored as `serde_json::Value`)
15/// * `Hex` - Hexadecimal representation of data (stored as `BString`)
16/// * `Bytes` - Raw binary data (stored as `Vec<u8>`)
17#[derive(Debug, Clone)]
18pub enum AttributeValue {
19    Int(isize),
20    Float(f32),
21    String(BString),
22    Json(serde_json::Value),
23    Hex(BString),
24    Bytes(Vec<u8>),
25}
26
27/// Represents an optional attribute in a graph
28///
29/// Attributes consist of three components:
30/// * `tag` - Identifier for the attribute (e.g., "name", "weight")
31/// * `attribute_type` - Single character identifying the data type:
32///   - 'i': Integer
33///   - 'f': Float
34///   - 'Z': String (default)
35///   - 'J': JSON
36///   - 'H': Hexadecimal
37///   - 'B': Binary bytes
38/// * `value` - The actual value stored as a BString
39///
40/// Attributes are typically formatted as "tag:type:value" when serialized.
41#[derive(Debug, Clone, Builder, Default)]
42#[builder(on(BString, into))]
43pub struct Attribute {
44    pub tag: BString,
45    #[builder(default = 'Z')]
46    pub attribute_type: char,
47    pub value: BString,
48}
49
50impl FromStr for Attribute {
51    type Err = anyhow::Error;
52
53    fn from_str(s: &str) -> Result<Self> {
54        // Format: tag:type:value
55        let parts: Vec<&str> = s.splitn(3, ':').collect();
56        if parts.len() < 3 {
57            return Err(anyhow!("Invalid attribute format: {}", s));
58        }
59
60        let tag = parts[0].into();
61        let attr_type = parts[1]
62            .chars()
63            .next()
64            .ok_or_else(|| anyhow!("Empty attribute type"))?;
65        let value = parts[2].into();
66
67        // Validate attribute type
68        match attr_type {
69            'i' | 'f' | 'Z' | 'J' | 'H' | 'B' => (),
70            _ => return Err(anyhow!("Unknown attribute type: {}", attr_type)),
71        }
72
73        Ok(Attribute {
74            tag,
75            attribute_type: attr_type,
76            value,
77        })
78    }
79}
80
81impl Attribute {
82    /// Get the attribute value as the appropriate type based on the attribute type
83    pub fn typed_value(&self) -> Result<AttributeValue> {
84        match self.attribute_type {
85            'i' => Ok(AttributeValue::Int(self.as_int()?)),
86            'f' => Ok(AttributeValue::Float(self.as_float()?)),
87            'Z' => Ok(AttributeValue::String(self.value.clone())),
88            'J' => Ok(AttributeValue::Json(self.as_json()?)),
89            'H' => {
90                // Parse hex string
91                Ok(AttributeValue::Hex(self.value.clone()))
92            }
93            'B' => {
94                // Parse byte array
95                let bytes = self.value.to_vec();
96                Ok(AttributeValue::Bytes(bytes))
97            }
98            _ => Err(anyhow!(
99                "Unsupported attribute type: {}",
100                self.attribute_type
101            )),
102        }
103    }
104
105    /// Get the integer value if the attribute type is 'i'
106    pub fn as_int(&self) -> Result<isize> {
107        if self.attribute_type != 'i' {
108            return Err(anyhow!("Attribute is not an integer type"));
109        }
110        self.value
111            .to_str()?
112            .parse()
113            .map_err(|e| anyhow!("Failed to parse integer: {}", e))
114    }
115
116    /// Get the float value if the attribute type is 'f'
117    pub fn as_float(&self) -> Result<f32> {
118        if self.attribute_type != 'f' {
119            return Err(anyhow!("Attribute is not a float type"));
120        }
121        self.value
122            .to_str()?
123            .parse()
124            .map_err(|e| anyhow!("Failed to parse float: {}", e))
125    }
126
127    /// Get the string value if the attribute type is 'Z'
128    pub fn as_string(&self) -> Result<&BStr> {
129        if self.attribute_type != 'Z' {
130            return Err(anyhow!("Attribute is not a string type"));
131        }
132        Ok(self.value.as_bstr())
133    }
134
135    /// Get the JSON value if the attribute type is 'J'
136    pub fn as_json(&self) -> Result<serde_json::Value> {
137        if self.attribute_type != 'J' {
138            return Err(anyhow!("Attribute is not a JSON type"));
139        }
140        serde_json::from_str(self.value.to_str()?)
141            .map_err(|e| anyhow!("Failed to parse JSON: {}", e))
142    }
143}
144
145impl fmt::Display for Attribute {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "{}:{}:{}", self.tag, self.attribute_type, self.value)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use std::str::FromStr;
155
156    #[test]
157    fn test_attribute_from_str_valid() {
158        let attr = Attribute::from_str("ptc:i:1").unwrap();
159        assert_eq!(attr.tag, "ptc");
160        assert_eq!(attr.attribute_type, 'i');
161        assert_eq!(attr.value, "1");
162
163        let attr = Attribute::from_str("ptf:f:0.5").unwrap();
164        assert_eq!(attr.tag, "ptf");
165        assert_eq!(attr.attribute_type, 'f');
166        assert_eq!(attr.value, "0.5");
167
168        let attr = Attribute::from_str("name:Z:test_value").unwrap();
169        assert_eq!(attr.tag, "name");
170        assert_eq!(attr.attribute_type, 'Z');
171        assert_eq!(attr.value, "test_value");
172
173        let attr = Attribute::from_str("data:J:{\"key\":\"value\"}").unwrap();
174        assert_eq!(attr.tag, "data");
175        assert_eq!(attr.attribute_type, 'J');
176        assert_eq!(attr.value, "{\"key\":\"value\"}");
177    }
178
179    #[test]
180    fn test_attribute_from_str_invalid_format() {
181        let result = Attribute::from_str("ptc:i");
182        assert!(result.is_err());
183
184        let result = Attribute::from_str("ptc");
185        assert!(result.is_err());
186    }
187
188    #[test]
189    fn test_attribute_from_str_invalid_type() {
190        let result = Attribute::from_str("ptc:x:1");
191        assert!(result.is_err());
192    }
193
194    #[test]
195    fn test_attribute_as_int() {
196        let attr = Attribute {
197            tag: "ptc".into(),
198            attribute_type: 'i',
199            value: "42".into(),
200        };
201        assert_eq!(attr.as_int().unwrap(), 42);
202
203        let attr = Attribute {
204            tag: "ptc".into(),
205            attribute_type: 'f',
206            value: "42".into(),
207        };
208        assert!(attr.as_int().is_err());
209
210        let attr = Attribute {
211            tag: "ptc".into(),
212            attribute_type: 'i',
213            value: "not_a_number".into(),
214        };
215        assert!(attr.as_int().is_err());
216    }
217
218    #[test]
219    fn test_attribute_as_float() {
220        let attr = Attribute::builder()
221            .tag("ptf")
222            .attribute_type('f')
223            .value("3.1")
224            .build();
225        assert_eq!(attr.as_float().unwrap(), 3.1);
226
227        let attr = Attribute::builder()
228            .tag("ptf")
229            .attribute_type('i')
230            .value("3.14")
231            .build();
232        assert!(attr.as_float().is_err());
233
234        let attr = Attribute::builder()
235            .tag("ptf")
236            .attribute_type('f')
237            .value("not_a_number")
238            .build();
239        assert!(attr.as_float().is_err());
240    }
241
242    #[test]
243    fn test_attribute_as_string() {
244        let attr = Attribute {
245            tag: "name".into(),
246            attribute_type: 'Z',
247            value: "test_value".into(),
248        };
249        assert_eq!(attr.as_string().unwrap(), "test_value");
250
251        let attr = Attribute {
252            tag: "name".into(),
253            attribute_type: 'i',
254            value: "test_value".into(),
255        };
256        assert!(attr.as_string().is_err());
257    }
258
259    #[test]
260    fn test_attribute_as_json() {
261        let attr = Attribute::builder()
262            .tag("data")
263            .attribute_type('J')
264            .value("{\"key\":\"value\"}")
265            .build();
266
267        let json = attr.as_json().unwrap();
268        assert_eq!(json["key"], "value");
269
270        let attr = Attribute::builder()
271            .tag("data")
272            .attribute_type('Z')
273            .value("{\"key\":\"value\"}")
274            .build();
275        assert!(attr.as_json().is_err());
276
277        let attr = Attribute::builder()
278            .tag("data")
279            .attribute_type('J')
280            .value("invalid_json")
281            .build();
282        assert!(attr.as_json().is_err());
283    }
284
285    #[test]
286    fn test_attribute_display() {
287        let attr = Attribute::builder()
288            .tag("ptc")
289            .attribute_type('i')
290            .value("1")
291            .build();
292        assert_eq!(attr.to_string(), "ptc:i:1");
293
294        let attr = Attribute::builder()
295            .tag("ptf")
296            .attribute_type('f')
297            .value("0.5")
298            .build();
299        assert_eq!(attr.to_string(), "ptf:f:0.5");
300    }
301}