mirror of
https://github.com/loro-dev/loro.git
synced 2025-02-05 12:14:43 +00:00
feat: make get_by_path work for tree (#594)
This commit is contained in:
parent
5a85e6e5d2
commit
d552955ec6
5 changed files with 286 additions and 39 deletions
5
.changeset/old-snails-sell.md
Normal file
5
.changeset/old-snails-sell.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"loro-crdt": patch
|
||||
---
|
||||
|
||||
Make getByPath work for "tree/0/key"
|
|
@ -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()?,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue