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 arena_btreemap::BTreeMap;
40use serde::{Deserialize, Serialize};
41use serde_json::{Value as SerdeJsonValue, json};
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.
50///
51/// On native targets, the BTreeMap uses [`SyncBumpArena`][crate::arena::SyncBumpArena]
52/// for arena allocation — O(1) allocation via bump pointer, O(chunks) drop
53/// instead of O(entries). On WASM, the global allocator is used (no `std::sync`).
54#[cfg(not(target_arch = "wasm32"))]
55pub type Children = BTreeMap<String, NodeData, crate::arena::SyncBumpArena>;
56
57/// WASM fallback: `Children` uses the global allocator (no `std::sync::Mutex`).
58#[cfg(target_arch = "wasm32")]
59pub type Children = BTreeMap<String, NodeData>;
60
61/// Data stored in a leaf node of the graph.
62///
63/// Combines the actual [`Value`] with an `updated_at` timestamp (Unix epoch
64/// seconds, as used by Gun.js). The timestamp determines conflict resolution:
65/// when two peers send conflicting puts for the same key, the newer timestamp
66/// wins.
67///
68/// # Conflict Resolution
69///
70/// Gun.js uses "last write wins" semantics based on `updated_at`. If two
71/// peers write to the same key with the same timestamp, the implementation
72/// does not guarantee ordering — this is by design in Gun.js.
73#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
74pub struct NodeData {
75    /// The value stored at this node.
76    pub value: Value,
77    /// Unix timestamp (seconds) of the last update. Newer wins conflicts.
78    pub updated_at: f64,
79}
80
81impl Default for NodeData {
82    fn default() -> Self {
83        Self {
84            value: Value::Null,
85            updated_at: 0.0,
86        }
87    }
88}
89
90/// Value types supported by BEAM and Gun.js.
91///
92/// These are the five valid leaf types in the distributed graph. Each variant
93/// maps to a JSON type, with the exception of [`Value::Link`] which represents
94/// a Gun.js soul relation (`{"#": "node_id"}`).
95///
96/// # Wire Format
97///
98/// | Variant | JSON representation | Example |
99/// |---------|---------------------|----------|
100/// | `Null` | `null` | `Value::Null` |
101/// | `Bit` | `true` / `false` | `Value::Bit(true)` |
102/// | `Number` | number | `Value::Number(42.0)` |
103/// | `Text` | string | `Value::Text("hello")` |
104/// | `Link` | `{"#": "soul"}` | `Value::Link("node/abc")` |
105///
106/// # NaN / Infinity
107///
108/// `Value::Number(f64)` can technically hold NaN or Infinity, but these are
109/// not valid in the Gun.js wire format. When converting to JSON, NaN/Infinity
110/// will produce `null` (serde_json behavior). Callers should validate before
111/// constructing `Value::Number` from untrusted input.
112#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
113pub enum Value {
114    /// Absence of a value (not the same as "deleted").
115    Null,
116    /// Boolean flag.
117    Bit(bool),
118    /// Floating-point number. Should be finite (not NaN/Infinity).
119    Number(f64),
120    /// Unicode text string.
121    Text(String),
122    /// A soul relation — a reference to another node by ID.
123    Link(String),
124}
125
126impl Value {
127    /// Returns the approximate byte size of the value's payload.
128    ///
129    /// For [`Value::Text`], this is the string's byte length. For all other
130    /// variants, it is the `size_of_val` of the enum discriminant + data.
131    ///
132    /// This is used for memory budgeting.
133    pub fn size(&self) -> usize {
134        match self {
135            Value::Text(s) => s.len(),
136            _ => std::mem::size_of_val(self),
137        }
138    }
139
140    /// Serializes the value to its Gun.js wire-format string.
141    ///
142    /// **Not** a [`Display`] implementation — this produces the exact string
143    /// representation used on the wire:
144    ///
145    /// - `Null` → `"null"`
146    /// - `Bit(true)` → `"true"`
147    /// - `Bit(false)` → `"false"`
148    /// - `Number(42.0)` → `"42"`
149    /// - `Text("hello")` → `"hello"`
150    /// - `Link("node/abc")` → `"node/abc"` (the soul string, not JSON)
151    ///
152    /// For JSON serialization with proper typing, use
153    /// `serde_json::to_string(&value)` instead.
154    pub fn to_string(&self) -> String {
155        match self {
156            Value::Null => "null".to_string(),
157            Value::Bit(bool) => {
158                if *bool {
159                    "true".to_string()
160                } else {
161                    "false".to_string()
162                }
163            }
164            Value::Number(n) => n.to_string(),
165            Value::Text(t) => t.clone(),
166            Value::Link(l) => l.clone(),
167        }
168    }
169
170    /// Returns `true` if this value is [`Value::Null`].
171    pub fn is_null(&self) -> bool {
172        matches!(self, Value::Null)
173    }
174
175    /// Returns `true` if this value is [`Value::Bit`].
176    pub fn is_bit(&self) -> bool {
177        matches!(self, Value::Bit(_))
178    }
179
180    /// Returns `true` if this value is [`Value::Number`].
181    pub fn is_number(&self) -> bool {
182        matches!(self, Value::Number(_))
183    }
184
185    /// Returns `true` if this value is [`Value::Text`].
186    pub fn is_text(&self) -> bool {
187        matches!(self, Value::Text(_))
188    }
189
190    /// Returns `true` if this value is [`Value::Link`].
191    pub fn is_link(&self) -> bool {
192        matches!(self, Value::Link(_))
193    }
194
195    /// Returns `Some(bool)` if this value is [`Value::Bit`], else `None`.
196    pub fn as_bit(&self) -> Option<bool> {
197        match self {
198            Value::Bit(b) => Some(*b),
199            _ => None,
200        }
201    }
202
203    /// Returns `Some(f64)` if this value is [`Value::Number`], else `None`.
204    pub fn as_number(&self) -> Option<f64> {
205        match self {
206            Value::Number(n) => Some(*n),
207            _ => None,
208        }
209    }
210
211    /// Returns `Some(&str)` if this value is [`Value::Text`], else `None`.
212    pub fn as_text(&self) -> Option<&str> {
213        match self {
214            Value::Text(t) => Some(t.as_str()),
215            _ => None,
216        }
217    }
218
219    /// Returns `Some(&str)` if this value is [`Value::Link`], else `None`.
220    pub fn as_link(&self) -> Option<&str> {
221        match self {
222            Value::Link(l) => Some(l.as_str()),
223            _ => None,
224        }
225    }
226}
227
228impl Value {
229    /// Convert a borrowed `&serde_json::Value` into a BEAM [`Value`].
230    ///
231    /// Zero-clone-preferred alternative to [`TryFrom<SerdeJsonValue>`].
232    /// Borrows the value and only allocates a `String` for `Text` and `Link`
233    /// variants (unavoidable — BEAM owns its strings).
234    ///
235    /// For `Null`, `Bool`, and `Number` variants: zero allocation.
236    /// For `String` and `Object` (link): one `String` allocation.
237    /// Saves one `JsonValue` clone per call vs `TryFrom<SerdeJsonValue>`.
238    pub fn from_json_ref(v: &SerdeJsonValue) -> Result<Value, &'static str> {
239        match v {
240            SerdeJsonValue::Null => Ok(Value::Null),
241            SerdeJsonValue::Bool(b) => Ok(Value::Bit(*b)),
242            SerdeJsonValue::String(s) => Ok(Value::Text(s.clone())),
243            SerdeJsonValue::Number(n) => match n.as_f64() {
244                Some(n) => Ok(Value::Number(n)),
245                _ => Err("not convertible to f64"),
246            },
247            SerdeJsonValue::Object(obj) => {
248                if let Some(soul) = obj.get("#").and_then(|v| v.as_str()) {
249                    Ok(Value::Link(soul.to_string()))
250                } else {
251                    Err("cannot convert json object into Value")
252                }
253            }
254            SerdeJsonValue::Array(_) => Err("cannot convert array into Value"),
255        }
256    }
257}
258///
259/// This conversion validates that the JSON value is one of the five
260/// supported types. Objects are accepted *only* if they are a Gun.js
261/// soul relation (`{"#": "soul"}`); all other objects are rejected.
262///
263/// # Errors
264///
265/// Returns `&'static str` if the JSON value cannot be represented as a
266/// BEAM [`Value`]:
267/// - Arrays → `"cannot convert array into Value"`
268/// - Non-soul objects → `"cannot convert json object into Value"`
269/// - Numbers not convertible to f64 → `"not convertible to f64"`
270///
271/// # Security Note
272///
273/// For production use, consider wrapping this in a custom error type rather
274/// than `&'static str`. The string error type limits programmatic error
275/// handling. This is a known limitation to address in a future API revision.
276impl TryFrom<SerdeJsonValue> for Value {
277    type Error = &'static str;
278
279    fn try_from(v: SerdeJsonValue) -> Result<Value, Self::Error> {
280        match v {
281            SerdeJsonValue::Null => Ok(Value::Null),
282            SerdeJsonValue::Bool(b) => Ok(Value::Bit(b)),
283            SerdeJsonValue::String(s) => Ok(Value::Text(s)),
284            SerdeJsonValue::Number(n) => match n.as_f64() {
285                Some(n) => Ok(Value::Number(n)),
286                _ => Err("not convertible to f64"),
287            },
288            SerdeJsonValue::Object(obj) => {
289                // Node reference: {"#": "soul"} → Value::Link
290                if let Some(soul) = obj.get("#").and_then(|v| v.as_str()) {
291                    Ok(Value::Link(soul.to_string()))
292                } else {
293                    Err("cannot convert json object into Value")
294                }
295            }
296            SerdeJsonValue::Array(_) => Err("cannot convert array into Value"),
297        }
298    }
299}
300
301/// Borrowed conversion from `&serde_json::Value` to BEAM [`Value`].
302///
303/// This avoids cloning the `JsonValue` when only a reference is available
304/// (e.g. during message parsing where the JSON tree is borrowed). The
305/// resulting `Value` still owns its data — `String` values are cloned
306/// only when the variant requires it (`Text`, `Link`), not for the
307/// common `Null`/`Bit`/`Number` cases.
308///
309/// # Errors
310///
311/// Same error semantics as [`TryFrom<SerdeJsonValue> for Value`].
312impl TryFrom<&SerdeJsonValue> for Value {
313    type Error = &'static str;
314
315    fn try_from(v: &SerdeJsonValue) -> Result<Value, Self::Error> {
316        match v {
317            SerdeJsonValue::Null => Ok(Value::Null),
318            SerdeJsonValue::Bool(b) => Ok(Value::Bit(*b)),
319            SerdeJsonValue::String(s) => Ok(Value::Text(s.clone())),
320            SerdeJsonValue::Number(n) => match n.as_f64() {
321                Some(n) => Ok(Value::Number(n)),
322                _ => Err("not convertible to f64"),
323            },
324            SerdeJsonValue::Object(obj) => {
325                if let Some(soul) = obj.get("#").and_then(|v| v.as_str()) {
326                    Ok(Value::Link(soul.to_string()))
327                } else {
328                    Err("cannot convert json object into Value")
329                }
330            }
331            SerdeJsonValue::Array(_) => Err("cannot convert array into Value"),
332        }
333    }
334}
335
336/// Converts a BEAM [`Value`] into a [`serde_json::Value`].
337///
338/// This is the inverse of [`TryFrom<SerdeJsonValue> for Value`]. The
339/// [`Value::Link`] variant serializes to a Gun.js soul relation object
340/// (`{"#": "soul"}`) so it round-trips correctly through JSON.
341impl From<Value> for SerdeJsonValue {
342    fn from(v: Value) -> SerdeJsonValue {
343        match v {
344            Value::Null => SerdeJsonValue::Null,
345            Value::Text(t) => SerdeJsonValue::String(t),
346            Value::Bit(b) => SerdeJsonValue::Bool(b),
347            Value::Number(n) => json!(n),
348            Value::Link(l) => json!({ "#": l }),
349        }
350    }
351}
352
353impl From<usize> for Value {
354    fn from(n: usize) -> Value {
355        Value::Number(n as f64)
356    }
357}
358
359impl From<f32> for Value {
360    fn from(n: f32) -> Value {
361        Value::Number(n as f64)
362    }
363}
364
365impl From<u64> for Value {
366    fn from(n: u64) -> Value {
367        Value::Number(n as f64)
368    }
369}
370
371impl From<&str> for Value {
372    fn from(s: &str) -> Value {
373        Value::Text(s.to_string())
374    }
375}
376
377impl From<String> for Value {
378    fn from(s: String) -> Value {
379        Value::Text(s)
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    // ── NodeData ──
388
389    #[test]
390    fn test_nodedata_default() {
391        let nd = NodeData::default();
392        assert!(nd.value.is_null());
393        assert_eq!(nd.updated_at, 0.0);
394    }
395
396    #[test]
397    fn test_nodedata_partial_eq() {
398        let a = NodeData {
399            value: Value::Text("x".into()),
400            updated_at: 1.0,
401        };
402        let b = NodeData {
403            value: Value::Text("x".into()),
404            updated_at: 1.0,
405        };
406        let c = NodeData {
407            value: Value::Text("y".into()),
408            updated_at: 1.0,
409        };
410        assert_eq!(a, b);
411        assert_ne!(a, c);
412    }
413
414    // ── Value::size ──
415
416    #[test]
417    fn test_value_size_text() {
418        assert_eq!(Value::Text("hello".into()).size(), 5);
419        assert_eq!(Value::Text("".into()).size(), 0);
420        assert_eq!(Value::Text("héllo".into()).size(), 6); // é is 2 bytes in UTF-8
421    }
422
423    #[test]
424    fn test_value_size_non_text() {
425        // Non-text variants use size_of_val
426        assert!(Value::Null.size() > 0);
427        assert!(Value::Bit(true).size() > 0);
428        assert!(Value::Number(42.0).size() > 0);
429        assert!(Value::Link("soul".into()).size() > 0);
430    }
431
432    // ── Value::to_string (wire format) ──
433
434    #[test]
435    fn test_value_to_string_null() {
436        assert_eq!(Value::Null.to_string(), "null");
437    }
438
439    #[test]
440    fn test_value_to_string_bit() {
441        assert_eq!(Value::Bit(true).to_string(), "true");
442        assert_eq!(Value::Bit(false).to_string(), "false");
443    }
444
445    #[test]
446    fn test_value_to_string_number() {
447        assert_eq!(Value::Number(42.0).to_string(), "42");
448        assert_eq!(Value::Number(3.15).to_string(), "3.15");
449    }
450
451    #[test]
452    fn test_value_to_string_text() {
453        assert_eq!(Value::Text("hello".into()).to_string(), "hello");
454    }
455
456    #[test]
457    fn test_value_to_string_link() {
458        assert_eq!(Value::Link("node/abc".into()).to_string(), "node/abc");
459    }
460
461    // ── Value type predicates ──
462
463    #[test]
464    fn test_value_is_null() {
465        assert!(Value::Null.is_null());
466        assert!(!Value::Bit(false).is_null());
467    }
468
469    #[test]
470    fn test_value_is_bit() {
471        assert!(Value::Bit(true).is_bit());
472        assert!(!Value::Null.is_bit());
473    }
474
475    #[test]
476    fn test_value_is_number() {
477        assert!(Value::Number(1.0).is_number());
478        assert!(!Value::Null.is_number());
479    }
480
481    #[test]
482    fn test_value_is_text() {
483        assert!(Value::Text("x".into()).is_text());
484        assert!(!Value::Null.is_text());
485    }
486
487    #[test]
488    fn test_value_is_link() {
489        assert!(Value::Link("soul".into()).is_link());
490        assert!(!Value::Null.is_link());
491    }
492
493    // ── Value accessors ──
494
495    #[test]
496    fn test_value_as_bit() {
497        assert_eq!(Value::Bit(true).as_bit(), Some(true));
498        assert_eq!(Value::Bit(false).as_bit(), Some(false));
499        assert_eq!(Value::Null.as_bit(), None);
500    }
501
502    #[test]
503    fn test_value_as_number() {
504        assert_eq!(Value::Number(42.0).as_number(), Some(42.0));
505        assert_eq!(Value::Null.as_number(), None);
506    }
507
508    #[test]
509    fn test_value_as_text() {
510        assert_eq!(Value::Text("hello".into()).as_text(), Some("hello"));
511        assert_eq!(Value::Null.as_text(), None);
512    }
513
514    #[test]
515    fn test_value_as_link() {
516        assert_eq!(Value::Link("soul".into()).as_link(), Some("soul"));
517        assert_eq!(Value::Null.as_link(), None);
518    }
519
520    // ── TryFrom<JsonValue> ──
521
522    #[test]
523    fn test_try_from_json_null() {
524        let v = Value::try_from(SerdeJsonValue::Null).unwrap();
525        assert!(v.is_null());
526    }
527
528    #[test]
529    fn test_try_from_json_bool() {
530        let v = Value::try_from(SerdeJsonValue::Bool(true)).unwrap();
531        assert_eq!(v.as_bit(), Some(true));
532    }
533
534    #[test]
535    fn test_try_from_json_string() {
536        let v = Value::try_from(SerdeJsonValue::String("hello".into())).unwrap();
537        assert_eq!(v.as_text(), Some("hello"));
538    }
539
540    #[test]
541    fn test_try_from_json_number() {
542        let v = Value::try_from(serde_json::json!(42.0)).unwrap();
543        assert_eq!(v.as_number(), Some(42.0));
544    }
545
546    #[test]
547    fn test_try_from_json_link() {
548        let json = serde_json::json!({ "#": "node/abc" });
549        let v = Value::try_from(json).unwrap();
550        assert_eq!(v.as_link(), Some("node/abc"));
551    }
552
553    #[test]
554    fn test_try_from_json_array_fails() {
555        let json = serde_json::json!([1, 2, 3]);
556        assert!(Value::try_from(json).is_err());
557    }
558
559    #[test]
560    fn test_try_from_json_object_without_soul_fails() {
561        let json = serde_json::json!({ "foo": "bar" });
562        assert!(Value::try_from(json).is_err());
563    }
564
565    // ── TryFrom<&JsonValue> (borrowed conversion) ──
566
567    #[test]
568    fn test_try_from_json_ref_null() {
569        let json = SerdeJsonValue::Null;
570        let v = Value::try_from(&json).unwrap();
571        assert!(v.is_null());
572    }
573
574    #[test]
575    fn test_try_from_json_ref_bool() {
576        let json = SerdeJsonValue::Bool(true);
577        let v = Value::try_from(&json).unwrap();
578        assert_eq!(v.as_bit(), Some(true));
579    }
580
581    #[test]
582    fn test_try_from_json_ref_string() {
583        let json = SerdeJsonValue::String("hello".into());
584        let v = Value::try_from(&json).unwrap();
585        assert_eq!(v.as_text(), Some("hello"));
586    }
587
588    #[test]
589    fn test_try_from_json_ref_number() {
590        let json = serde_json::json!(42.0);
591        let v = Value::try_from(&json).unwrap();
592        assert_eq!(v.as_number(), Some(42.0));
593    }
594
595    #[test]
596    fn test_try_from_json_ref_link() {
597        let json = serde_json::json!({ "#": "node/abc" });
598        let v = Value::try_from(&json).unwrap();
599        assert_eq!(v.as_link(), Some("node/abc"));
600    }
601
602    #[test]
603    fn test_try_from_json_ref_array_fails() {
604        let json = serde_json::json!([1, 2, 3]);
605        assert!(Value::try_from(&json).is_err());
606    }
607
608    #[test]
609    fn test_try_from_json_ref_object_without_soul_fails() {
610        let json = serde_json::json!({ "foo": "bar" });
611        assert!(Value::try_from(&json).is_err());
612    }
613
614    #[test]
615    fn test_try_from_json_ref_matches_owned() {
616        // Borrowed and owned conversions should produce identical results.
617        let cases = vec![
618            SerdeJsonValue::Null,
619            SerdeJsonValue::Bool(true),
620            SerdeJsonValue::Bool(false),
621            SerdeJsonValue::String("test".into()),
622            serde_json::json!(42.0),
623            serde_json::json!({ "#": "soul/123" }),
624        ];
625        for json in cases {
626            let owned = Value::try_from(json.clone()).unwrap();
627            let borrowed = Value::try_from(&json).unwrap();
628            assert_eq!(owned, borrowed, "mismatch for json: {}", json);
629        }
630    }
631
632    // ── From<Value> for JsonValue (round-trip) ──
633
634    #[test]
635    fn test_roundtrip_null() {
636        let v = Value::Null;
637        let json: SerdeJsonValue = v.clone().into();
638        let v2 = Value::try_from(json).unwrap();
639        assert_eq!(v, v2);
640    }
641
642    #[test]
643    fn test_roundtrip_bit() {
644        let v = Value::Bit(true);
645        let json: SerdeJsonValue = v.clone().into();
646        let v2 = Value::try_from(json).unwrap();
647        assert_eq!(v, v2);
648    }
649
650    #[test]
651    fn test_roundtrip_text() {
652        let v = Value::Text("hello world".into());
653        let json: SerdeJsonValue = v.clone().into();
654        let v2 = Value::try_from(json).unwrap();
655        assert_eq!(v, v2);
656    }
657
658    #[test]
659    fn test_roundtrip_number() {
660        let v = Value::Number(42.0);
661        let json: SerdeJsonValue = v.clone().into();
662        let v2 = Value::try_from(json).unwrap();
663        assert_eq!(v, v2);
664    }
665
666    #[test]
667    fn test_roundtrip_link() {
668        let v = Value::Link("node/abc".into());
669        let json: SerdeJsonValue = v.clone().into();
670        let v2 = Value::try_from(json).unwrap();
671        assert_eq!(v, v2);
672    }
673
674    // ── From<&str> / From<String> ──
675
676    #[test]
677    fn test_from_str() {
678        let v: Value = "hello".into();
679        assert_eq!(v.as_text(), Some("hello"));
680    }
681
682    #[test]
683    fn test_from_string() {
684        let v: Value = String::from("world").into();
685        assert_eq!(v.as_text(), Some("world"));
686    }
687
688    // ── From<usize> / From<u64> / From<f32> ──
689
690    #[test]
691    fn test_from_usize() {
692        let v: Value = 42usize.into();
693        assert_eq!(v.as_number(), Some(42.0));
694    }
695
696    #[test]
697    fn test_from_u64() {
698        let v: Value = 99u64.into();
699        assert_eq!(v.as_number(), Some(99.0));
700    }
701
702    #[test]
703    fn test_from_f32() {
704        let v: Value = 3.15f32.into();
705        assert!((v.as_number().unwrap() - 3.15).abs() < 0.001);
706    }
707
708    // ── Children type alias ──
709
710    #[test]
711    fn test_children_btreemap() {
712        let mut children: Children = BTreeMap::default();
713        children.insert(
714            "key1".to_string(),
715            NodeData {
716                value: Value::Text("v1".into()),
717                updated_at: 1.0,
718            },
719        );
720        children.insert(
721            "key2".to_string(),
722            NodeData {
723                value: Value::Text("v2".into()),
724                updated_at: 2.0,
725            },
726        );
727        // BTreeMap is sorted
728        let keys: Vec<&String> = children.keys().collect();
729        assert_eq!(keys, vec!["key1", "key2"]);
730    }
731}