···11+//! Typed structs for [Loro](https://loro.dev/) CRDTs.
22+//!
33+//! [Loro](https://loro.dev/) is a CRDT library — it gives you conflict-free
44+//! replicated containers (lists, maps, trees, rich text, etc.) with undo/redo,
55+//! sync, and time travel. But working with it directly means string-keyed
66+//! container lookups and manual [`LoroValue`](loro::LoroValue) matching.
77+//!
88+//! `#[loroscope]` generates typed accessors from a struct definition, so field
99+//! access is checked at compile time.
1010+//!
1111+//! # Example
1212+//!
1313+//! ```
1414+//! use loroscope::loroscope;
1515+//!
1616+//! #[loroscope]
1717+//! struct TodoItem {
1818+//! title: Text,
1919+//! done: bool,
2020+//! }
2121+//!
2222+//! #[loroscope]
2323+//! struct App {
2424+//! todos: List<TodoItem>,
2525+//! }
2626+//!
2727+//! let app = App::new();
2828+//!
2929+//! let item = app.todos().push_new();
3030+//! item.title().insert(0, "Buy milk").unwrap();
3131+//! item.set_done(false);
3232+//!
3333+//! assert_eq!(app.todos().len(), 1);
3434+//! assert!(!app.todos().get(0).unwrap().done());
3535+//!
3636+//! // The underlying LoroDoc is always available for export, import,
3737+//! // undo, subscriptions, etc.
3838+//! let _doc = app.doc();
3939+//! ```
4040+//!
4141+//! # Field types
4242+//!
4343+//! | Type | Accessor |
4444+//! |---|---|
4545+//! | `f64`, `i64`, `bool`, `String` | Getter + `set_` setter |
4646+//! | [`Text`], [`Counter`] | Getter returning the container |
4747+//! | [`List<T>`], [`Map<V>`], [`MovableList<T>`], [`Tree<T>`] | Getter returning a typed collection |
4848+//! | `LoroList`, `LoroMap`, `LoroText`, … | Getter returning the raw Loro container |
4949+//! | Any `#[loroscope]` struct | Getter returning a nested typed view |
5050+5151+#![deny(missing_docs)]
5252+#![deny(unreachable_pub)]
5353+#![deny(clippy::unwrap_used)]
5454+#![deny(clippy::doc_markdown)]
5555+5656+pub use loroscope_macros::loroscope;
5757+5858+/// A rich-text container. Type alias for [`loro::LoroText`].
5959+pub type Text = loro::LoroText;
6060+6161+/// A CRDT counter. Type alias for [`loro::LoroCounter`].
6262+pub type Counter = loro::LoroCounter;
6363+6464+mod list;
6565+mod map;
6666+mod movable_list;
6767+mod tree;
6868+6969+pub use list::List;
7070+pub use loro::{TreeID, TreeParentId};
7171+pub use map::Map;
7272+pub use movable_list::MovableList;
7373+pub use tree::Tree;
7474+7575+/// Trait implemented automatically by `#[loroscope]` structs.
7676+///
7777+/// This allows structs to be used as elements in [`List<T>`], [`Map<V>`],
7878+/// and [`MovableList<T>`]. You do not need to implement this trait manually.
7979+pub trait FromLoroMap: Sized {
8080+ /// Creates a typed view from a [`loro::LoroMap`].
8181+ fn from_loro_map(map: loro::LoroMap) -> Self;
8282+}
8383+8484+#[doc(hidden)]
8585+#[allow(missing_docs)]
8686+pub mod __private {
8787+ pub use loro::{
8888+ Container, ContainerTrait, LoroCounter, LoroDoc, LoroList, LoroMap, LoroMovableList,
8989+ LoroText, LoroTree, LoroValue, ValueOrContainer,
9090+ };
9191+}
+159
loroscope/src/list.rs
···11+use std::marker::PhantomData;
22+33+use loro::{LoroList, LoroMap, LoroValue};
44+55+use crate::FromLoroMap;
66+77+/// A typed list where every element is of type `T`.
88+///
99+/// Use this as a field type in a `#[loroscope]` struct:
1010+///
1111+/// ```ignore
1212+/// #[loroscope]
1313+/// struct Project {
1414+/// items: List<Item>,
1515+/// }
1616+/// ```
1717+///
1818+/// `T` can be a `#[loroscope]` struct, a primitive (`f64`, `i64`, `bool`,
1919+/// `String`), or a Loro container type (`LoroText`, `LoroCounter`,
2020+/// `LoroTree`).
2121+pub struct List<T> {
2222+ list: LoroList,
2323+ _phantom: PhantomData<T>,
2424+}
2525+2626+impl<T> Clone for List<T> {
2727+ fn clone(&self) -> Self {
2828+ Self {
2929+ list: self.list.clone(),
3030+ _phantom: PhantomData,
3131+ }
3232+ }
3333+}
3434+3535+impl<T> List<T> {
3636+ /// Creates a typed view from a [`LoroList`].
3737+ pub fn new(list: LoroList) -> Self {
3838+ Self {
3939+ list,
4040+ _phantom: PhantomData,
4141+ }
4242+ }
4343+4444+ /// Returns the number of elements.
4545+ pub fn len(&self) -> usize {
4646+ self.list.len()
4747+ }
4848+4949+ /// Returns `true` if the list is empty.
5050+ pub fn is_empty(&self) -> bool {
5151+ self.list.len() == 0
5252+ }
5353+5454+ /// Returns a reference to the underlying [`LoroList`].
5555+ pub fn raw(&self) -> &LoroList {
5656+ &self.list
5757+ }
5858+}
5959+6060+// --- #[loroscope] structs ----------------------------------------------------
6161+6262+impl<T: FromLoroMap> List<T> {
6363+ /// Returns the element at `index`, or `None` if out of bounds.
6464+ pub fn get(&self, index: usize) -> Option<T> {
6565+ self.list
6666+ .get(index)
6767+ .and_then(|v| v.into_container().ok())
6868+ .and_then(|c| c.into_map().ok())
6969+ .map(T::from_loro_map)
7070+ }
7171+7272+ /// Returns an iterator over all elements.
7373+ pub fn iter(&self) -> impl Iterator<Item = T> + '_ {
7474+ (0..self.len()).map(|i| self.get(i).expect("index is within bounds"))
7575+ }
7676+7777+ /// Appends a new element and returns it.
7878+ pub fn push_new(&self) -> T {
7979+ T::from_loro_map(
8080+ self.list
8181+ .push_container(LoroMap::new())
8282+ .expect("push to attached list"),
8383+ )
8484+ }
8585+}
8686+8787+// --- Primitives --------------------------------------------------------------
8888+8989+macro_rules! impl_primitive {
9090+ ($ty:ty, $variant:pat => $expr:expr) => {
9191+ impl List<$ty> {
9292+ /// Returns the element at `index`, or `None` if out of bounds.
9393+ pub fn get(&self, index: usize) -> Option<$ty> {
9494+ self.list
9595+ .get(index)
9696+ .and_then(|v| v.into_value().ok())
9797+ .and_then(|v| match v {
9898+ $variant => Some($expr),
9999+ _ => None,
100100+ })
101101+ }
102102+103103+ /// Returns an iterator over all elements.
104104+ pub fn iter(&self) -> impl Iterator<Item = $ty> + '_ {
105105+ (0..self.len()).map(|i| self.get(i).expect("index is within bounds"))
106106+ }
107107+ }
108108+ };
109109+}
110110+111111+impl_primitive!(f64, LoroValue::Double(d) => d);
112112+impl_primitive!(i64, LoroValue::I64(i) => i);
113113+impl_primitive!(bool, LoroValue::Bool(b) => b);
114114+impl_primitive!(String, LoroValue::String(s) => s.to_string());
115115+116116+macro_rules! impl_push {
117117+ ($($value_ty:ty => $param_ty:ty),* $(,)?) => {$(
118118+ impl List<$value_ty> {
119119+ /// Appends a value to the end of the list.
120120+ pub fn push(&self, val: $param_ty) {
121121+ self.list.push(val).expect("push to attached list");
122122+ }
123123+ }
124124+ )*};
125125+}
126126+127127+impl_push!(f64 => f64, i64 => i64, bool => bool, String => &str);
128128+129129+// --- Loro containers ---------------------------------------------------------
130130+131131+macro_rules! impl_container {
132132+ ($ty:ty, $variant:ident) => {
133133+ impl List<$ty> {
134134+ /// Returns the container at `index`, or `None` if out of bounds.
135135+ pub fn get(&self, index: usize) -> Option<$ty> {
136136+ self.list
137137+ .get(index)
138138+ .and_then(|v| v.into_container().ok())
139139+ .and_then(|c| c.$variant().ok())
140140+ }
141141+142142+ /// Appends a new empty container and returns it.
143143+ pub fn push_new(&self) -> $ty {
144144+ self.list
145145+ .push_container(<$ty>::new())
146146+ .expect("push to attached list")
147147+ }
148148+149149+ /// Returns an iterator over all elements.
150150+ pub fn iter(&self) -> impl Iterator<Item = $ty> + '_ {
151151+ (0..self.len()).map(|i| self.get(i).expect("index is within bounds"))
152152+ }
153153+ }
154154+ };
155155+}
156156+157157+impl_container!(loro::LoroText, into_text);
158158+impl_container!(loro::LoroCounter, into_counter);
159159+impl_container!(loro::LoroTree, into_tree);
+176
loroscope/src/map.rs
···11+use std::marker::PhantomData;
22+33+use loro::{LoroList, LoroMap, LoroMovableList, LoroTree, LoroValue};
44+55+use crate::FromLoroMap;
66+77+/// A typed string-keyed map where every value is of type `V`.
88+///
99+/// Use this as a field type in a `#[loroscope]` struct:
1010+///
1111+/// ```ignore
1212+/// #[loroscope]
1313+/// struct Settings {
1414+/// values: Map<i64>,
1515+/// }
1616+/// ```
1717+///
1818+/// `V` can be a `#[loroscope]` struct, a primitive (`f64`, `i64`, `bool`,
1919+/// `String`), a Loro container type (`LoroText`, `LoroCounter`, `LoroTree`),
2020+/// or another collection ([`List<T>`](crate::List),
2121+/// [`MovableList<T>`](crate::MovableList), `Map<V>`).
2222+pub struct Map<V> {
2323+ map: LoroMap,
2424+ _phantom: PhantomData<V>,
2525+}
2626+2727+impl<V> Clone for Map<V> {
2828+ fn clone(&self) -> Self {
2929+ Self {
3030+ map: self.map.clone(),
3131+ _phantom: PhantomData,
3232+ }
3333+ }
3434+}
3535+3636+impl<V> Map<V> {
3737+ /// Creates a typed view from a [`LoroMap`].
3838+ pub fn new(map: LoroMap) -> Self {
3939+ Self {
4040+ map,
4141+ _phantom: PhantomData,
4242+ }
4343+ }
4444+4545+ /// Returns the number of entries.
4646+ pub fn len(&self) -> usize {
4747+ self.map.len()
4848+ }
4949+5050+ /// Returns `true` if the map is empty.
5151+ pub fn is_empty(&self) -> bool {
5252+ self.map.len() == 0
5353+ }
5454+5555+ /// Returns a reference to the underlying [`LoroMap`].
5656+ pub fn raw(&self) -> &LoroMap {
5757+ &self.map
5858+ }
5959+}
6060+6161+// --- #[loroscope] structs ----------------------------------------------------
6262+6363+impl<V: FromLoroMap> Map<V> {
6464+ /// Returns the value for `key`, or `None` if the key is missing.
6565+ pub fn get(&self, key: &str) -> Option<V> {
6666+ self.map
6767+ .get(key)
6868+ .and_then(|v| v.into_container().ok())
6969+ .and_then(|c| c.into_map().ok())
7070+ .map(V::from_loro_map)
7171+ }
7272+7373+ /// Returns the value for `key`, creating it if it doesn't exist.
7474+ pub fn get_or_create(&self, key: &str) -> V {
7575+ V::from_loro_map(
7676+ self.map
7777+ .get_or_create_container(key, LoroMap::new())
7878+ .expect("create container on attached map"),
7979+ )
8080+ }
8181+}
8282+8383+// --- Primitives --------------------------------------------------------------
8484+8585+macro_rules! impl_primitive {
8686+ ($ty:ty, $variant:pat => $expr:expr) => {
8787+ impl Map<$ty> {
8888+ /// Returns the value for `key`, or `None` if missing.
8989+ pub fn get(&self, key: &str) -> Option<$ty> {
9090+ self.map
9191+ .get(key)
9292+ .and_then(|v| v.into_value().ok())
9393+ .and_then(|v| match v {
9494+ $variant => Some($expr),
9595+ _ => None,
9696+ })
9797+ }
9898+ }
9999+ };
100100+}
101101+102102+impl_primitive!(f64, LoroValue::Double(d) => d);
103103+impl_primitive!(i64, LoroValue::I64(i) => i);
104104+impl_primitive!(bool, LoroValue::Bool(b) => b);
105105+impl_primitive!(String, LoroValue::String(s) => s.to_string());
106106+107107+macro_rules! impl_insert {
108108+ ($($value_ty:ty => $param_ty:ty),* $(,)?) => {$(
109109+ impl Map<$value_ty> {
110110+ /// Inserts a value for the given key, overwriting any previous value.
111111+ pub fn insert(&self, key: &str, val: $param_ty) {
112112+ self.map.insert(key, val).expect("insert into attached map");
113113+ }
114114+ }
115115+ )*};
116116+}
117117+118118+impl_insert!(f64 => f64, i64 => i64, bool => bool, String => &str);
119119+120120+// --- Loro containers ---------------------------------------------------------
121121+122122+macro_rules! impl_container {
123123+ ($ty:ty, $variant:ident) => {
124124+ impl Map<$ty> {
125125+ /// Returns the container for `key`, or `None` if missing.
126126+ pub fn get(&self, key: &str) -> Option<$ty> {
127127+ self.map
128128+ .get(key)
129129+ .and_then(|v| v.into_container().ok())
130130+ .and_then(|c| c.$variant().ok())
131131+ }
132132+133133+ /// Returns the container for `key`, creating it if it doesn't exist.
134134+ pub fn get_or_create(&self, key: &str) -> $ty {
135135+ self.map
136136+ .get_or_create_container(key, <$ty>::new())
137137+ .expect("create container on attached map")
138138+ }
139139+ }
140140+ };
141141+}
142142+143143+impl_container!(loro::LoroText, into_text);
144144+impl_container!(loro::LoroCounter, into_counter);
145145+impl_container!(loro::LoroTree, into_tree);
146146+147147+// --- Wrapper types -----------------------------------------------------------
148148+149149+macro_rules! impl_wrapper {
150150+ ($wrapper:ident < $($param:ident),+ >, $loro_ty:ty, $variant:ident) => {
151151+ impl<$($param),+> Map<crate::$wrapper<$($param),+>> {
152152+ /// Returns the value for `key`, or `None` if missing.
153153+ pub fn get(&self, key: &str) -> Option<crate::$wrapper<$($param),+>> {
154154+ self.map
155155+ .get(key)
156156+ .and_then(|v| v.into_container().ok())
157157+ .and_then(|c| c.$variant().ok())
158158+ .map(crate::$wrapper::new)
159159+ }
160160+161161+ /// Returns the value for `key`, creating it if it doesn't exist.
162162+ pub fn get_or_create(&self, key: &str) -> crate::$wrapper<$($param),+> {
163163+ crate::$wrapper::new(
164164+ self.map
165165+ .get_or_create_container(key, <$loro_ty>::new())
166166+ .expect("create container on attached map"),
167167+ )
168168+ }
169169+ }
170170+ };
171171+}
172172+173173+impl_wrapper!(List<T>, LoroList, into_list);
174174+impl_wrapper!(MovableList<T>, LoroMovableList, into_movable_list);
175175+impl_wrapper!(Map<V>, LoroMap, into_map);
176176+impl_wrapper!(Tree<T>, LoroTree, into_tree);
+172
loroscope/src/movable_list.rs
···11+use std::marker::PhantomData;
22+33+use loro::{LoroMap, LoroMovableList, LoroValue};
44+55+use crate::FromLoroMap;
66+77+/// A typed list that supports reordering and in-place updates.
88+///
99+/// Unlike [`List`](crate::List), elements can be moved with [`mov`](Self::mov)
1010+/// and replaced with `set`.
1111+///
1212+/// Use this as a field type in a `#[loroscope]` struct:
1313+///
1414+/// ```ignore
1515+/// #[loroscope]
1616+/// struct Playlist {
1717+/// songs: MovableList<Song>,
1818+/// }
1919+/// ```
2020+///
2121+/// `T` can be a `#[loroscope]` struct, a primitive (`f64`, `i64`, `bool`,
2222+/// `String`), or a Loro container type (`LoroText`, `LoroCounter`,
2323+/// `LoroTree`).
2424+pub struct MovableList<T> {
2525+ list: LoroMovableList,
2626+ _phantom: PhantomData<T>,
2727+}
2828+2929+impl<T> Clone for MovableList<T> {
3030+ fn clone(&self) -> Self {
3131+ Self {
3232+ list: self.list.clone(),
3333+ _phantom: PhantomData,
3434+ }
3535+ }
3636+}
3737+3838+impl<T> MovableList<T> {
3939+ /// Creates a typed view from a [`LoroMovableList`].
4040+ pub fn new(list: LoroMovableList) -> Self {
4141+ Self {
4242+ list,
4343+ _phantom: PhantomData,
4444+ }
4545+ }
4646+4747+ /// Returns the number of elements.
4848+ pub fn len(&self) -> usize {
4949+ self.list.len()
5050+ }
5151+5252+ /// Returns `true` if the list is empty.
5353+ pub fn is_empty(&self) -> bool {
5454+ self.list.len() == 0
5555+ }
5656+5757+ /// Returns a reference to the underlying [`LoroMovableList`].
5858+ pub fn raw(&self) -> &LoroMovableList {
5959+ &self.list
6060+ }
6161+6262+ /// Moves the element at index `from` to index `to`.
6363+ pub fn mov(&self, from: usize, to: usize) {
6464+ self.list.mov(from, to).expect("mov on attached list");
6565+ }
6666+}
6767+6868+// --- #[loroscope] structs ----------------------------------------------------
6969+7070+impl<T: FromLoroMap> MovableList<T> {
7171+ /// Returns the element at `index`, or `None` if out of bounds.
7272+ pub fn get(&self, index: usize) -> Option<T> {
7373+ self.list
7474+ .get(index)
7575+ .and_then(|v| v.into_container().ok())
7676+ .and_then(|c| c.into_map().ok())
7777+ .map(T::from_loro_map)
7878+ }
7979+8080+ /// Returns an iterator over all elements.
8181+ pub fn iter(&self) -> impl Iterator<Item = T> + '_ {
8282+ (0..self.len()).map(|i| self.get(i).expect("index is within bounds"))
8383+ }
8484+8585+ /// Appends a new element and returns it.
8686+ pub fn push_new(&self) -> T {
8787+ T::from_loro_map(
8888+ self.list
8989+ .push_container(LoroMap::new())
9090+ .expect("push to attached list"),
9191+ )
9292+ }
9393+}
9494+9595+// --- Primitives --------------------------------------------------------------
9696+9797+macro_rules! impl_primitive {
9898+ ($ty:ty, $variant:pat => $expr:expr) => {
9999+ impl MovableList<$ty> {
100100+ /// Returns the element at `index`, or `None` if out of bounds.
101101+ pub fn get(&self, index: usize) -> Option<$ty> {
102102+ self.list
103103+ .get(index)
104104+ .and_then(|v| v.into_value().ok())
105105+ .and_then(|v| match v {
106106+ $variant => Some($expr),
107107+ _ => None,
108108+ })
109109+ }
110110+111111+ /// Returns an iterator over all elements.
112112+ pub fn iter(&self) -> impl Iterator<Item = $ty> + '_ {
113113+ (0..self.len()).map(|i| self.get(i).expect("index is within bounds"))
114114+ }
115115+ }
116116+ };
117117+}
118118+119119+impl_primitive!(f64, LoroValue::Double(d) => d);
120120+impl_primitive!(i64, LoroValue::I64(i) => i);
121121+impl_primitive!(bool, LoroValue::Bool(b) => b);
122122+impl_primitive!(String, LoroValue::String(s) => s.to_string());
123123+124124+macro_rules! impl_push_set {
125125+ ($($value_ty:ty => $param_ty:ty),* $(,)?) => {$(
126126+ impl MovableList<$value_ty> {
127127+ /// Appends a value to the end of the list.
128128+ pub fn push(&self, val: $param_ty) {
129129+ self.list.push(val).expect("push to attached list");
130130+ }
131131+132132+ /// Replaces the value at `index`.
133133+ pub fn set(&self, index: usize, val: $param_ty) {
134134+ self.list.set(index, val).expect("set on attached list");
135135+ }
136136+ }
137137+ )*};
138138+}
139139+140140+impl_push_set!(f64 => f64, i64 => i64, bool => bool, String => &str);
141141+142142+// --- Loro containers ---------------------------------------------------------
143143+144144+macro_rules! impl_container {
145145+ ($ty:ty, $variant:ident) => {
146146+ impl MovableList<$ty> {
147147+ /// Returns the container at `index`, or `None` if out of bounds.
148148+ pub fn get(&self, index: usize) -> Option<$ty> {
149149+ self.list
150150+ .get(index)
151151+ .and_then(|v| v.into_container().ok())
152152+ .and_then(|c| c.$variant().ok())
153153+ }
154154+155155+ /// Appends a new empty container and returns it.
156156+ pub fn push_new(&self) -> $ty {
157157+ self.list
158158+ .push_container(<$ty>::new())
159159+ .expect("push to attached list")
160160+ }
161161+162162+ /// Returns an iterator over all elements.
163163+ pub fn iter(&self) -> impl Iterator<Item = $ty> + '_ {
164164+ (0..self.len()).map(|i| self.get(i).expect("index is within bounds"))
165165+ }
166166+ }
167167+ };
168168+}
169169+170170+impl_container!(loro::LoroText, into_text);
171171+impl_container!(loro::LoroCounter, into_counter);
172172+impl_container!(loro::LoroTree, into_tree);
+192
loroscope/src/tree.rs
···11+use std::marker::PhantomData;
22+33+use loro::{LoroTree, TreeID, TreeParentId};
44+55+use crate::FromLoroMap;
66+77+/// A typed tree (hierarchy) where each node's metadata is of type `T`.
88+///
99+/// Use this as a field type in a `#[loroscope]` struct:
1010+///
1111+/// ```ignore
1212+/// #[loroscope]
1313+/// struct FileNode {
1414+/// name: Text,
1515+/// size: i64,
1616+/// }
1717+///
1818+/// #[loroscope]
1919+/// struct Project {
2020+/// files: Tree<FileNode>,
2121+/// }
2222+/// ```
2323+///
2424+/// Each tree node has a [`TreeID`] and an associated metadata map that is
2525+/// converted to `T` via [`FromLoroMap`]. Structural operations (move, delete,
2626+/// parent/child queries) are available on all `Tree<T>`, while typed metadata
2727+/// access (`get`, `create_root`, etc.) requires `T: FromLoroMap`.
2828+///
2929+/// Fractional indexing is enabled by default so positional methods
3030+/// ([`mov_to`](Self::mov_to), [`mov_after`](Self::mov_after),
3131+/// [`mov_before`](Self::mov_before), [`create_at`](Self::create_at))
3232+/// always work.
3333+pub struct Tree<T> {
3434+ tree: LoroTree,
3535+ _phantom: PhantomData<T>,
3636+}
3737+3838+impl<T> Clone for Tree<T> {
3939+ fn clone(&self) -> Self {
4040+ Self {
4141+ tree: self.tree.clone(),
4242+ _phantom: PhantomData,
4343+ }
4444+ }
4545+}
4646+4747+impl<T> Tree<T> {
4848+ /// Creates a typed view from a [`LoroTree`].
4949+ ///
5050+ /// Enables fractional indexing (jitter=0) so that positional operations
5151+ /// are always available.
5252+ pub fn new(tree: LoroTree) -> Self {
5353+ tree.enable_fractional_index(0);
5454+ Self {
5555+ tree,
5656+ _phantom: PhantomData,
5757+ }
5858+ }
5959+6060+ /// Returns `true` if the tree has no (non-deleted) root nodes.
6161+ pub fn is_empty(&self) -> bool {
6262+ self.tree.roots().is_empty()
6363+ }
6464+6565+ /// Returns a reference to the underlying [`LoroTree`].
6666+ pub fn raw(&self) -> &LoroTree {
6767+ &self.tree
6868+ }
6969+7070+ /// Returns all root node IDs (excluding deleted nodes).
7171+ pub fn roots(&self) -> Vec<TreeID> {
7272+ self.tree.roots()
7373+ }
7474+7575+ /// Returns the children of `parent`, or `None` if the node doesn't exist.
7676+ pub fn children(&self, parent: TreeID) -> Option<Vec<TreeID>> {
7777+ self.tree.children(parent)
7878+ }
7979+8080+ /// Returns the number of children of `parent`, or `None` if the node
8181+ /// doesn't exist.
8282+ pub fn children_num(&self, parent: TreeID) -> Option<usize> {
8383+ self.tree.children_num(parent)
8484+ }
8585+8686+ /// Returns the parent of `target`, or `None` if the node doesn't exist.
8787+ pub fn parent(&self, target: TreeID) -> Option<TreeParentId> {
8888+ self.tree.parent(target)
8989+ }
9090+9191+ /// Returns whether the tree contains `target` (including deleted nodes).
9292+ pub fn contains(&self, target: TreeID) -> bool {
9393+ self.tree.contains(target)
9494+ }
9595+9696+ /// Marks a node as deleted.
9797+ pub fn delete(&self, target: TreeID) {
9898+ self.tree.delete(target).expect("delete on attached tree");
9999+ }
100100+101101+ /// Moves `target` to be a child of `parent`.
102102+ pub fn mov(&self, target: TreeID, parent: TreeID) {
103103+ self.tree.mov(target, parent).expect("mov on attached tree");
104104+ }
105105+106106+ /// Moves `target` to be a root node.
107107+ pub fn mov_to_root(&self, target: TreeID) {
108108+ self.tree
109109+ .mov(target, TreeParentId::Root)
110110+ .expect("mov to root on attached tree");
111111+ }
112112+113113+ /// Moves `target` to position `index` under `parent`.
114114+ pub fn mov_to(&self, target: TreeID, parent: TreeID, index: usize) {
115115+ self.tree
116116+ .mov_to(target, parent, index)
117117+ .expect("mov_to on attached tree");
118118+ }
119119+120120+ /// Moves `target` to the position immediately after `after`.
121121+ pub fn mov_after(&self, target: TreeID, after: TreeID) {
122122+ self.tree
123123+ .mov_after(target, after)
124124+ .expect("mov_after on attached tree");
125125+ }
126126+127127+ /// Moves `target` to the position immediately before `before`.
128128+ pub fn mov_before(&self, target: TreeID, before: TreeID) {
129129+ self.tree
130130+ .mov_before(target, before)
131131+ .expect("mov_before on attached tree");
132132+ }
133133+}
134134+135135+// --- Typed metadata access (requires FromLoroMap) ----------------------------
136136+137137+impl<T: FromLoroMap> Tree<T> {
138138+ /// Returns the typed metadata for `id`, or `None` if the node doesn't
139139+ /// exist or has been deleted.
140140+ pub fn get(&self, id: TreeID) -> Option<T> {
141141+ // Only return data for nodes that are live (have a non-Deleted parent).
142142+ let parent = self.tree.parent(id)?;
143143+ if parent == TreeParentId::Deleted {
144144+ return None;
145145+ }
146146+ self.tree.get_meta(id).ok().map(T::from_loro_map)
147147+ }
148148+149149+ /// Creates a new root node and returns its ID together with the typed
150150+ /// metadata view.
151151+ pub fn create_root(&self) -> (TreeID, T) {
152152+ let id = self
153153+ .tree
154154+ .create(TreeParentId::Root)
155155+ .expect("create root on attached tree");
156156+ let meta = self
157157+ .tree
158158+ .get_meta(id)
159159+ .expect("get_meta on just-created node");
160160+ (id, T::from_loro_map(meta))
161161+ }
162162+163163+ /// Creates a new child node under `parent` and returns its ID together
164164+ /// with the typed metadata view.
165165+ pub fn create_child(&self, parent: TreeID) -> (TreeID, T) {
166166+ let id = self
167167+ .tree
168168+ .create(parent)
169169+ .expect("create child on attached tree");
170170+ let meta = self
171171+ .tree
172172+ .get_meta(id)
173173+ .expect("get_meta on just-created node");
174174+ (id, T::from_loro_map(meta))
175175+ }
176176+177177+ /// Creates a new child node at position `index` under `parent` and
178178+ /// returns its ID together with the typed metadata view.
179179+ ///
180180+ /// Requires fractional indexing to be enabled (it is by default).
181181+ pub fn create_at(&self, parent: TreeID, index: usize) -> (TreeID, T) {
182182+ let id = self
183183+ .tree
184184+ .create_at(parent, index)
185185+ .expect("create_at on attached tree");
186186+ let meta = self
187187+ .tree
188188+ .get_meta(id)
189189+ .expect("get_meta on just-created node");
190190+ (id, T::from_loro_map(meta))
191191+ }
192192+}
+420
loroscope/tests/integration.rs
···11+use loroscope::loroscope;
22+33+#[loroscope]
44+struct NpcStats {
55+ hp: i64,
66+ attack: i64,
77+ defense: i64,
88+}
99+1010+#[loroscope]
1111+struct Npc {
1212+ name: Text,
1313+ x: f64,
1414+ y: f64,
1515+ hp: i64,
1616+ visible: bool,
1717+ stats: NpcStats,
1818+}
1919+2020+#[loroscope]
2121+struct Project {
2222+ npcs: List<Npc>,
2323+ name: Text,
2424+ settings: Map<i64>,
2525+}
2626+2727+// ---- Test 1: Primitive fields ----
2828+2929+#[test]
3030+fn primitive_fields() {
3131+ let project = Project::new();
3232+3333+ let npc = project.npcs().push_new();
3434+ npc.set_x(42.0);
3535+ npc.set_y(100.0);
3636+ npc.set_hp(20);
3737+ npc.set_visible(true);
3838+3939+ assert_eq!(npc.x(), 42.0);
4040+ assert_eq!(npc.y(), 100.0);
4141+ assert_eq!(npc.hp(), 20);
4242+ assert!(npc.visible());
4343+}
4444+4545+// ---- Test 2: Nested structs ----
4646+4747+#[test]
4848+fn nested_structs() {
4949+ let project = Project::new();
5050+5151+ let npc = project.npcs().push_new();
5252+ let stats = npc.stats();
5353+ stats.set_hp(100);
5454+ stats.set_attack(50);
5555+ stats.set_defense(30);
5656+5757+ assert_eq!(stats.hp(), 100);
5858+ assert_eq!(stats.attack(), 50);
5959+ assert_eq!(stats.defense(), 30);
6060+}
6161+6262+// ---- Test 3: Default values (unwrap_or_default) ----
6363+6464+#[test]
6565+fn default_values() {
6666+ let project = Project::new();
6767+6868+ let npc = project.npcs().push_new();
6969+ assert_eq!(npc.x(), 0.0);
7070+ assert_eq!(npc.y(), 0.0);
7171+ assert_eq!(npc.hp(), 0);
7272+ assert!(!npc.visible());
7373+}
7474+7575+// ---- Test 4: Shared views ----
7676+7777+#[test]
7878+fn shared_views() {
7979+ let project1 = Project::new();
8080+ let doc = project1.doc();
8181+ let project2 = Project::from(doc.get_map("Project"));
8282+8383+ let npc = project1.npcs().push_new();
8484+ npc.set_hp(42);
8585+ doc.commit();
8686+8787+ let npc2 = project2.npcs().get(0).unwrap();
8888+ assert_eq!(npc2.hp(), 42);
8989+}
9090+9191+// ---- Test 5: Undo manager ----
9292+9393+#[test]
9494+fn undo_manager() {
9595+ let project = Project::new();
9696+ let doc = project.doc();
9797+9898+ let npc = project.npcs().push_new();
9999+ npc.set_hp(10);
100100+ doc.commit();
101101+102102+ let mut undo = loro::UndoManager::new(&doc);
103103+104104+ npc.set_hp(99);
105105+ doc.commit();
106106+ assert_eq!(npc.hp(), 99);
107107+108108+ undo.undo().unwrap();
109109+ assert_eq!(npc.hp(), 10);
110110+}
111111+112112+// ---- Test 6: List operations ----
113113+114114+#[test]
115115+fn list_operations() {
116116+ let project = Project::new();
117117+118118+ let npc1 = project.npcs().push_new();
119119+ npc1.set_hp(10);
120120+ let npc2 = project.npcs().push_new();
121121+ npc2.set_hp(20);
122122+123123+ assert_eq!(project.npcs().len(), 2);
124124+ assert!(!project.npcs().is_empty());
125125+ assert_eq!(project.npcs().get(0).unwrap().hp(), 10);
126126+ assert_eq!(project.npcs().get(1).unwrap().hp(), 20);
127127+128128+ let hps: Vec<i64> = project.npcs().iter().map(|n: Npc| n.hp()).collect();
129129+ assert_eq!(hps, vec![10, 20]);
130130+}
131131+132132+// ---- Test 7: Map operations ----
133133+134134+#[test]
135135+fn map_operations() {
136136+ let project = Project::new();
137137+138138+ project.settings().insert("volume", 80);
139139+ project.settings().insert("brightness", 50);
140140+141141+ assert_eq!(project.settings().get("volume"), Some(80));
142142+ assert_eq!(project.settings().get("brightness"), Some(50));
143143+ assert_eq!(project.settings().get("nonexistent"), None);
144144+ assert_eq!(project.settings().len(), 2);
145145+}
146146+147147+// ---- Test 8: Raw Loro types ----
148148+149149+#[loroscope]
150150+struct RawExample {
151151+ items: LoroList,
152152+ metadata: LoroMap,
153153+ notes: LoroText,
154154+}
155155+156156+#[test]
157157+fn raw_loro_types() {
158158+ let example = RawExample::new();
159159+160160+ // LoroList — user handles types themselves
161161+ example.items().push(42).unwrap();
162162+ example.items().push("hello").unwrap();
163163+ assert_eq!(example.items().len(), 2);
164164+165165+ // LoroMap — untyped key-value pairs
166166+ example.metadata().insert("key", "value").unwrap();
167167+ assert_eq!(example.metadata().len(), 1);
168168+169169+ // LoroText
170170+ example.notes().insert(0, "hello world").unwrap();
171171+ assert_eq!(example.notes().to_string(), "hello world");
172172+}
173173+174174+// ---- Test 9: raw() and doc() on generated structs ----
175175+176176+#[test]
177177+fn raw_and_doc_accessor() {
178178+ let project = Project::new();
179179+180180+ // All structs expose &LoroMap via raw()
181181+ let _raw_map: &loro::LoroMap = project.raw();
182182+183183+ // doc() returns the owning LoroDoc
184184+ let _doc: loro::LoroDoc = project.doc();
185185+186186+ // Nested struct also has raw() and doc()
187187+ let npc = project.npcs().push_new();
188188+ let _raw_map: &loro::LoroMap = npc.raw();
189189+ let _doc: loro::LoroDoc = npc.doc();
190190+191191+ // Can use raw() to do things the typed API doesn't cover
192192+ npc.raw().insert("custom_field", 999).unwrap();
193193+}
194194+195195+// ---- Test 10: derive pass-through and custom Debug ----
196196+197197+#[loroscope]
198198+#[derive(Debug, Clone)]
199199+struct SimplePoint {
200200+ x: f64,
201201+ y: f64,
202202+}
203203+204204+#[test]
205205+fn derive_debug() {
206206+ let point = SimplePoint::new();
207207+ point.set_x(1.5);
208208+ point.set_y(2.5);
209209+210210+ let debug_str = format!("{:?}", point);
211211+ assert!(debug_str.contains("SimplePoint"));
212212+ assert!(debug_str.contains("x: 1.5"));
213213+ assert!(debug_str.contains("y: 2.5"));
214214+}
215215+216216+#[test]
217217+fn derive_clone() {
218218+ let point = SimplePoint::new();
219219+ let point2 = point.clone();
220220+ point.set_x(42.0);
221221+ // Clone shares the same underlying CRDT, so both see the change
222222+ assert_eq!(point2.x(), 42.0);
223223+}
224224+225225+#[test]
226226+fn sync_export_import() {
227227+ use loro::ExportMode;
228228+229229+ // Alice creates a document and adds an NPC.
230230+ let alice = Project::new();
231231+ let alice_doc = alice.doc();
232232+ let npc = alice.npcs().push_new();
233233+ npc.set_hp(10);
234234+ alice_doc.commit();
235235+236236+ // Bob starts from Alice's document.
237237+ let bob = Project::new();
238238+ let bob_doc = bob.doc();
239239+ bob_doc
240240+ .import(&alice_doc.export(ExportMode::all_updates()).unwrap())
241241+ .unwrap();
242242+243243+ // Both edit concurrently.
244244+ let npc = alice.npcs().push_new();
245245+ npc.set_hp(20);
246246+ alice_doc.commit();
247247+248248+ let npc = bob.npcs().push_new();
249249+ npc.set_hp(30);
250250+ bob_doc.commit();
251251+252252+ // Sync — all 3 NPCs appear in both documents.
253253+ bob_doc
254254+ .import(&alice_doc.export(ExportMode::all_updates()).unwrap())
255255+ .unwrap();
256256+ alice_doc
257257+ .import(&bob_doc.export(ExportMode::all_updates()).unwrap())
258258+ .unwrap();
259259+260260+ assert_eq!(alice.npcs().len(), 3);
261261+ assert_eq!(bob.npcs().len(), 3);
262262+}
263263+264264+// ---- Tree tests ----
265265+266266+#[loroscope]
267267+struct FileNode {
268268+ name: Text,
269269+ size: i64,
270270+}
271271+272272+#[loroscope]
273273+struct FileSystem {
274274+ files: Tree<FileNode>,
275275+}
276276+277277+#[test]
278278+fn tree_create_and_get() {
279279+ let fs = FileSystem::new();
280280+ let tree = fs.files();
281281+282282+ let (root_id, root) = tree.create_root();
283283+ root.name().insert(0, "root").unwrap();
284284+ root.set_size(100);
285285+286286+ let retrieved = tree.get(root_id).unwrap();
287287+ assert_eq!(retrieved.name().to_string(), "root");
288288+ assert_eq!(retrieved.size(), 100);
289289+}
290290+291291+#[test]
292292+fn tree_structure_queries() {
293293+ let fs = FileSystem::new();
294294+ let tree = fs.files();
295295+296296+ let (root_id, root) = tree.create_root();
297297+ root.name().insert(0, "root").unwrap();
298298+299299+ let (child1_id, child1) = tree.create_child(root_id);
300300+ child1.name().insert(0, "child1").unwrap();
301301+302302+ let (child2_id, child2) = tree.create_child(root_id);
303303+ child2.name().insert(0, "child2").unwrap();
304304+305305+ // roots
306306+ assert_eq!(tree.roots(), vec![root_id]);
307307+ assert!(!tree.is_empty());
308308+309309+ // children
310310+ let children = tree.children(root_id).unwrap();
311311+ assert_eq!(children.len(), 2);
312312+ assert!(children.contains(&child1_id));
313313+ assert!(children.contains(&child2_id));
314314+315315+ assert_eq!(tree.children_num(root_id), Some(2));
316316+317317+ // parent
318318+ assert_eq!(
319319+ tree.parent(child1_id),
320320+ Some(loroscope::TreeParentId::Node(root_id))
321321+ );
322322+ assert_eq!(tree.parent(root_id), Some(loroscope::TreeParentId::Root));
323323+324324+ // contains
325325+ assert!(tree.contains(root_id));
326326+ assert!(tree.contains(child1_id));
327327+}
328328+329329+#[test]
330330+fn tree_move_operations() {
331331+ let fs = FileSystem::new();
332332+ let tree = fs.files();
333333+334334+ let (root1_id, _) = tree.create_root();
335335+ let (root2_id, _) = tree.create_root();
336336+ let (child_id, _) = tree.create_child(root1_id);
337337+338338+ // Move child from root1 to root2
339339+ tree.mov(child_id, root2_id);
340340+ assert_eq!(
341341+ tree.parent(child_id),
342342+ Some(loroscope::TreeParentId::Node(root2_id))
343343+ );
344344+ assert_eq!(tree.children_num(root1_id), Some(0));
345345+ assert_eq!(tree.children_num(root2_id), Some(1));
346346+347347+ // Move child to root
348348+ tree.mov_to_root(child_id);
349349+ assert_eq!(tree.parent(child_id), Some(loroscope::TreeParentId::Root));
350350+ assert_eq!(tree.roots().len(), 3);
351351+}
352352+353353+#[test]
354354+fn tree_positional_operations() {
355355+ let fs = FileSystem::new();
356356+ let tree = fs.files();
357357+358358+ let (root_id, _) = tree.create_root();
359359+ let (a_id, a) = tree.create_child(root_id);
360360+ a.name().insert(0, "a").unwrap();
361361+ let (b_id, b) = tree.create_child(root_id);
362362+ b.name().insert(0, "b").unwrap();
363363+364364+ // create_at inserts at position 0 (before both)
365365+ let (c_id, c) = tree.create_at(root_id, 0);
366366+ c.name().insert(0, "c").unwrap();
367367+368368+ let children = tree.children(root_id).unwrap();
369369+ assert_eq!(children[0], c_id);
370370+371371+ // mov_to: move b to position 0
372372+ tree.mov_to(b_id, root_id, 0);
373373+ let children = tree.children(root_id).unwrap();
374374+ assert_eq!(children[0], b_id);
375375+376376+ // mov_after: move a after c
377377+ tree.mov_after(a_id, c_id);
378378+ let children = tree.children(root_id).unwrap();
379379+ let c_pos = children.iter().position(|id| *id == c_id).unwrap();
380380+ let a_pos = children.iter().position(|id| *id == a_id).unwrap();
381381+ assert_eq!(a_pos, c_pos + 1);
382382+383383+ // mov_before: move b before c
384384+ tree.mov_before(b_id, c_id);
385385+ let children = tree.children(root_id).unwrap();
386386+ let b_pos = children.iter().position(|id| *id == b_id).unwrap();
387387+ let c_pos = children.iter().position(|id| *id == c_id).unwrap();
388388+ assert_eq!(b_pos + 1, c_pos);
389389+}
390390+391391+#[test]
392392+fn tree_delete() {
393393+ let fs = FileSystem::new();
394394+ let tree = fs.files();
395395+396396+ let (root_id, _) = tree.create_root();
397397+ let (child_id, _) = tree.create_child(root_id);
398398+399399+ tree.delete(child_id);
400400+401401+ // get returns None for deleted nodes
402402+ assert!(tree.get(child_id).is_none());
403403+404404+ // children excludes deleted nodes
405405+ assert_eq!(tree.children_num(root_id), Some(0));
406406+}
407407+408408+#[test]
409409+fn tree_get_nonexistent() {
410410+ let fs = FileSystem::new();
411411+ let tree = fs.files();
412412+413413+ // Fabricate a TreeID that doesn't exist in this tree
414414+ let fake_id = loroscope::TreeID {
415415+ peer: 999,
416416+ counter: 999,
417417+ };
418418+ assert!(tree.get(fake_id).is_none());
419419+ assert!(!tree.contains(fake_id));
420420+}