Skip to content

Commit d552955

Browse files
authored
feat: make get_by_path work for tree (#594)
1 parent 5a85e6e commit d552955

File tree

5 files changed

+283
-36
lines changed

5 files changed

+283
-36
lines changed

.changeset/old-snails-sell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"loro-crdt": patch
3+
---
4+
5+
Make getByPath work for "tree/0/key"

crates/loro-internal/src/state.rs

Lines changed: 109 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use enum_as_inner::EnumAsInner;
1313
use enum_dispatch::enum_dispatch;
1414
use fxhash::{FxHashMap, FxHashSet};
1515
use itertools::Itertools;
16-
use loro_common::{ContainerID, LoroError, LoroResult};
16+
use loro_common::{ContainerID, LoroError, LoroResult, TreeID};
1717
use loro_delta::DeltaItem;
1818
use tracing::{info_span, instrument, warn};
1919

@@ -1528,51 +1528,125 @@ impl DocState {
15281528
return None;
15291529
}
15301530

1531+
enum CurContainer {
1532+
Container(ContainerIdx),
1533+
TreeNode {
1534+
tree: ContainerIdx,
1535+
node: Option<TreeID>,
1536+
},
1537+
}
1538+
15311539
let mut state_idx = {
15321540
let root_index = path[0].as_key()?;
1533-
self.arena.get_root_container_idx_by_key(root_index)?
1541+
CurContainer::Container(self.arena.get_root_container_idx_by_key(root_index)?)
15341542
};
15351543

15361544
if path.len() == 1 {
1537-
let cid = self.arena.idx_to_id(state_idx)?;
1538-
return Some(LoroValue::Container(cid));
1545+
if let CurContainer::Container(c) = state_idx {
1546+
let cid = self.arena.idx_to_id(c)?;
1547+
return Some(LoroValue::Container(cid));
1548+
}
15391549
}
15401550

1541-
for index in path[..path.len() - 1].iter().skip(1) {
1542-
let parent_state = self.store.get_container_mut(state_idx)?;
1543-
match parent_state {
1544-
State::ListState(l) => {
1545-
let Some(LoroValue::Container(c)) = l.get(*index.as_seq()?) else {
1546-
return None;
1547-
};
1548-
state_idx = self.arena.register_container(c);
1549-
}
1550-
State::MovableListState(l) => {
1551-
let Some(LoroValue::Container(c)) = l.get(*index.as_seq()?, IndexType::ForUser)
1552-
else {
1553-
return None;
1554-
};
1555-
state_idx = self.arena.register_container(c);
1556-
}
1557-
State::MapState(m) => {
1558-
let Some(LoroValue::Container(c)) = m.get(index.as_key()?) else {
1559-
return None;
1560-
};
1561-
state_idx = self.arena.register_container(c);
1562-
}
1563-
State::RichtextState(_) => return None,
1564-
State::TreeState(_) => {
1565-
let id = index.as_node()?;
1566-
let cid = id.associated_meta_container();
1567-
state_idx = self.arena.register_container(&cid);
1551+
let mut i = 1;
1552+
while i < path.len() - 1 {
1553+
let index = &path[i];
1554+
match state_idx {
1555+
CurContainer::Container(idx) => {
1556+
let parent_state = self.store.get_container_mut(idx)?;
1557+
match parent_state {
1558+
State::ListState(l) => {
1559+
let Some(LoroValue::Container(c)) = l.get(*index.as_seq()?) else {
1560+
return None;
1561+
};
1562+
state_idx = CurContainer::Container(self.arena.register_container(c));
1563+
}
1564+
State::MovableListState(l) => {
1565+
let Some(LoroValue::Container(c)) =
1566+
l.get(*index.as_seq()?, IndexType::ForUser)
1567+
else {
1568+
return None;
1569+
};
1570+
state_idx = CurContainer::Container(self.arena.register_container(c));
1571+
}
1572+
State::MapState(m) => {
1573+
let Some(LoroValue::Container(c)) = m.get(index.as_key()?) else {
1574+
return None;
1575+
};
1576+
state_idx = CurContainer::Container(self.arena.register_container(c));
1577+
}
1578+
State::RichtextState(_) => return None,
1579+
State::TreeState(_) => {
1580+
state_idx = CurContainer::TreeNode {
1581+
tree: idx,
1582+
node: None,
1583+
};
1584+
continue;
1585+
}
1586+
#[cfg(feature = "counter")]
1587+
State::CounterState(_) => return None,
1588+
State::UnknownState(_) => unreachable!(),
1589+
}
15681590
}
1569-
#[cfg(feature = "counter")]
1570-
State::CounterState(_) => return None,
1571-
State::UnknownState(_) => unreachable!(),
1591+
CurContainer::TreeNode { tree, node } => match index {
1592+
Index::Key(internal_string) => {
1593+
let node = node?;
1594+
let idx = self
1595+
.arena
1596+
.register_container(&node.associated_meta_container());
1597+
let map = self.store.get_container(idx)?;
1598+
let Some(LoroValue::Container(c)) =
1599+
map.as_map_state().unwrap().get(internal_string)
1600+
else {
1601+
return None;
1602+
};
1603+
1604+
state_idx = CurContainer::Container(self.arena.register_container(c));
1605+
}
1606+
Index::Seq(i) => {
1607+
let tree_state =
1608+
self.store.get_container_mut(tree)?.as_tree_state().unwrap();
1609+
let parent: TreeParentId = if let Some(node) = node {
1610+
node.into()
1611+
} else {
1612+
TreeParentId::Root
1613+
};
1614+
let child = tree_state.get_children(&parent)?.nth(*i)?;
1615+
state_idx = CurContainer::TreeNode {
1616+
tree,
1617+
node: Some(child),
1618+
};
1619+
}
1620+
Index::Node(tree_id) => {
1621+
let tree_state =
1622+
self.store.get_container_mut(tree)?.as_tree_state().unwrap();
1623+
if tree_state.parent(tree_id).is_some() {
1624+
state_idx = CurContainer::TreeNode {
1625+
tree,
1626+
node: Some(*tree_id),
1627+
}
1628+
} else {
1629+
return None;
1630+
}
1631+
}
1632+
},
15721633
}
1634+
i += 1;
15731635
}
15741636

1575-
let parent_state = self.store.get_container_mut(state_idx)?;
1637+
let parent_idx = match state_idx {
1638+
CurContainer::Container(container_idx) => container_idx,
1639+
CurContainer::TreeNode { tree, node } => {
1640+
if let Some(node) = node {
1641+
self.arena
1642+
.register_container(&node.associated_meta_container())
1643+
} else {
1644+
tree
1645+
}
1646+
}
1647+
};
1648+
1649+
let parent_state = self.store.get_container_mut(parent_idx)?;
15761650
let index = path.last().unwrap();
15771651
let value: LoroValue = match parent_state {
15781652
State::ListState(l) => l.get(*index.as_seq()?).cloned()?,

crates/loro-wasm/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,21 @@ impl LoroDoc {
16631663

16641664
/// Get the value or container at the given path
16651665
///
1666+
/// The path can be specified in different ways depending on the container type:
1667+
///
1668+
/// For Tree:
1669+
/// 1. Using node IDs: `tree/{node_id}/property`
1670+
/// 2. Using indices: `tree/0/1/property`
1671+
///
1672+
/// For List and MovableList:
1673+
/// - Using indices: `list/0` or `list/1/property`
1674+
///
1675+
/// For Map:
1676+
/// - Using keys: `map/key` or `map/nested/property`
1677+
///
1678+
/// For tree structures, index-based paths follow depth-first traversal order.
1679+
/// The indices start from 0 and represent the position of a node among its siblings.
1680+
///
16661681
/// @example
16671682
/// ```ts
16681683
/// import { LoroDoc } from "loro-crdt";

crates/loro/src/lib.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,58 @@ impl LoroDoc {
697697
}
698698

699699
/// Get the handler by the string path.
700+
///
701+
/// The path can be specified in different ways depending on the container type:
702+
///
703+
/// For Tree:
704+
/// 1. Using node IDs: `tree/{node_id}/property`
705+
/// 2. Using indices: `tree/0/1/property`
706+
///
707+
/// For List and MovableList:
708+
/// - Using indices: `list/0` or `list/1/property`
709+
///
710+
/// For Map:
711+
/// - Using keys: `map/key` or `map/nested/property`
712+
///
713+
/// For tree structures, index-based paths follow depth-first traversal order.
714+
/// The indices start from 0 and represent the position of a node among its siblings.
715+
///
716+
/// # Examples
717+
/// ```
718+
/// # use loro::{LoroDoc, LoroValue};
719+
/// let doc = LoroDoc::new();
720+
///
721+
/// // Tree example
722+
/// let tree = doc.get_tree("tree");
723+
/// let root = tree.create(None).unwrap();
724+
/// tree.get_meta(root).unwrap().insert("name", "root").unwrap();
725+
/// // Access tree by ID or index
726+
/// let name1 = doc.get_by_str_path(&format!("tree/{}/name", root)).unwrap().into_value().unwrap();
727+
/// let name2 = doc.get_by_str_path("tree/0/name").unwrap().into_value().unwrap();
728+
/// assert_eq!(name1, name2);
729+
///
730+
/// // List example
731+
/// let list = doc.get_list("list");
732+
/// list.insert(0, "first").unwrap();
733+
/// list.insert(1, "second").unwrap();
734+
/// // Access list by index
735+
/// let item = doc.get_by_str_path("list/0");
736+
/// assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "first".into());
737+
///
738+
/// // Map example
739+
/// let map = doc.get_map("map");
740+
/// map.insert("key", "value").unwrap();
741+
/// // Access map by key
742+
/// let value = doc.get_by_str_path("map/key");
743+
/// assert_eq!(value.unwrap().into_value().unwrap().into_string().unwrap(), "value".into());
744+
///
745+
/// // MovableList example
746+
/// let mlist = doc.get_movable_list("mlist");
747+
/// mlist.insert(0, "item").unwrap();
748+
/// // Access movable list by index
749+
/// let item = doc.get_by_str_path("mlist/0");
750+
/// assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "item".into());
751+
/// ```
700752
#[inline]
701753
pub fn get_by_str_path(&self, path: &str) -> Option<ValueOrContainer> {
702754
self.doc.get_by_str_path(path).map(ValueOrContainer::from)

crates/loro/tests/loro_rust_test.rs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::{
1212
use loro::{
1313
awareness::Awareness, loro_value, CommitOptions, ContainerID, ContainerTrait, ContainerType,
1414
ExportMode, Frontiers, FrontiersNotIncluded, IdSpan, LoroDoc, LoroError, LoroList, LoroMap,
15-
LoroText, LoroValue, ToJson,
15+
LoroStringValue, LoroText, LoroValue, ToJson,
1616
};
1717
use loro_internal::{
1818
encoding::EncodedBlobMode, handler::TextDelta, id::ID, version_range, vv, LoroResult,
@@ -2349,3 +2349,104 @@ fn test_detach_and_attach() {
23492349
doc.attach();
23502350
assert!(!doc.is_detached());
23512351
}
2352+
2353+
#[test]
2354+
fn test_rust_get_value_by_path() {
2355+
let doc = LoroDoc::new();
2356+
let tree = doc.get_tree("tree");
2357+
let root = tree.create(None).unwrap();
2358+
let child1 = tree.create(root).unwrap();
2359+
let child2 = tree.create(root).unwrap();
2360+
let grandchild = tree.create(child1).unwrap();
2361+
2362+
// Set up metadata for nodes
2363+
tree.get_meta(root).unwrap().insert("name", "root").unwrap();
2364+
tree.get_meta(child1)
2365+
.unwrap()
2366+
.insert("name", "child1")
2367+
.unwrap();
2368+
tree.get_meta(child2)
2369+
.unwrap()
2370+
.insert("name", "child2")
2371+
.unwrap();
2372+
tree.get_meta(grandchild)
2373+
.unwrap()
2374+
.insert("name", "grandchild")
2375+
.unwrap();
2376+
2377+
// Test getting values by path
2378+
let root_meta = doc.get_by_str_path(&format!("tree/{}", root)).unwrap();
2379+
let root_name = doc.get_by_str_path(&format!("tree/{}/name", root)).unwrap();
2380+
let child1_meta = doc.get_by_str_path(&format!("tree/{}", child1)).unwrap();
2381+
let child1_name = doc
2382+
.get_by_str_path(&format!("tree/{}/name", child1))
2383+
.unwrap();
2384+
let grandchild_name = doc
2385+
.get_by_str_path(&format!("tree/{}/name", grandchild))
2386+
.unwrap();
2387+
2388+
// Verify the values
2389+
assert!(root_meta.into_container().unwrap().is_map());
2390+
assert_eq!(
2391+
root_name.into_value().unwrap().into_string().unwrap(),
2392+
LoroStringValue::from("root")
2393+
);
2394+
assert!(child1_meta.into_container().unwrap().is_map());
2395+
assert_eq!(
2396+
child1_name.into_value().unwrap().into_string().unwrap(),
2397+
LoroStringValue::from("child1")
2398+
);
2399+
assert_eq!(
2400+
grandchild_name.into_value().unwrap().into_string().unwrap(),
2401+
LoroStringValue::from("grandchild")
2402+
);
2403+
2404+
// Test non-existent paths
2405+
assert!(doc.get_by_str_path("tree/nonexistent").is_none());
2406+
assert!(doc
2407+
.get_by_str_path(&format!("tree/{}/nonexistent", root))
2408+
.is_none());
2409+
2410+
// Verify values accessed by index
2411+
assert_eq!(
2412+
doc.get_by_str_path("tree/0/name")
2413+
.unwrap()
2414+
.into_value()
2415+
.unwrap()
2416+
.into_string()
2417+
.unwrap(),
2418+
LoroStringValue::from("root")
2419+
);
2420+
assert_eq!(
2421+
doc.get_by_str_path("tree/0/0/name")
2422+
.unwrap()
2423+
.into_value()
2424+
.unwrap()
2425+
.into_string()
2426+
.unwrap(),
2427+
LoroStringValue::from("child1")
2428+
);
2429+
assert_eq!(
2430+
doc.get_by_str_path("tree/0/1/name")
2431+
.unwrap()
2432+
.into_value()
2433+
.unwrap()
2434+
.into_string()
2435+
.unwrap(),
2436+
LoroStringValue::from("child2")
2437+
);
2438+
assert_eq!(
2439+
doc.get_by_str_path("tree/0/0/0/name")
2440+
.unwrap()
2441+
.into_value()
2442+
.unwrap()
2443+
.into_string()
2444+
.unwrap(),
2445+
LoroStringValue::from("grandchild")
2446+
);
2447+
2448+
// Test invalid index paths
2449+
assert!(doc.get_by_str_path("tree/1").is_none()); // Invalid root index
2450+
assert!(doc.get_by_str_path("tree/0/2").is_none()); // Invalid child index
2451+
assert!(doc.get_by_str_path("tree/0/0/1").is_none()); // Invalid grandchild index
2452+
}

0 commit comments

Comments
 (0)