Skip to main content

grafeo_common/types/
value.rs

1//! Property values and keys for nodes and edges.
2//!
3//! [`Value`] is the dynamic type that can hold any property value - strings,
4//! numbers, lists, maps, etc. [`PropertyKey`] is an interned string for
5//! efficient property lookups.
6
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use std::fmt;
10use std::sync::Arc;
11
12use super::Timestamp;
13
14/// An interned property name - cheap to clone and compare.
15///
16/// Property names like "name", "age", "created_at" get used repeatedly, so
17/// we intern them with `Arc<str>`. You can create these from strings directly.
18#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
19pub struct PropertyKey(Arc<str>);
20
21impl PropertyKey {
22    /// Creates a new property key from a string.
23    #[must_use]
24    pub fn new(s: impl Into<Arc<str>>) -> Self {
25        Self(s.into())
26    }
27
28    /// Returns the string representation.
29    #[must_use]
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33}
34
35impl fmt::Debug for PropertyKey {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "PropertyKey({:?})", self.0)
38    }
39}
40
41impl fmt::Display for PropertyKey {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(f, "{}", self.0)
44    }
45}
46
47impl From<&str> for PropertyKey {
48    fn from(s: &str) -> Self {
49        Self::new(s)
50    }
51}
52
53impl From<String> for PropertyKey {
54    fn from(s: String) -> Self {
55        Self::new(s)
56    }
57}
58
59impl AsRef<str> for PropertyKey {
60    fn as_ref(&self) -> &str {
61        &self.0
62    }
63}
64
65/// A dynamically-typed property value.
66///
67/// Nodes and edges can have properties of various types - this enum holds
68/// them all. Follows the GQL type system, so you can store nulls, booleans,
69/// numbers, strings, timestamps, lists, and maps.
70///
71/// # Examples
72///
73/// ```
74/// use grafeo_common::types::Value;
75///
76/// let name = Value::from("Alice");
77/// let age = Value::from(30i64);
78/// let active = Value::from(true);
79///
80/// // Check types
81/// assert!(name.as_str().is_some());
82/// assert_eq!(age.as_int64(), Some(30));
83/// ```
84#[derive(Clone, PartialEq, Serialize, Deserialize)]
85pub enum Value {
86    /// Null/missing value
87    Null,
88
89    /// Boolean value
90    Bool(bool),
91
92    /// 64-bit signed integer
93    Int64(i64),
94
95    /// 64-bit floating point
96    Float64(f64),
97
98    /// UTF-8 string (uses Arc for cheap cloning)
99    String(Arc<str>),
100
101    /// Binary data
102    Bytes(Arc<[u8]>),
103
104    /// Timestamp with timezone
105    Timestamp(Timestamp),
106
107    /// Ordered list of values
108    List(Arc<[Value]>),
109
110    /// Key-value map (uses BTreeMap for deterministic ordering)
111    Map(Arc<BTreeMap<PropertyKey, Value>>),
112}
113
114impl Value {
115    /// Returns `true` if this value is null.
116    #[inline]
117    #[must_use]
118    pub const fn is_null(&self) -> bool {
119        matches!(self, Value::Null)
120    }
121
122    /// Returns the boolean value if this is a Bool, otherwise None.
123    #[inline]
124    #[must_use]
125    pub const fn as_bool(&self) -> Option<bool> {
126        match self {
127            Value::Bool(b) => Some(*b),
128            _ => None,
129        }
130    }
131
132    /// Returns the integer value if this is an Int64, otherwise None.
133    #[inline]
134    #[must_use]
135    pub const fn as_int64(&self) -> Option<i64> {
136        match self {
137            Value::Int64(i) => Some(*i),
138            _ => None,
139        }
140    }
141
142    /// Returns the float value if this is a Float64, otherwise None.
143    #[inline]
144    #[must_use]
145    pub const fn as_float64(&self) -> Option<f64> {
146        match self {
147            Value::Float64(f) => Some(*f),
148            _ => None,
149        }
150    }
151
152    /// Returns the string value if this is a String, otherwise None.
153    #[inline]
154    #[must_use]
155    pub fn as_str(&self) -> Option<&str> {
156        match self {
157            Value::String(s) => Some(s),
158            _ => None,
159        }
160    }
161
162    /// Returns the bytes value if this is Bytes, otherwise None.
163    #[inline]
164    #[must_use]
165    pub fn as_bytes(&self) -> Option<&[u8]> {
166        match self {
167            Value::Bytes(b) => Some(b),
168            _ => None,
169        }
170    }
171
172    /// Returns the timestamp value if this is a Timestamp, otherwise None.
173    #[inline]
174    #[must_use]
175    pub const fn as_timestamp(&self) -> Option<Timestamp> {
176        match self {
177            Value::Timestamp(t) => Some(*t),
178            _ => None,
179        }
180    }
181
182    /// Returns the list value if this is a List, otherwise None.
183    #[inline]
184    #[must_use]
185    pub fn as_list(&self) -> Option<&[Value]> {
186        match self {
187            Value::List(l) => Some(l),
188            _ => None,
189        }
190    }
191
192    /// Returns the map value if this is a Map, otherwise None.
193    #[inline]
194    #[must_use]
195    pub fn as_map(&self) -> Option<&BTreeMap<PropertyKey, Value>> {
196        match self {
197            Value::Map(m) => Some(m),
198            _ => None,
199        }
200    }
201
202    /// Returns the type name of this value.
203    #[must_use]
204    pub const fn type_name(&self) -> &'static str {
205        match self {
206            Value::Null => "NULL",
207            Value::Bool(_) => "BOOL",
208            Value::Int64(_) => "INT64",
209            Value::Float64(_) => "FLOAT64",
210            Value::String(_) => "STRING",
211            Value::Bytes(_) => "BYTES",
212            Value::Timestamp(_) => "TIMESTAMP",
213            Value::List(_) => "LIST",
214            Value::Map(_) => "MAP",
215        }
216    }
217
218    /// Serializes this value to bytes.
219    #[must_use]
220    pub fn serialize(&self) -> Vec<u8> {
221        bincode::serde::encode_to_vec(self, bincode::config::standard())
222            .expect("Value serialization should not fail")
223    }
224
225    /// Deserializes a value from bytes.
226    ///
227    /// # Errors
228    ///
229    /// Returns an error if the bytes do not represent a valid Value.
230    pub fn deserialize(bytes: &[u8]) -> Result<Self, bincode::error::DecodeError> {
231        let (value, _) = bincode::serde::decode_from_slice(bytes, bincode::config::standard())?;
232        Ok(value)
233    }
234}
235
236impl fmt::Debug for Value {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        match self {
239            Value::Null => write!(f, "Null"),
240            Value::Bool(b) => write!(f, "Bool({b})"),
241            Value::Int64(i) => write!(f, "Int64({i})"),
242            Value::Float64(fl) => write!(f, "Float64({fl})"),
243            Value::String(s) => write!(f, "String({s:?})"),
244            Value::Bytes(b) => write!(f, "Bytes([{}; {} bytes])", b.first().unwrap_or(&0), b.len()),
245            Value::Timestamp(t) => write!(f, "Timestamp({t:?})"),
246            Value::List(l) => write!(f, "List({l:?})"),
247            Value::Map(m) => write!(f, "Map({m:?})"),
248        }
249    }
250}
251
252impl fmt::Display for Value {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        match self {
255            Value::Null => write!(f, "NULL"),
256            Value::Bool(b) => write!(f, "{b}"),
257            Value::Int64(i) => write!(f, "{i}"),
258            Value::Float64(fl) => write!(f, "{fl}"),
259            Value::String(s) => write!(f, "{s:?}"),
260            Value::Bytes(b) => write!(f, "<bytes: {} bytes>", b.len()),
261            Value::Timestamp(t) => write!(f, "{t}"),
262            Value::List(l) => {
263                write!(f, "[")?;
264                for (i, v) in l.iter().enumerate() {
265                    if i > 0 {
266                        write!(f, ", ")?;
267                    }
268                    write!(f, "{v}")?;
269                }
270                write!(f, "]")
271            }
272            Value::Map(m) => {
273                write!(f, "{{")?;
274                for (i, (k, v)) in m.iter().enumerate() {
275                    if i > 0 {
276                        write!(f, ", ")?;
277                    }
278                    write!(f, "{k}: {v}")?;
279                }
280                write!(f, "}}")
281            }
282        }
283    }
284}
285
286// Convenient From implementations
287impl From<bool> for Value {
288    fn from(b: bool) -> Self {
289        Value::Bool(b)
290    }
291}
292
293impl From<i64> for Value {
294    fn from(i: i64) -> Self {
295        Value::Int64(i)
296    }
297}
298
299impl From<i32> for Value {
300    fn from(i: i32) -> Self {
301        Value::Int64(i64::from(i))
302    }
303}
304
305impl From<f64> for Value {
306    fn from(f: f64) -> Self {
307        Value::Float64(f)
308    }
309}
310
311impl From<f32> for Value {
312    fn from(f: f32) -> Self {
313        Value::Float64(f64::from(f))
314    }
315}
316
317impl From<&str> for Value {
318    fn from(s: &str) -> Self {
319        Value::String(s.into())
320    }
321}
322
323impl From<String> for Value {
324    fn from(s: String) -> Self {
325        Value::String(s.into())
326    }
327}
328
329impl From<Arc<str>> for Value {
330    fn from(s: Arc<str>) -> Self {
331        Value::String(s)
332    }
333}
334
335impl From<Vec<u8>> for Value {
336    fn from(b: Vec<u8>) -> Self {
337        Value::Bytes(b.into())
338    }
339}
340
341impl From<&[u8]> for Value {
342    fn from(b: &[u8]) -> Self {
343        Value::Bytes(b.into())
344    }
345}
346
347impl From<Timestamp> for Value {
348    fn from(t: Timestamp) -> Self {
349        Value::Timestamp(t)
350    }
351}
352
353impl<T: Into<Value>> From<Vec<T>> for Value {
354    fn from(v: Vec<T>) -> Self {
355        Value::List(v.into_iter().map(Into::into).collect())
356    }
357}
358
359impl<T: Into<Value>> From<Option<T>> for Value {
360    fn from(opt: Option<T>) -> Self {
361        match opt {
362            Some(v) => v.into(),
363            None => Value::Null,
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn test_value_type_checks() {
374        assert!(Value::Null.is_null());
375        assert!(!Value::Bool(true).is_null());
376
377        assert_eq!(Value::Bool(true).as_bool(), Some(true));
378        assert_eq!(Value::Bool(false).as_bool(), Some(false));
379        assert_eq!(Value::Int64(42).as_bool(), None);
380
381        assert_eq!(Value::Int64(42).as_int64(), Some(42));
382        assert_eq!(Value::String("test".into()).as_int64(), None);
383
384        assert_eq!(Value::Float64(1.234).as_float64(), Some(1.234));
385        assert_eq!(Value::String("hello".into()).as_str(), Some("hello"));
386    }
387
388    #[test]
389    fn test_value_from_conversions() {
390        let v: Value = true.into();
391        assert_eq!(v.as_bool(), Some(true));
392
393        let v: Value = 42i64.into();
394        assert_eq!(v.as_int64(), Some(42));
395
396        let v: Value = 1.234f64.into();
397        assert_eq!(v.as_float64(), Some(1.234));
398
399        let v: Value = "hello".into();
400        assert_eq!(v.as_str(), Some("hello"));
401
402        let v: Value = vec![1u8, 2, 3].into();
403        assert_eq!(v.as_bytes(), Some(&[1u8, 2, 3][..]));
404    }
405
406    #[test]
407    fn test_value_serialization_roundtrip() {
408        let values = vec![
409            Value::Null,
410            Value::Bool(true),
411            Value::Int64(i64::MAX),
412            Value::Float64(std::f64::consts::PI),
413            Value::String("hello world".into()),
414            Value::Bytes(vec![0, 1, 2, 255].into()),
415            Value::List(vec![Value::Int64(1), Value::Int64(2)].into()),
416        ];
417
418        for v in values {
419            let bytes = v.serialize();
420            let decoded = Value::deserialize(&bytes).unwrap();
421            assert_eq!(v, decoded);
422        }
423    }
424
425    #[test]
426    fn test_property_key() {
427        let key = PropertyKey::new("name");
428        assert_eq!(key.as_str(), "name");
429
430        let key2: PropertyKey = "age".into();
431        assert_eq!(key2.as_str(), "age");
432
433        // Keys should be comparable ("age" < "name" alphabetically)
434        assert!(key2 < key);
435    }
436
437    #[test]
438    fn test_value_type_name() {
439        assert_eq!(Value::Null.type_name(), "NULL");
440        assert_eq!(Value::Bool(true).type_name(), "BOOL");
441        assert_eq!(Value::Int64(0).type_name(), "INT64");
442        assert_eq!(Value::Float64(0.0).type_name(), "FLOAT64");
443        assert_eq!(Value::String("".into()).type_name(), "STRING");
444        assert_eq!(Value::Bytes(vec![].into()).type_name(), "BYTES");
445        assert_eq!(Value::List(vec![].into()).type_name(), "LIST");
446        assert_eq!(Value::Map(BTreeMap::new().into()).type_name(), "MAP");
447    }
448}