From d552955ec6c7843cad0908668a89b0d4e52160a7 Mon Sep 17 00:00:00 2001 From: Zixuan Chen Date: Tue, 31 Dec 2024 13:11:12 +0800 Subject: [PATCH] feat: make get_by_path work for tree (#594) --- .changeset/old-snails-sell.md | 5 + crates/loro-internal/src/state.rs | 150 +++++++++++++++++++++------- crates/loro-wasm/src/lib.rs | 15 +++ crates/loro/src/lib.rs | 52 ++++++++++ crates/loro/tests/loro_rust_test.rs | 103 ++++++++++++++++++- 5 files changed, 286 insertions(+), 39 deletions(-) create mode 100644 .changeset/old-snails-sell.md diff --git a/.changeset/old-snails-sell.md b/.changeset/old-snails-sell.md new file mode 100644 index 00000000..c667045a --- /dev/null +++ b/.changeset/old-snails-sell.md @@ -0,0 +1,5 @@ +--- +"loro-crdt": patch +--- + +Make getByPath work for "tree/0/key" diff --git a/crates/loro-internal/src/state.rs b/crates/loro-internal/src/state.rs index f0213271..9ef61f1c 100644 --- a/crates/loro-internal/src/state.rs +++ b/crates/loro-internal/src/state.rs @@ -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, + }, + } + 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()?, diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 2f463895..035888c0 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -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"; diff --git a/crates/loro/src/lib.rs b/crates/loro/src/lib.rs index 61ed4e5c..7dcbbb52 100644 --- a/crates/loro/src/lib.rs +++ b/crates/loro/src/lib.rs @@ -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 { self.doc.get_by_str_path(path).map(ValueOrContainer::from) diff --git a/crates/loro/tests/loro_rust_test.rs b/crates/loro/tests/loro_rust_test.rs index d99df990..193132d5 100644 --- a/crates/loro/tests/loro_rust_test.rs +++ b/crates/loro/tests/loro_rust_test.rs @@ -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 +}