// SPDX-FileCopyrightText: 2026 Alex Bates // // SPDX-License-Identifier: AGPL-3.0-or-later #![allow( clippy::missing_panics_doc, clippy::unwrap_used, reason = "construction asserts containers are attached" )] use std::fmt; use std::marker::PhantomData; use loro::{LoroList, LoroMap, LoroMovableList, LoroTree, LoroValue}; use crate::FromLoroMap; /// A typed string-keyed map where every value is of type `V`. /// /// Use this as a field type in a `#[loroscope]` struct: /// /// ``` /// use loroscope::loroscope; /// /// #[loroscope] /// struct Settings { /// pub values: Map, /// } /// /// let settings = Settings::new(); /// settings.values().insert("volume", 80); /// assert_eq!(settings.values().get("volume"), Some(80)); /// ``` /// /// `V` can be a `#[loroscope]` struct, a primitive (`f64`, `i64`, `bool`, /// `String`), a Loro container type (`LoroText`, `LoroCounter`, `LoroTree`), /// or another collection ([`List`](crate::List), /// [`MovableList`](crate::MovableList), `Map`). pub struct Map { map: LoroMap, _phantom: PhantomData, } impl Clone for Map { fn clone(&self) -> Self { Self { map: self.map.clone(), _phantom: PhantomData, } } } impl fmt::Debug for Map { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Map").field("len", &self.len()).finish() } } impl Map { /// Creates a typed view from a [`LoroMap`]. /// /// # Panics /// /// Panics if `map` is not attached to a [`LoroDoc`](loro::LoroDoc). #[must_use] pub fn new(map: LoroMap) -> Self { assert!( map.is_attached(), "Map requires an attached LoroMap (one that belongs to a LoroDoc)" ); Self { map, _phantom: PhantomData, } } /// Returns the number of entries. #[must_use] pub fn len(&self) -> usize { self.map.len() } /// Returns `true` if the map is empty. #[must_use] pub fn is_empty(&self) -> bool { self.map.is_empty() } /// Returns a reference to the underlying [`LoroMap`]. #[must_use] pub fn raw(&self) -> &LoroMap { &self.map } /// Deletes the entry for `key`. pub fn delete(&self, key: &str) { self.map.delete(key).unwrap(); } /// Removes all entries from the map. pub fn clear(&self) { self.map.clear().unwrap(); } /// Returns an iterator over the keys in the map. pub fn keys(&self) -> impl Iterator + '_ { self.map.keys().map(|s| s.to_string()) } } impl Map { /// Returns the value for `key`, or `None` if the key is missing. pub fn get(&self, key: &str) -> Option { self.map .get(key) .and_then(|v| v.into_container().ok()) .and_then(|c| c.into_map().ok()) .map(V::from_loro_map) } /// Returns the value for `key`, creating it if it doesn't exist. pub fn get_or_create(&self, key: &str) -> V { V::from_loro_map( self.map .get_or_create_container(key, LoroMap::new()) .unwrap(), ) } /// Returns an iterator over all key-value pairs. pub fn iter(&self) -> impl Iterator { let mut items = Vec::with_capacity(self.len()); self.map.for_each(|key, voc| { if let Some(map) = voc.into_container().ok().and_then(|c| c.into_map().ok()) { items.push((key.to_owned(), V::from_loro_map(map))); } }); items.into_iter() } } macro_rules! impl_primitive { ($ty:ty, $variant:pat => $expr:expr) => { impl Map<$ty> { /// Returns the value for `key`, or `None` if missing. pub fn get(&self, key: &str) -> Option<$ty> { self.map .get(key) .and_then(|v| v.into_value().ok()) .and_then(|v| match v { $variant => Some($expr), _ => None, }) } /// Returns an iterator over all key-value pairs. pub fn iter(&self) -> impl Iterator { let mut items = Vec::with_capacity(self.len()); self.map.for_each(|key, voc| { if let Some(v) = voc.into_value().ok().and_then(|v| match v { $variant => Some($expr), _ => None, }) { items.push((key.to_owned(), v)); } }); items.into_iter() } } }; } impl_primitive!(f64, LoroValue::Double(d) => d); impl_primitive!(i64, LoroValue::I64(i) => i); impl_primitive!(bool, LoroValue::Bool(b) => b); impl_primitive!(String, LoroValue::String(s) => s.to_string()); macro_rules! impl_insert { ($($value_ty:ty => $param_ty:ty),* $(,)?) => {$( impl Map<$value_ty> { /// Inserts a value for the given key, overwriting any previous value. pub fn insert(&self, key: &str, val: $param_ty) { self.map.insert(key, val).unwrap(); } } )*}; } impl_insert!(f64 => f64, i64 => i64, bool => bool, String => &str); macro_rules! impl_container { ($ty:ty, $variant:ident) => { impl Map<$ty> { /// Returns the container for `key`, or `None` if missing. pub fn get(&self, key: &str) -> Option<$ty> { self.map .get(key) .and_then(|v| v.into_container().ok()) .and_then(|c| c.$variant().ok()) } /// Returns the container for `key`, creating it if it doesn't exist. pub fn get_or_create(&self, key: &str) -> $ty { self.map.get_or_create_container(key, <$ty>::new()).unwrap() } } }; } impl_container!(loro::LoroText, into_text); impl_container!(loro::LoroCounter, into_counter); impl_container!(loro::LoroTree, into_tree); macro_rules! impl_wrapper { ($wrapper:ident < $($param:ident),+ >, $loro_ty:ty, $variant:ident) => { impl<$($param),+> Map> { /// Returns the value for `key`, or `None` if missing. pub fn get(&self, key: &str) -> Option> { self.map .get(key) .and_then(|v| v.into_container().ok()) .and_then(|c| c.$variant().ok()) .map(crate::$wrapper::new) } /// Returns the value for `key`, creating it if it doesn't exist. pub fn get_or_create(&self, key: &str) -> crate::$wrapper<$($param),+> { crate::$wrapper::new( self.map .get_or_create_container(key, <$loro_ty>::new()) .unwrap(), ) } } }; } impl_wrapper!(List, LoroList, into_list); impl_wrapper!(MovableList, LoroMovableList, into_movable_list); impl_wrapper!(Map, LoroMap, into_map); // Tree needs a manual impl because Tree requires T: FromLoroMap. impl Map> { /// Returns the value for `key`, or `None` if missing. pub fn get(&self, key: &str) -> Option> { self.map .get(key) .and_then(|v| v.into_container().ok()) .and_then(|c| c.into_tree().ok()) .map(crate::Tree::new) } /// Returns the value for `key`, creating it if it doesn't exist. pub fn get_or_create(&self, key: &str) -> crate::Tree { crate::Tree::new( self.map .get_or_create_container(key, LoroTree::new()) .unwrap(), ) } }