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
219impl Value {
220    /// Convert a borrowed `&serde_json::Value` into a BEAM [`Value`].
221    ///
222    /// Zero-clone-preferred alternative to [`TryFrom<SerdeJsonValue>`].
223    /// Borrows the value and only allocates a `String` for `Text` and `Link`
224    /// variants (unavoidable — BEAM owns its strings).
225    ///
226    /// For `Null`, `Bool`, and `Number` variants: zero allocation.
227    /// For `String` and `Object` (link): one `String` allocation.
228    /// Saves one `JsonValue` clone per call vs `TryFrom<SerdeJsonValue>`.
229    pub fn from_json_ref(v: &SerdeJsonValue) -> Result<Value, &'static str> {
230        match v {
231            SerdeJsonValue::Null => Ok(Value::Null),
232            SerdeJsonValue::Bool(b) => Ok(Value::Bit(*b)),
233            SerdeJsonValue::String(s) => Ok(Value::Text(s.clone())),
234            SerdeJsonValue::Number(n) => match n.as_f64() {
235                Some(n) => Ok(Value::Number(n)),
236                _ => Err("not convertible to f64"),
237            },
238            SerdeJsonValue::Object(obj) => {
239                if let Some(soul) = obj.get("#").and_then(|v| v.as_str()) {
240                    Ok(Value::Link(soul.to_string()))
241                } else {
242                    Err("cannot convert json object into Value")
243                }
244            }
245            SerdeJsonValue::Array(_) => Err("cannot convert array into Value"),
246        }
247    }
248}
249///
250/// This conversion validates that the JSON value is one of the five
251/// supported types. Objects are accepted *only* if they are a Gun.js
252/// soul relation (`{"#": "soul"}`); all other objects are rejected.
253///
254/// # Errors
255///
256/// Returns `&'static str` if the JSON value cannot be represented as a
257/// BEAM [`Value`]:
258/// - Arrays → `"cannot convert array into Value"`
259/// - Non-soul objects → `"cannot convert json object into Value"`
260/// - Numbers not convertible to f64 → `"not convertible to f64"`
261///
262/// # Security Note
263///
264/// For production use, consider wrapping this in a custom error type rather
265/// than `&'static str`. The string error type limits programmatic error
266/// handling. This is a known limitation to address in a future API revision.
267impl TryFrom<SerdeJsonValue> for Value {
268    type Error = &'static str;
269
270    fn try_from(v: SerdeJsonValue) -> Result<Value, Self::Error> {
271        match v {
272            SerdeJsonValue::Null => Ok(Value::Null),
273            SerdeJsonValue::Bool(b) => Ok(Value::Bit(b)),
274            SerdeJsonValue::String(s) => Ok(Value::Text(s)),
275            SerdeJsonValue::Number(n) => match n.as_f64() {
276                Some(n) => Ok(Value::Number(n)),
277                _ => Err("not convertible to f64"),
278            },
279            SerdeJsonValue::Object(obj) => {
280                // Node reference: {"#": "soul"} → Value::Link
281                if let Some(soul) = obj.get("#").and_then(|v| v.as_str()) {
282                    Ok(Value::Link(soul.to_string()))
283                } else {
284                    Err("cannot convert json object into Value")
285                }
286            }
287            SerdeJsonValue::Array(_) => Err("cannot convert array into Value"),
288        }
289    }
290}
291
292/// Borrowed conversion from `&serde_json::Value` to BEAM [`Value`].
293///
294/// This avoids cloning the `JsonValue` when only a reference is available
295/// (e.g. during message parsing where the JSON tree is borrowed). The
296/// resulting `Value` still owns its data — `String` values are cloned
297/// only when the variant requires it (`Text`, `Link`), not for the
298/// common `Null`/`Bit`/`Number` cases.
299///
300/// # Errors
301///
302/// Same error semantics as [`TryFrom<SerdeJsonValue> for Value`].
303impl TryFrom<&SerdeJsonValue> for Value {
304    type Error = &'static str;
305
306    fn try_from(v: &SerdeJsonValue) -> Result<Value, Self::Error> {
307        match v {
308            SerdeJsonValue::Null => Ok(Value::Null),
309            SerdeJsonValue::Bool(b) => Ok(Value::Bit(*b)),
310            SerdeJsonValue::String(s) => Ok(Value::Text(s.clone())),
311            SerdeJsonValue::Number(n) => match n.as_f64() {
312                Some(n) => Ok(Value::Number(n)),
313                _ => Err("not convertible to f64"),
314            },
315            SerdeJsonValue::Object(obj) => {
316                if let Some(soul) = obj.get("#").and_then(|v| v.as_str()) {
317                    Ok(Value::Link(soul.to_string()))
318                } else {
319                    Err("cannot convert json object into Value")
320                }
321            }
322            SerdeJsonValue::Array(_) => Err("cannot convert array into Value"),
323        }
324    }
325}
326
327/// Converts a BEAM [`Value`] into a [`serde_json::Value`].
328///
329/// This is the inverse of [`TryFrom<SerdeJsonValue> for Value`]. The
330/// [`Value::Link`] variant serializes to a Gun.js soul relation object
331/// (`{"#": "soul"}`) so it round-trips correctly through JSON.
332impl From<Value> for SerdeJsonValue {
333    fn from(v: Value) -> SerdeJsonValue {
334        match v {
335            Value::Null => SerdeJsonValue::Null,
336            Value::Text(t) => SerdeJsonValue::String(t),
337            Value::Bit(b) => SerdeJsonValue::Bool(b),
338            Value::Number(n) => json!(n),
339            Value::Link(l) => json!({ "#": l }),
340        }
341    }
342}
343
344impl From<usize> for Value {
345    fn from(n: usize) -> Value {
346        Value::Number(n as f64)
347    }
348}
349
350impl From<f32> for Value {
351    fn from(n: f32) -> Value {
352        Value::Number(n as f64)
353    }
354}
355
356impl From<u64> for Value {
357    fn from(n: u64) -> Value {
358        Value::Number(n as f64)
359    }
360}
361
362impl From<&str> for Value {
363    fn from(s: &str) -> Value {
364        Value::Text(s.to_string())
365    }
366}
367
368impl From<String> for Value {
369    fn from(s: String) -> Value {
370        Value::Text(s)
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    // ── NodeData ──
379
380    #[test]
381    fn test_nodedata_default() {
382        let nd = NodeData::default();
383        assert!(nd.value.is_null());
384        assert_eq!(nd.updated_at, 0.0);
385    }
386
387    #[test]
388    fn test_nodedata_partial_eq() {
389        let a = NodeData {
390            value: Value::Text("x".into()),
391            updated_at: 1.0,
392        };
393        let b = NodeData {
394            value: Value::Text("x".into()),
395            updated_at: 1.0,
396        };
397        let c = NodeData {
398            value: Value::Text("y".into()),
399            updated_at: 1.0,
400        };
401        assert_eq!(a, b);
402        assert_ne!(a, c);
403    }
404
405    // ── Value::size ──
406
407    #[test]
408    fn test_value_size_text() {
409        assert_eq!(Value::Text("hello".into()).size(), 5);
410        assert_eq!(Value::Text("".into()).size(), 0);
411        assert_eq!(Value::Text("héllo".into()).size(), 6); // é is 2 bytes in UTF-8
412    }
413
414    #[test]
415    fn test_value_size_non_text() {
416        // Non-text variants use size_of_val
417        assert!(Value::Null.size() > 0);
418        assert!(Value::Bit(true).size() > 0);
419        assert!(Value::Number(42.0).size() > 0);
420        assert!(Value::Link("soul".into()).size() > 0);
421    }
422
423    // ── Value::to_string (wire format) ──
424
425    #[test]
426    fn test_value_to_string_null() {
427        assert_eq!(Value::Null.to_string(), "null");
428    }
429
430    #[test]
431    fn test_value_to_string_bit() {
432        assert_eq!(Value::Bit(true).to_string(), "true");
433        assert_eq!(Value::Bit(false).to_string(), "false");
434    }
435
436    #[test]
437    fn test_value_to_string_number() {
438        assert_eq!(Value::Number(42.0).to_string(), "42");
439        assert_eq!(Value::Number(3.15).to_string(), "3.15");
440    }
441
442    #[test]
443    fn test_value_to_string_text() {
444        assert_eq!(Value::Text("hello".into()).to_string(), "hello");
445    }
446
447    #[test]
448    fn test_value_to_string_link() {
449        assert_eq!(Value::Link("node/abc".into()).to_string(), "node/abc");
450    }
451
452    // ── Value type predicates ──
453
454    #[test]
455    fn test_value_is_null() {
456        assert!(Value::Null.is_null());
457        assert!(!Value::Bit(false).is_null());
458    }
459
460    #[test]
461    fn test_value_is_bit() {
462        assert!(Value::Bit(true).is_bit());
463        assert!(!Value::Null.is_bit());
464    }
465
466    #[test]
467    fn test_value_is_number() {
468        assert!(Value::Number(1.0).is_number());
469        assert!(!Value::Null.is_number());
470    }
471
472    #[test]
473    fn test_value_is_text() {
474        assert!(Value::Text("x".into()).is_text());
475        assert!(!Value::Null.is_text());
476    }
477
478    #[test]
479    fn test_value_is_link() {
480        assert!(Value::Link("soul".into()).is_link());
481        assert!(!Value::Null.is_link());
482    }
483
484    // ── Value accessors ──
485
486    #[test]
487    fn test_value_as_bit() {
488        assert_eq!(Value::Bit(true).as_bit(), Some(true));
489        assert_eq!(Value::Bit(false).as_bit(), Some(false));
490        assert_eq!(Value::Null.as_bit(), None);
491    }
492
493    #[test]
494    fn test_value_as_number() {
495        assert_eq!(Value::Number(42.0).as_number(), Some(42.0));
496        assert_eq!(Value::Null.as_number(), None);
497    }
498
499    #[test]
500    fn test_value_as_text() {
501        assert_eq!(Value::Text("hello".into()).as_text(), Some("hello"));
502        assert_eq!(Value::Null.as_text(), None);
503    }
504
505    #[test]
506    fn test_value_as_link() {
507        assert_eq!(Value::Link("soul".into()).as_link(), Some("soul"));
508        assert_eq!(Value::Null.as_link(), None);
509    }
510
511    // ── TryFrom<JsonValue> ──
512
513    #[test]
514    fn test_try_from_json_null() {
515        let v = Value::try_from(SerdeJsonValue::Null).unwrap();
516        assert!(v.is_null());
517    }
518
519    #[test]
520    fn test_try_from_json_bool() {
521        let v = Value::try_from(SerdeJsonValue::Bool(true)).unwrap();
522        assert_eq!(v.as_bit(), Some(true));
523    }
524
525    #[test]
526    fn test_try_from_json_string() {
527        let v = Value::try_from(SerdeJsonValue::String("hello".into())).unwrap();
528        assert_eq!(v.as_text(), Some("hello"));
529    }
530
531    #[test]
532    fn test_try_from_json_number() {
533        let v = Value::try_from(serde_json::json!(42.0)).unwrap();
534        assert_eq!(v.as_number(), Some(42.0));
535    }
536
537    #[test]
538    fn test_try_from_json_link() {
539        let json = serde_json::json!({ "#": "node/abc" });
540        let v = Value::try_from(json).unwrap();
541        assert_eq!(v.as_link(), Some("node/abc"));
542    }
543
544    #[test]
545    fn test_try_from_json_array_fails() {
546        let json = serde_json::json!([1, 2, 3]);
547        assert!(Value::try_from(json).is_err());
548    }
549
550    #[test]
551    fn test_try_from_json_object_without_soul_fails() {
552        let json = serde_json::json!({ "foo": "bar" });
553        assert!(Value::try_from(json).is_err());
554    }
555
556    // ── TryFrom<&JsonValue> (borrowed conversion) ──
557
558    #[test]
559    fn test_try_from_json_ref_null() {
560        let json = SerdeJsonValue::Null;
561        let v = Value::try_from(&json).unwrap();
562        assert!(v.is_null());
563    }
564
565    #[test]
566    fn test_try_from_json_ref_bool() {
567        let json = SerdeJsonValue::Bool(true);
568        let v = Value::try_from(&json).unwrap();
569        assert_eq!(v.as_bit(), Some(true));
570    }
571
572    #[test]
573    fn test_try_from_json_ref_string() {
574        let json = SerdeJsonValue::String("hello".into());
575        let v = Value::try_from(&json).unwrap();
576        assert_eq!(v.as_text(), Some("hello"));
577    }
578
579    #[test]
580    fn test_try_from_json_ref_number() {
581        let json = serde_json::json!(42.0);
582        let v = Value::try_from(&json).unwrap();
583        assert_eq!(v.as_number(), Some(42.0));
584    }
585
586    #[test]
587    fn test_try_from_json_ref_link() {
588        let json = serde_json::json!({ "#": "node/abc" });
589        let v = Value::try_from(&json).unwrap();
590        assert_eq!(v.as_link(), Some("node/abc"));
591    }
592
593    #[test]
594    fn test_try_from_json_ref_array_fails() {
595        let json = serde_json::json!([1, 2, 3]);
596        assert!(Value::try_from(&json).is_err());
597    }
598
599    #[test]
600    fn test_try_from_json_ref_object_without_soul_fails() {
601        let json = serde_json::json!({ "foo": "bar" });
602        assert!(Value::try_from(&json).is_err());
603    }
604
605    #[test]
606    fn test_try_from_json_ref_matches_owned() {
607        // Borrowed and owned conversions should produce identical results.
608        let cases = vec![
609            SerdeJsonValue::Null,
610            SerdeJsonValue::Bool(true),
611            SerdeJsonValue::Bool(false),
612            SerdeJsonValue::String("test".into()),
613            serde_json::json!(42.0),
614            serde_json::json!({ "#": "soul/123" }),
615        ];
616        for json in cases {
617            let owned = Value::try_from(json.clone()).unwrap();
618            let borrowed = Value::try_from(&json).unwrap();
619            assert_eq!(owned, borrowed, "mismatch for json: {}", json);
620        }
621    }
622
623    // ── From<Value> for JsonValue (round-trip) ──
624
625    #[test]
626    fn test_roundtrip_null() {
627        let v = Value::Null;
628        let json: SerdeJsonValue = v.clone().into();
629        let v2 = Value::try_from(json).unwrap();
630        assert_eq!(v, v2);
631    }
632
633    #[test]
634    fn test_roundtrip_bit() {
635        let v = Value::Bit(true);
636        let json: SerdeJsonValue = v.clone().into();
637        let v2 = Value::try_from(json).unwrap();
638        assert_eq!(v, v2);
639    }
640
641    #[test]
642    fn test_roundtrip_text() {
643        let v = Value::Text("hello world".into());
644        let json: SerdeJsonValue = v.clone().into();
645        let v2 = Value::try_from(json).unwrap();
646        assert_eq!(v, v2);
647    }
648
649    #[test]
650    fn test_roundtrip_number() {
651        let v = Value::Number(42.0);
652        let json: SerdeJsonValue = v.clone().into();
653        let v2 = Value::try_from(json).unwrap();
654        assert_eq!(v, v2);
655    }
656
657    #[test]
658    fn test_roundtrip_link() {
659        let v = Value::Link("node/abc".into());
660        let json: SerdeJsonValue = v.clone().into();
661        let v2 = Value::try_from(json).unwrap();
662        assert_eq!(v, v2);
663    }
664
665    // ── From<&str> / From<String> ──
666
667    #[test]
668    fn test_from_str() {
669        let v: Value = "hello".into();
670        assert_eq!(v.as_text(), Some("hello"));
671    }
672
673    #[test]
674    fn test_from_string() {
675        let v: Value = String::from("world").into();
676        assert_eq!(v.as_text(), Some("world"));
677    }
678
679    // ── From<usize> / From<u64> / From<f32> ──
680
681    #[test]
682    fn test_from_usize() {
683        let v: Value = 42usize.into();
684        assert_eq!(v.as_number(), Some(42.0));
685    }
686
687    #[test]
688    fn test_from_u64() {
689        let v: Value = 99u64.into();
690        assert_eq!(v.as_number(), Some(99.0));
691    }
692
693    #[test]
694    fn test_from_f32() {
695        let v: Value = 3.15f32.into();
696        assert!((v.as_number().unwrap() - 3.15).abs() < 0.001);
697    }
698
699    // ── Children type alias ──
700
701    #[test]
702    fn test_children_btreemap() {
703        let mut children: Children = BTreeMap::new();
704        children.insert(
705            "key1".to_string(),
706            NodeData {
707                value: Value::Text("v1".into()),
708                updated_at: 1.0,
709            },
710        );
711        children.insert(
712            "key2".to_string(),
713            NodeData {
714                value: Value::Text("v2".into()),
715                updated_at: 2.0,
716            },
717        );
718        // BTreeMap is sorted
719        let keys: Vec<&String> = children.keys().collect();
720        assert_eq!(keys, vec!["key1", "key2"]);
721    }
722}