feat: make get_by_path work for tree (#594)

This commit is contained in:
Zixuan Chen 2024-12-31 13:11:12 +08:00 committed by GitHub
parent 5a85e6e5d2
commit d552955ec6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 286 additions and 39 deletions

View file

@ -0,0 +1,5 @@
---
"loro-crdt": patch
---
Make getByPath work for "tree/0/key"

View file

@ -13,7 +13,7 @@ use enum_as_inner::EnumAsInner;
use enum_dispatch::enum_dispatch;
use fxhash::{FxHashMap, FxHashSet};
use itertools::Itertools;
use loro_common::{ContainerID, LoroError, LoroResult};
use loro_common::{ContainerID, LoroError, LoroResult, TreeID};
use loro_delta::DeltaItem;
use tracing::{info_span, instrument, warn};
@ -1528,51 +1528,125 @@ impl DocState {
return None;
}
enum CurContainer {
Container(ContainerIdx),
TreeNode {
tree: ContainerIdx,
node: Option<TreeID>,
},
}
let mut state_idx = {
let root_index = path[0].as_key()?;
self.arena.get_root_container_idx_by_key(root_index)?
CurContainer::Container(self.arena.get_root_container_idx_by_key(root_index)?)
};
if path.len() == 1 {
let cid = self.arena.idx_to_id(state_idx)?;
return Some(LoroValue::Container(cid));
}
for index in path[..path.len() - 1].iter().skip(1) {
let parent_state = self.store.get_container_mut(state_idx)?;
match parent_state {
State::ListState(l) => {
let Some(LoroValue::Container(c)) = l.get(*index.as_seq()?) else {
return None;
};
state_idx = self.arena.register_container(c);
}
State::MovableListState(l) => {
let Some(LoroValue::Container(c)) = l.get(*index.as_seq()?, IndexType::ForUser)
else {
return None;
};
state_idx = self.arena.register_container(c);
}
State::MapState(m) => {
let Some(LoroValue::Container(c)) = m.get(index.as_key()?) else {
return None;
};
state_idx = self.arena.register_container(c);
}
State::RichtextState(_) => return None,
State::TreeState(_) => {
let id = index.as_node()?;
let cid = id.associated_meta_container();
state_idx = self.arena.register_container(&cid);
}
#[cfg(feature = "counter")]
State::CounterState(_) => return None,
State::UnknownState(_) => unreachable!(),
if let CurContainer::Container(c) = state_idx {
let cid = self.arena.idx_to_id(c)?;
return Some(LoroValue::Container(cid));
}
}
let parent_state = self.store.get_container_mut(state_idx)?;
let mut i = 1;
while i < path.len() - 1 {
let index = &path[i];
match state_idx {
CurContainer::Container(idx) => {
let parent_state = self.store.get_container_mut(idx)?;
match parent_state {
State::ListState(l) => {
let Some(LoroValue::Container(c)) = l.get(*index.as_seq()?) else {
return None;
};
state_idx = CurContainer::Container(self.arena.register_container(c));
}
State::MovableListState(l) => {
let Some(LoroValue::Container(c)) =
l.get(*index.as_seq()?, IndexType::ForUser)
else {
return None;
};
state_idx = CurContainer::Container(self.arena.register_container(c));
}
State::MapState(m) => {
let Some(LoroValue::Container(c)) = m.get(index.as_key()?) else {
return None;
};
state_idx = CurContainer::Container(self.arena.register_container(c));
}
State::RichtextState(_) => return None,
State::TreeState(_) => {
state_idx = CurContainer::TreeNode {
tree: idx,
node: None,
};
continue;
}
#[cfg(feature = "counter")]
State::CounterState(_) => return None,
State::UnknownState(_) => unreachable!(),
}
}
CurContainer::TreeNode { tree, node } => match index {
Index::Key(internal_string) => {
let node = node?;
let idx = self
.arena
.register_container(&node.associated_meta_container());
let map = self.store.get_container(idx)?;
let Some(LoroValue::Container(c)) =
map.as_map_state().unwrap().get(internal_string)
else {
return None;
};
state_idx = CurContainer::Container(self.arena.register_container(c));
}
Index::Seq(i) => {
let tree_state =
self.store.get_container_mut(tree)?.as_tree_state().unwrap();
let parent: TreeParentId = if let Some(node) = node {
node.into()
} else {
TreeParentId::Root
};
let child = tree_state.get_children(&parent)?.nth(*i)?;
state_idx = CurContainer::TreeNode {
tree,
node: Some(child),
};
}
Index::Node(tree_id) => {
let tree_state =
self.store.get_container_mut(tree)?.as_tree_state().unwrap();
if tree_state.parent(tree_id).is_some() {
state_idx = CurContainer::TreeNode {
tree,
node: Some(*tree_id),
}
} else {
return None;
}
}
},
}
i += 1;
}
let parent_idx = match state_idx {
CurContainer::Container(container_idx) => container_idx,
CurContainer::TreeNode { tree, node } => {
if let Some(node) = node {
self.arena
.register_container(&node.associated_meta_container())
} else {
tree
}
}
};
let parent_state = self.store.get_container_mut(parent_idx)?;
let index = path.last().unwrap();
let value: LoroValue = match parent_state {
State::ListState(l) => l.get(*index.as_seq()?).cloned()?,

View file

@ -1663,6 +1663,21 @@ impl LoroDoc {
/// Get the value or container at the given path
///
/// The path can be specified in different ways depending on the container type:
///
/// For Tree:
/// 1. Using node IDs: `tree/{node_id}/property`
/// 2. Using indices: `tree/0/1/property`
///
/// For List and MovableList:
/// - Using indices: `list/0` or `list/1/property`
///
/// For Map:
/// - Using keys: `map/key` or `map/nested/property`
///
/// For tree structures, index-based paths follow depth-first traversal order.
/// The indices start from 0 and represent the position of a node among its siblings.
///
/// @example
/// ```ts
/// import { LoroDoc } from "loro-crdt";

View file

@ -697,6 +697,58 @@ impl LoroDoc {
}
/// Get the handler by the string path.
///
/// The path can be specified in different ways depending on the container type:
///
/// For Tree:
/// 1. Using node IDs: `tree/{node_id}/property`
/// 2. Using indices: `tree/0/1/property`
///
/// For List and MovableList:
/// - Using indices: `list/0` or `list/1/property`
///
/// For Map:
/// - Using keys: `map/key` or `map/nested/property`
///
/// For tree structures, index-based paths follow depth-first traversal order.
/// The indices start from 0 and represent the position of a node among its siblings.
///
/// # Examples
/// ```
/// # use loro::{LoroDoc, LoroValue};
/// let doc = LoroDoc::new();
///
/// // Tree example
/// let tree = doc.get_tree("tree");
/// let root = tree.create(None).unwrap();
/// tree.get_meta(root).unwrap().insert("name", "root").unwrap();
/// // Access tree by ID or index
/// let name1 = doc.get_by_str_path(&format!("tree/{}/name", root)).unwrap().into_value().unwrap();
/// let name2 = doc.get_by_str_path("tree/0/name").unwrap().into_value().unwrap();
/// assert_eq!(name1, name2);
///
/// // List example
/// let list = doc.get_list("list");
/// list.insert(0, "first").unwrap();
/// list.insert(1, "second").unwrap();
/// // Access list by index
/// let item = doc.get_by_str_path("list/0");
/// assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "first".into());
///
/// // Map example
/// let map = doc.get_map("map");
/// map.insert("key", "value").unwrap();
/// // Access map by key
/// let value = doc.get_by_str_path("map/key");
/// assert_eq!(value.unwrap().into_value().unwrap().into_string().unwrap(), "value".into());
///
/// // MovableList example
/// let mlist = doc.get_movable_list("mlist");
/// mlist.insert(0, "item").unwrap();
/// // Access movable list by index
/// let item = doc.get_by_str_path("mlist/0");
/// assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "item".into());
/// ```
#[inline]
pub fn get_by_str_path(&self, path: &str) -> Option<ValueOrContainer> {
self.doc.get_by_str_path(path).map(ValueOrContainer::from)

View file

@ -12,7 +12,7 @@ use std::{
use loro::{
awareness::Awareness, loro_value, CommitOptions, ContainerID, ContainerTrait, ContainerType,
ExportMode, Frontiers, FrontiersNotIncluded, IdSpan, LoroDoc, LoroError, LoroList, LoroMap,
LoroText, LoroValue, ToJson,
LoroStringValue, LoroText, LoroValue, ToJson,
};
use loro_internal::{
encoding::EncodedBlobMode, handler::TextDelta, id::ID, version_range, vv, LoroResult,
@ -2349,3 +2349,104 @@ fn test_detach_and_attach() {
doc.attach();
assert!(!doc.is_detached());
}
#[test]
fn test_rust_get_value_by_path() {
let doc = LoroDoc::new();
let tree = doc.get_tree("tree");
let root = tree.create(None).unwrap();
let child1 = tree.create(root).unwrap();
let child2 = tree.create(root).unwrap();
let grandchild = tree.create(child1).unwrap();
// Set up metadata for nodes
tree.get_meta(root).unwrap().insert("name", "root").unwrap();
tree.get_meta(child1)
.unwrap()
.insert("name", "child1")
.unwrap();
tree.get_meta(child2)
.unwrap()
.insert("name", "child2")
.unwrap();
tree.get_meta(grandchild)
.unwrap()
.insert("name", "grandchild")
.unwrap();
// Test getting values by path
let root_meta = doc.get_by_str_path(&format!("tree/{}", root)).unwrap();
let root_name = doc.get_by_str_path(&format!("tree/{}/name", root)).unwrap();
let child1_meta = doc.get_by_str_path(&format!("tree/{}", child1)).unwrap();
let child1_name = doc
.get_by_str_path(&format!("tree/{}/name", child1))
.unwrap();
let grandchild_name = doc
.get_by_str_path(&format!("tree/{}/name", grandchild))
.unwrap();
// Verify the values
assert!(root_meta.into_container().unwrap().is_map());
assert_eq!(
root_name.into_value().unwrap().into_string().unwrap(),
LoroStringValue::from("root")
);
assert!(child1_meta.into_container().unwrap().is_map());
assert_eq!(
child1_name.into_value().unwrap().into_string().unwrap(),
LoroStringValue::from("child1")
);
assert_eq!(
grandchild_name.into_value().unwrap().into_string().unwrap(),
LoroStringValue::from("grandchild")
);
// Test non-existent paths
assert!(doc.get_by_str_path("tree/nonexistent").is_none());
assert!(doc
.get_by_str_path(&format!("tree/{}/nonexistent", root))
.is_none());
// Verify values accessed by index
assert_eq!(
doc.get_by_str_path("tree/0/name")
.unwrap()
.into_value()
.unwrap()
.into_string()
.unwrap(),
LoroStringValue::from("root")
);
assert_eq!(
doc.get_by_str_path("tree/0/0/name")
.unwrap()
.into_value()
.unwrap()
.into_string()
.unwrap(),
LoroStringValue::from("child1")
);
assert_eq!(
doc.get_by_str_path("tree/0/1/name")
.unwrap()
.into_value()
.unwrap()
.into_string()
.unwrap(),
LoroStringValue::from("child2")
);
assert_eq!(
doc.get_by_str_path("tree/0/0/0/name")
.unwrap()
.into_value()
.unwrap()
.into_string()
.unwrap(),
LoroStringValue::from("grandchild")
);
// Test invalid index paths
assert!(doc.get_by_str_path("tree/1").is_none()); // Invalid root index
assert!(doc.get_by_str_path("tree/0/2").is_none()); // Invalid child index
assert!(doc.get_by_str_path("tree/0/0/1").is_none()); // Invalid grandchild index
}