Skip to main content

beam/
types.rs

1#![allow(clippy::inherent_to_string)] // to_string is wire-format serialization, not Display
2
3//! Core type system for BEAM — the Rust port of Gun.js.
4//!
5//! This module defines the fundamental data types that flow through the
6//! distributed graph database:
7//!
8//! - [`Value`] — the five wire-compatible leaf types (null, bool, number, text, link)
9//! - [`NodeData`] — a leaf node's payload (value + timestamp)
10//! - [`Children`] — a branch node's sorted child map
11//!
12//! ## Gun.js Wire Compatibility
13//!
14//! All types implement [`serde::Serialize`] / [`serde::Deserialize`] so they
15//! can be serialized to the JSON wire format that Gun.js uses. The
16//! [`Value::to_string`] method produces the Gun.js wire representation
17//! (not the [`std::fmt::Display`] representation).
18//!
19//! ## Value Validation
20//!
21//! Valid values are a subset of JSON: null, boolean, number (finite, not NaN
22//! or Infinity), text, or a soul relation link. Arrays need special
23//! algorithms to handle concurrency and are not supported directly. Objects
24//! are valid *only* as node references: `{"#": soul}`.
25//!
26//! ## Example
27//!
28//! ```
29//! use beam::types::{Value, NodeData};
30//!
31//! let v = Value::Text("hello".into());
32//! assert_eq!(v.to_string(), "hello");
33//! assert_eq!(v.size(), 5);
34//!
35//! let node = NodeData::default();
36//! assert!(node.value.is_null());
37//! ```
38
39use serde::{Deserialize, Serialize};
40use serde_json::{Value as SerdeJsonValue, json};
41use std::collections::BTreeMap;
42use std::convert::TryFrom;
43
44/// Branch node — a sorted map of child key to child data.
45///
46/// Each entry represents one child of a graph node. The key is the child's
47/// name within the parent; the value is the child's [`NodeData`] (value +
48/// timestamp). `BTreeMap` is used to ensure deterministic iteration order,
49/// which is important for consistent checksums across distributed peers.
50pub type Children = BTreeMap<String, NodeData>;
51
52/// Data stored in a leaf node of the graph.
53///
54/// Combines the actual [`Value`] with an `updated_at` timestamp (Unix epoch
55/// seconds, as used by Gun.js). The timestamp determines conflict resolution:
56/// when two peers send conflicting puts for the same key, the newer timestamp
57/// wins.
58///
59/// # Conflict Resolution
60///
61/// Gun.js uses "last write wins" semantics based on `updated_at`. If two
62/// peers write to the same key with the same timestamp, the implementation
63/// does not guarantee ordering — this is by design in Gun.js.
64#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
65pub struct NodeData {
66    /// The value stored at this node.
67    pub value: Value,
68    /// Unix timestamp (seconds) of the last update. Newer wins conflicts.
69    pub updated_at: f64,
70}
71
72impl Default for NodeData {
73    fn default() -> Self {
74        Self {
75            value: Value::Null,
76            updated_at: 0.0,
77        }
78    }
79}
80
81/// Value types supported by BEAM and Gun.js.
82///
83/// These are the five valid leaf types in the distributed graph. Each variant
84/// maps to a JSON type, with the exception of [`Value::Link`] which represents
85/// a Gun.js soul relation (`{"#": "node_id"}`).
86///
87/// # Wire Format
88///
89/// | Variant | JSON representation | Example |
90/// |---------|---------------------|----------|
91/// | `Null` | `null` | `Value::Null` |
92/// | `Bit` | `true` / `false` | `Value::Bit(true)` |
93/// | `Number` | number | `Value::Number(42.0)` |
94/// | `Text` | string | `Value::Text("hello")` |
95/// | `Link` | `{"#": "soul"}` | `Value::Link("node/abc")` |
96///
97/// # NaN / Infinity
98///
99/// `Value::Number(f64)` can technically hold NaN or Infinity, but these are
100/// not valid in the Gun.js wire format. When converting to JSON, NaN/Infinity
101/// will produce `null` (serde_json behavior). Callers should validate before
102/// constructing `Value::Number` from untrusted input.
103#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
104pub enum Value {
105    /// Absence of a value (not the same as "deleted").
106    Null,
107    /// Boolean flag.
108    Bit(bool),
109    /// Floating-point number. Should be finite (not NaN/Infinity).
110    Number(f64),
111    /// Unicode text string.
112    Text(String),
113    /// A soul relation — a reference to another node by ID.
114    Link(String),
115}
116
117impl Value {
118    /// Returns the approximate byte size of the value's payload.
119    ///
120    /// For [`Value::Text`], this is the string's byte length. For all other
121    /// variants, it is the `size_of_val` of the enum discriminant + data.
122    ///
123    /// This is used for memory budgeting.
124    pub fn size(&self) -> usize {
125        match self {
126            Value::Text(s) => s.len(),
127            _ => std::mem::size_of_val(self),
128        }
129    }
130
131    /// Serializes the value to its Gun.js wire-format string.
132    ///
133    /// **Not** a [`Display`] implementation — this produces the exact string
134    /// representation used on the wire:
135    ///
136    /// - `Null` → `"null"`
137    /// - `Bit(true)` → `"true"`
138    /// - `Bit(false)` → `"false"`
139    /// - `Number(42.0)` → `"42"`
140    /// - `Text("hello")` → `"hello"`
141    /// - `Link("node/abc")` → `"node/abc"` (the soul string, not JSON)
142    ///
143    /// For JSON serialization with proper typing, use
144    /// `serde_json::to_string(&value)` instead.
145    pub fn to_string(&self) -> String {
146        match self {
147            Value::Null => "null".to_string(),
148            Value::Bit(bool) => {
149                if *bool {
150                    "true".to_string()
151                } else {
152                    "false".to_string()
153                }
154            }
155            Value::Number(n) => n.to_string(),
156            Value::Text(t) => t.clone(),
157            Value::Link(l) => l.clone(),
158        }
159    }
160
161    /// Returns `true` if this value is [`Value::Null`].
162    pub fn is_null(&self) -> bool {
163        matches!(self, Value::Null)
164    }
165
166    /// Returns `true` if this value is [`Value::Bit`].
167    pub fn is_bit(&self) -> bool {
168        matches!(self, Value::Bit(_))
169    }
170
171    /// Returns `true` if this value is [`Value::Number`].
172    pub fn is_number(&self) -> bool {
173        matches!(self, Value::Number(_))
174    }
175
176    /// Returns `true` if this value is [`Value::Text`].
177    pub fn is_text(&self) -> bool {
178        matches!(self, Value::Text(_))
179    }
180
181    /// Returns `true` if this value is [`Value::Link`].
182    pub fn is_link(&self) -> bool {
183        matches!(self, Value::Link(_))
184    }
185
186    /// Returns `Some(bool)` if this value is [`Value::Bit`], else `None`.
187    pub fn as_bit(&self) -> Option<bool> {
188        match self {
189            Value::Bit(b) => Some(*b),
190            _ => None,
191        }
192    }
193
194    /// Returns `Some(f64)` if this value is [`Value::Number`], else `None`.
195    pub fn as_number(&self) -> Option<f64> {
196        match self {
197            Value::Number(n) => Some(*n),
198            _ => None,
199        }
200    }
201
202    /// Returns `Some(&str)` if this value is [`Value::Text`], else `None`.
203    pub fn as_text(&self) -> Option<&str> {
204        match self {
205            Value::Text(t) => Some(t.as_str()),
206            _ => None,
207        }
208    }
209
210    /// Returns `Some(&str)` if this value is [`Value::Link`], else `None`.
211    pub fn as_link(&self) -> Option<&str> {
212        match self {
213            Value::Link(l) => Some(l.as_str()),
214            _ => None,
215        }
216    }
217}
218
219/// Converts a [`serde_json::Value`] into a BEAM [`Value`].
220///
221/// This conversion validates that the JSON value is one of the five
222/// supported types. Objects are accepted *only* if they are a Gun.js
223/// soul relation (`{"#": "soul"}`); all other objects are rejected.
224///
225/// # Errors
226///
227/// Returns `&'static str` if the JSON value cannot be represented as a
228/// BEAM [`Value`]:
229/// - Arrays → `"cannot convert array into Value"`
230/// - Non-soul objects → `"cannot convert json object into Value"`
231/// - Numbers not convertible to f64 → `"not convertible to f64"`
232///
233/// # Security Note
234///
235/// For production use, consider wrapping this in a custom error type rather
236/// than `&'static str`. The string error type limits programmatic error
237/// handling. This is a known limitation to address in a future API revision.
238impl TryFrom<SerdeJsonValue> for Value {
239    type Error = &'static str;
240
241    fn try_from(v: SerdeJsonValue) -> Result<Value, Self::Error> {
242        match v {
243            SerdeJsonValue::Null => Ok(Value::Null),
244            SerdeJsonValue::Bool(b) => Ok(Value::Bit(b)),
245            SerdeJsonValue::String(s) => Ok(Value::Text(s)),
246            SerdeJsonValue::Number(n) => match n.as_f64() {
247                Some(n) => Ok(Value::Number(n)),
248                _ => Err("not convertible to f64"),
249            },
250            SerdeJsonValue::Object(obj) => {
251                // Node reference: {"#": "soul"} → Value::Link
252                if let Some(soul) = obj.get("#").and_then(|v| v.as_str()) {
253                    Ok(Value::Link(soul.to_string()))
254                } else {
255                    Err("cannot convert json object into Value")
256                }
257            }
258            SerdeJsonValue::Array(_) => Err("cannot convert array into Value"),
259        }
260    }
261}
262
263/// Converts a BEAM [`Value`] into a [`serde_json::Value`].
264///
265/// This is the inverse of [`TryFrom<SerdeJsonValue> for Value`]. The
266/// [`Value::Link`] variant serializes to a Gun.js soul relation object
267/// (`{"#": "soul"}`) so it round-trips correctly through JSON.
268impl From<Value> for SerdeJsonValue {
269    fn from(v: Value) -> SerdeJsonValue {
270        match v {
271            Value::Null => SerdeJsonValue::Null,
272            Value::Text(t) => SerdeJsonValue::String(t),
273            Value::Bit(b) => SerdeJsonValue::Bool(b),
274            Value::Number(n) => json!(n),
275            Value::Link(l) => json!({ "#": l }),
276        }
277    }
278}
279
280impl From<usize> for Value {
281    fn from(n: usize) -> Value {
282        Value::Number(n as f64)
283    }
284}
285
286impl From<f32> for Value {
287    fn from(n: f32) -> Value {
288        Value::Number(n as f64)
289    }
290}
291
292impl From<u64> for Value {
293    fn from(n: u64) -> Value {
294        Value::Number(n as f64)
295    }
296}
297
298impl From<&str> for Value {
299    fn from(s: &str) -> Value {
300        Value::Text(s.to_string())
301    }
302}
303
304impl From<String> for Value {
305    fn from(s: String) -> Value {
306        Value::Text(s)
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    // ── NodeData ──
315
316    #[test]
317    fn test_nodedata_default() {
318        let nd = NodeData::default();
319        assert!(nd.value.is_null());
320        assert_eq!(nd.updated_at, 0.0);
321    }
322
323    #[test]
324    fn test_nodedata_partial_eq() {
325        let a = NodeData {
326            value: Value::Text("x".into()),
327            updated_at: 1.0,
328        };
329        let b = NodeData {
330            value: Value::Text("x".into()),
331            updated_at: 1.0,
332        };
333        let c = NodeData {
334            value: Value::Text("y".into()),
335            updated_at: 1.0,
336        };
337        assert_eq!(a, b);
338        assert_ne!(a, c);
339    }
340
341    // ── Value::size ──
342
343    #[test]
344    fn test_value_size_text() {
345        assert_eq!(Value::Text("hello".into()).size(), 5);
346        assert_eq!(Value::Text("".into()).size(), 0);
347        assert_eq!(Value::Text("héllo".into()).size(), 6); // é is 2 bytes in UTF-8
348    }
349
350    #[test]
351    fn test_value_size_non_text() {
352        // Non-text variants use size_of_val
353        assert!(Value::Null.size() > 0);
354        assert!(Value::Bit(true).size() > 0);
355        assert!(Value::Number(42.0).size() > 0);
356        assert!(Value::Link("soul".into()).size() > 0);
357    }
358
359    // ── Value::to_string (wire format) ──
360
361    #[test]
362    fn test_value_to_string_null() {
363        assert_eq!(Value::Null.to_string(), "null");
364    }
365
366    #[test]
367    fn test_value_to_string_bit() {
368        assert_eq!(Value::Bit(true).to_string(), "true");
369        assert_eq!(Value::Bit(false).to_string(), "false");
370    }
371
372    #[test]
373    fn test_value_to_string_number() {
374        assert_eq!(Value::Number(42.0).to_string(), "42");
375        assert_eq!(Value::Number(3.15).to_string(), "3.15");
376    }
377
378    #[test]
379    fn test_value_to_string_text() {
380        assert_eq!(Value::Text("hello".into()).to_string(), "hello");
381    }
382
383    #[test]
384    fn test_value_to_string_link() {
385        assert_eq!(Value::Link("node/abc".into()).to_string(), "node/abc");
386    }
387
388    // ── Value type predicates ──
389
390    #[test]
391    fn test_value_is_null() {
392        assert!(Value::Null.is_null());
393        assert!(!Value::Bit(false).is_null());
394    }
395
396    #[test]
397    fn test_value_is_bit() {
398        assert!(Value::Bit(true).is_bit());
399        assert!(!Value::Null.is_bit());
400    }
401
402    #[test]
403    fn test_value_is_number() {
404        assert!(Value::Number(1.0).is_number());
405        assert!(!Value::Null.is_number());
406    }
407
408    #[test]
409    fn test_value_is_text() {
410        assert!(Value::Text("x".into()).is_text());
411        assert!(!Value::Null.is_text());
412    }
413
414    #[test]
415    fn test_value_is_link() {
416        assert!(Value::Link("soul".into()).is_link());
417        assert!(!Value::Null.is_link());
418    }
419
420    // ── Value accessors ──
421
422    #[test]
423    fn test_value_as_bit() {
424        assert_eq!(Value::Bit(true).as_bit(), Some(true));
425        assert_eq!(Value::Bit(false).as_bit(), Some(false));
426        assert_eq!(Value::Null.as_bit(), None);
427    }
428
429    #[test]
430    fn test_value_as_number() {
431        assert_eq!(Value::Number(42.0).as_number(), Some(42.0));
432        assert_eq!(Value::Null.as_number(), None);
433    }
434
435    #[test]
436    fn test_value_as_text() {
437        assert_eq!(Value::Text("hello".into()).as_text(), Some("hello"));
438        assert_eq!(Value::Null.as_text(), None);
439    }
440
441    #[test]
442    fn test_value_as_link() {
443        assert_eq!(Value::Link("soul".into()).as_link(), Some("soul"));
444        assert_eq!(Value::Null.as_link(), None);
445    }
446
447    // ── TryFrom<JsonValue> ──
448
449    #[test]
450    fn test_try_from_json_null() {
451        let v = Value::try_from(SerdeJsonValue::Null).unwrap();
452        assert!(v.is_null());
453    }
454
455    #[test]
456    fn test_try_from_json_bool() {
457        let v = Value::try_from(SerdeJsonValue::Bool(true)).unwrap();
458        assert_eq!(v.as_bit(), Some(true));
459    }
460
461    #[test]
462    fn test_try_from_json_string() {
463        let v = Value::try_from(SerdeJsonValue::String("hello".into())).unwrap();
464        assert_eq!(v.as_text(), Some("hello"));
465    }
466
467    #[test]
468    fn test_try_from_json_number() {
469        let v = Value::try_from(serde_json::json!(42.0)).unwrap();
470        assert_eq!(v.as_number(), Some(42.0));
471    }
472
473    #[test]
474    fn test_try_from_json_link() {
475        let json = serde_json::json!({ "#": "node/abc" });
476        let v = Value::try_from(json).unwrap();
477        assert_eq!(v.as_link(), Some("node/abc"));
478    }
479
480    #[test]
481    fn test_try_from_json_array_fails() {
482        let json = serde_json::json!([1, 2, 3]);
483        assert!(Value::try_from(json).is_err());
484    }
485
486    #[test]
487    fn test_try_from_json_object_without_soul_fails() {
488        let json = serde_json::json!({ "foo": "bar" });
489        assert!(Value::try_from(json).is_err());
490    }
491
492    // ── From<Value> for JsonValue (round-trip) ──
493
494    #[test]
495    fn test_roundtrip_null() {
496        let v = Value::Null;
497        let json: SerdeJsonValue = v.clone().into();
498        let v2 = Value::try_from(json).unwrap();
499        assert_eq!(v, v2);
500    }
501
502    #[test]
503    fn test_roundtrip_bit() {
504        let v = Value::Bit(true);
505        let json: SerdeJsonValue = v.clone().into();
506        let v2 = Value::try_from(json).unwrap();
507        assert_eq!(v, v2);
508    }
509
510    #[test]
511    fn test_roundtrip_text() {
512        let v = Value::Text("hello world".into());
513        let json: SerdeJsonValue = v.clone().into();
514        let v2 = Value::try_from(json).unwrap();
515        assert_eq!(v, v2);
516    }
517
518    #[test]
519    fn test_roundtrip_number() {
520        let v = Value::Number(42.0);
521        let json: SerdeJsonValue = v.clone().into();
522        let v2 = Value::try_from(json).unwrap();
523        assert_eq!(v, v2);
524    }
525
526    #[test]
527    fn test_roundtrip_link() {
528        let v = Value::Link("node/abc".into());
529        let json: SerdeJsonValue = v.clone().into();
530        let v2 = Value::try_from(json).unwrap();
531        assert_eq!(v, v2);
532    }
533
534    // ── From<&str> / From<String> ──
535
536    #[test]
537    fn test_from_str() {
538        let v: Value = "hello".into();
539        assert_eq!(v.as_text(), Some("hello"));
540    }
541
542    #[test]
543    fn test_from_string() {
544        let v: Value = String::from("world").into();
545        assert_eq!(v.as_text(), Some("world"));
546    }
547
548    // ── From<usize> / From<u64> / From<f32> ──
549
550    #[test]
551    fn test_from_usize() {
552        let v: Value = 42usize.into();
553        assert_eq!(v.as_number(), Some(42.0));
554    }
555
556    #[test]
557    fn test_from_u64() {
558        let v: Value = 99u64.into();
559        assert_eq!(v.as_number(), Some(99.0));
560    }
561
562    #[test]
563    fn test_from_f32() {
564        let v: Value = 3.15f32.into();
565        assert!((v.as_number().unwrap() - 3.15).abs() < 0.001);
566    }
567
568    // ── Children type alias ──
569
570    #[test]
571    fn test_children_btreemap() {
572        let mut children: Children = BTreeMap::new();
573        children.insert(
574            "key1".to_string(),
575            NodeData {
576                value: Value::Text("v1".into()),
577                updated_at: 1.0,
578            },
579        );
580        children.insert(
581            "key2".to_string(),
582            NodeData {
583                value: Value::Text("v2".into()),
584                updated_at: 2.0,
585            },
586        );
587        // BTreeMap is sorted
588        let keys: Vec<&String> = children.keys().collect();
589        assert_eq!(keys, vec!["key1", "key2"]);
590    }
591}