use std::{
collections::{HashMap, HashSet},
ops::{Deref, DerefMut},
};
use thiserror::Error;
use uuid::Uuid;
use crate::{
db::{
CustomDataItem, CustomIcon, CustomIconId, CustomIconMut, CustomIconNotFoundError, CustomIconRef, Entry,
EntryId, EntryMut, EntryRef, Icon, Times,
},
Database,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct GroupId(Uuid);
impl GroupId {
pub(crate) fn new() -> Self {
Self(Uuid::new_v4())
}
pub(crate) const fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
pub fn uuid(&self) -> Uuid {
self.0
}
}
impl std::fmt::Display for GroupId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct Group {
pub(crate) id: GroupId,
pub(crate) parent: Option<GroupId>,
pub name: String,
pub notes: Option<String>,
pub(crate) icon: Option<Icon>,
pub(crate) groups: HashSet<GroupId>,
pub(crate) entries: HashSet<EntryId>,
pub times: Times,
pub custom_data: HashMap<String, CustomDataItem>,
pub is_expanded: bool,
pub default_autotype_sequence: Option<String>,
pub enable_autotype: Option<bool>,
pub enable_searching: Option<bool>,
pub(crate) last_top_visible_entry: Option<EntryId>,
}
impl Group {
pub fn id(&self) -> GroupId {
self.id
}
pub(crate) fn new(parent: Option<GroupId>) -> Group {
Group {
id: GroupId::new(),
parent,
name: String::new(),
notes: None,
icon: None,
groups: HashSet::new(),
entries: HashSet::new(),
times: Times::new(),
custom_data: HashMap::new(),
is_expanded: true,
default_autotype_sequence: None,
enable_autotype: None,
enable_searching: None,
last_top_visible_entry: None,
}
}
pub(crate) fn with_id(id: GroupId, parent: Option<GroupId>) -> Group {
Group {
id,
parent,
name: String::new(),
notes: None,
icon: None,
groups: HashSet::new(),
entries: HashSet::new(),
times: Times::new(),
custom_data: HashMap::new(),
is_expanded: true,
default_autotype_sequence: None,
enable_autotype: None,
enable_searching: None,
last_top_visible_entry: None,
}
}
pub fn group_ids(&self) -> impl Iterator<Item = GroupId> + '_ {
self.groups.iter().cloned()
}
pub fn entry_ids(&self) -> impl Iterator<Item = EntryId> + '_ {
self.entries.iter().cloned()
}
pub fn icon(&self) -> Option<&Icon> {
self.icon.as_ref()
}
}
#[derive(Clone)]
pub struct GroupRef<'a> {
database: &'a crate::db::Database,
id: GroupId,
}
impl GroupRef<'_> {
pub(crate) fn new(database: &Database, id: GroupId) -> GroupRef<'_> {
GroupRef { database, id }
}
pub fn group(&self, id: GroupId) -> Option<GroupRef<'_>> {
self.groups
.contains(&id)
.then(move || GroupRef::new(self.database, id))
}
pub fn entry(&self, id: EntryId) -> Option<EntryRef<'_>> {
self.entries
.contains(&id)
.then(move || EntryRef::new(self.database, id))
}
pub fn groups(&self) -> impl Iterator<Item = GroupRef<'_>> + '_ {
self.groups
.iter()
.map(move |id| GroupRef::new(self.database, *id))
}
pub fn entries(&self) -> impl Iterator<Item = EntryRef<'_>> + '_ {
self.entries
.iter()
.map(move |id| EntryRef::new(self.database, *id))
}
pub fn group_by_name(&self, name: &str) -> Option<GroupRef<'_>> {
self.groups().find(|g| g.name.eq_ignore_ascii_case(name))
}
pub fn entry_by_name(&self, title: &str) -> Option<EntryRef<'_>> {
self.entries().find(|e| {
e.get(crate::db::fields::TITLE)
.is_some_and(|t| t.eq_ignore_ascii_case(title))
})
}
pub fn group_by_path(&self, path: &[&str]) -> Option<GroupRef<'_>> {
let mut current = self.id;
for part in path {
current = self
.database
.groups
.get(¤t)?
.groups
.iter()
.filter_map(|id| self.database.groups.get(id))
.find(|g| g.name.eq_ignore_ascii_case(part))?
.id;
}
Some(GroupRef::new(self.database, current))
}
pub fn database(&self) -> &Database {
self.database
}
pub fn parent(&self) -> Option<GroupRef<'_>> {
self.parent.map(|id| GroupRef::new(self.database, id))
}
pub fn custom_icon(&self) -> Option<CustomIconRef<'_>> {
if let Some(Icon::Custom(cid)) = self.icon {
Some(CustomIconRef::new(self.database, cid))
} else {
None
}
}
}
impl Deref for GroupRef<'_> {
type Target = Group;
#[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref(&self) -> &Self::Target {
self.database
.groups
.get(&self.id)
.expect("GroupRef points to a non-existing group")
}
}
pub struct GroupMut<'a> {
database: &'a mut crate::db::Database,
id: GroupId,
}
impl GroupMut<'_> {
pub(crate) fn new(database: &mut Database, id: GroupId) -> GroupMut<'_> {
GroupMut { database, id }
}
pub fn as_ref(&self) -> GroupRef<'_> {
GroupRef::new(self.database, self.id)
}
pub fn group_mut(&mut self, id: GroupId) -> Option<GroupMut<'_>> {
self.groups
.contains(&id)
.then(move || GroupMut::new(self.database, id))
}
pub fn entry_mut(&mut self, id: EntryId) -> Option<EntryMut<'_>> {
self.entries
.contains(&id)
.then(move || EntryMut::new(self.database, id))
}
pub fn edit(&mut self, f: impl FnOnce(&mut GroupMut<'_>)) -> &mut Self {
f(self);
self
}
pub fn edit_tracking(&mut self, f: impl FnOnce(&mut GroupTrack<'_>)) -> &mut Self {
let mut tracker = self.track_changes();
f(&mut tracker);
tracker.as_mut().times.last_modification = Some(Times::now());
self
}
pub fn add_group(&mut self) -> GroupMut<'_> {
let new_group = Group::new(Some(self.id));
let id = new_group.id;
self.groups.insert(id);
self.database.groups.insert(id, new_group);
GroupMut::new(self.database, id)
}
pub fn add_entry(&mut self) -> EntryMut<'_> {
let new_entry = Entry::new(self.id);
let id = new_entry.id();
self.entries.insert(id);
self.database.entries.insert(id, new_entry);
EntryMut::new(self.database, id)
}
pub(crate) fn add_entry_with_id(&mut self, id: EntryId) -> EntryMut<'_> {
if self.database.entries.contains_key(&id) {
panic!("Entry with ID {} already exists", id);
}
let new_entry = Entry::with_id(id, self.id);
self.entries.insert(id);
self.database.entries.insert(id, new_entry);
EntryMut::new(self.database, id)
}
pub(crate) fn add_group_with_id(&mut self, id: GroupId) -> GroupMut<'_> {
if self.database.groups.contains_key(&id) {
panic!("Group with ID {} already exists", id);
}
let new_group = Group::with_id(id, Some(self.id));
self.groups.insert(id);
self.database.groups.insert(id, new_group);
GroupMut::new(self.database, id)
}
pub fn database_mut(&mut self) -> &mut Database {
self.database
}
pub fn parent_mut(&mut self) -> Option<GroupMut<'_>> {
self.parent.map(move |id| GroupMut::new(self.database, id))
}
pub fn group_by_name_mut(&mut self, name: &str) -> Option<GroupMut<'_>> {
let gid = self.as_ref().groups().find_map(|g| {
if g.name.eq_ignore_ascii_case(name) {
Some(g.id)
} else {
None
}
});
gid.map(move |id| GroupMut::new(self.database, id))
}
pub fn entry_by_name_mut(&mut self, title: &str) -> Option<EntryMut<'_>> {
let eid = self.as_ref().entries().find_map(|e| {
e.get(crate::db::fields::TITLE)
.is_some_and(|t| t.eq_ignore_ascii_case(title))
.then(|| e.id())
});
eid.map(move |id| EntryMut::new(self.database, id))
}
pub fn group_by_path_mut(&mut self, path: &[&str]) -> Option<GroupMut<'_>> {
let mut current = self.id;
for part in path {
current = self
.database
.groups
.get(¤t)?
.groups
.iter()
.filter_map(|id| self.database.groups.get(id))
.find(|g| g.name.eq_ignore_ascii_case(part))?
.id;
}
Some(GroupMut::new(self.database, current))
}
pub fn set_icon_none(&mut self) {
let id = self.id;
if let Some(Icon::Custom(custom_icon_id)) = self.icon {
if let Some(mut custom_icon) = self.database.custom_icon_mut(custom_icon_id) {
custom_icon.groups.retain(|&group_id| group_id != id);
}
}
self.icon = None;
}
pub fn set_icon_builtin(&mut self, icon_id: usize) {
self.set_icon_none();
self.icon = Some(Icon::BuiltIn(icon_id));
}
pub fn set_icon_custom(&mut self, custom_icon_id: CustomIconId) -> Result<(), CustomIconNotFoundError> {
self.set_icon_none();
let id = self.id;
let mut custom_icon = self
.database
.custom_icon_mut(custom_icon_id)
.ok_or(CustomIconNotFoundError(custom_icon_id))?;
custom_icon.groups.insert(id);
self.icon = Some(Icon::Custom(custom_icon_id));
Ok(())
}
pub fn set_icon_custom_new(&mut self, data: Vec<u8>) -> CustomIconMut<'_> {
self.set_icon_none();
let custom_icon_id = CustomIconId::new();
let id = self.id;
self.database.custom_icons.insert(
custom_icon_id,
CustomIcon {
id: custom_icon_id,
entries: HashSet::new(),
groups: vec![id].into_iter().collect(),
data,
},
);
self.icon = Some(Icon::Custom(custom_icon_id));
CustomIconMut::new(self.database, custom_icon_id)
}
pub fn custom_icon_mut(&mut self) -> Option<CustomIconMut<'_>> {
if let Some(Icon::Custom(custom_icon_id)) = self.icon {
Some(CustomIconMut::new(self.database, custom_icon_id))
} else {
None
}
}
pub fn move_to(&mut self, new_parent_id: GroupId) -> Result<(), MoveGroupError> {
let old_parent_id = self.parent.ok_or(MoveGroupError::CannotMoveRoot)?;
if !self.database.groups.contains_key(&new_parent_id) {
return Err(MoveGroupError::NotFound(new_parent_id));
}
let mut current = Some(new_parent_id);
while let Some(curr_id) = current {
if curr_id == self.id {
return Err(MoveGroupError::WouldCreateCycle);
}
current = self.database.groups.get(&curr_id).and_then(|g| g.parent);
}
#[allow(clippy::unwrap_used, clippy::missing_panics_doc)] let mut old_parent = self.database.group_mut(old_parent_id).unwrap();
old_parent.groups.remove(&self.id);
#[allow(clippy::unwrap_used, clippy::missing_panics_doc)] let mut new_parent = self.database.group_mut(new_parent_id).unwrap();
new_parent.groups.insert(self.id);
self.parent = Some(new_parent_id);
Ok(())
}
pub fn remove(self) {
if let Some(parent_id) = self.parent {
if let Some(mut parent) = self.database.group_mut(parent_id) {
parent.groups.remove(&self.id);
}
}
let entry_ids: Vec<EntryId> = self.entries.iter().cloned().collect();
for entry_id in entry_ids {
if let Some(entry) = self.database.entry_mut(entry_id) {
entry.remove();
}
}
let child_group_ids: Vec<GroupId> = self.groups.iter().cloned().collect();
for child_id in child_group_ids {
if let Some(child_group) = self.database.group_mut(child_id) {
child_group.remove();
}
}
self.database.groups.remove(&self.id);
}
pub fn track_changes(&mut self) -> GroupTrack<'_> {
GroupTrack {
database: self.database,
id: self.id,
}
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum MoveGroupError {
#[error("Cannot move the root group")]
CannotMoveRoot,
#[error("Destination group with ID {0} not found")]
NotFound(GroupId),
#[error("Cannot move a group into itself or one of its descendants")]
WouldCreateCycle,
}
impl Deref for GroupMut<'_> {
type Target = Group;
#[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref(&self) -> &Self::Target {
self.database
.groups
.get(&self.id)
.expect("GroupMut points to a non-existing group")
}
}
impl DerefMut for GroupMut<'_> {
#[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref_mut(&mut self) -> &mut Self::Target {
self.database
.groups
.get_mut(&self.id)
.expect("GroupMut points to a non-existing group")
}
}
pub struct GroupTrack<'a> {
database: &'a mut crate::db::Database,
id: GroupId,
}
impl GroupTrack<'_> {
pub fn as_mut(&mut self) -> GroupMut<'_> {
GroupMut::new(self.database, self.id)
}
pub fn move_to(&mut self, new_parent_id: GroupId) -> Result<(), MoveGroupError> {
self.as_mut().move_to(new_parent_id)?;
self.times.location_changed = Some(Times::now());
Ok(())
}
pub fn remove(self) -> Result<(), CannotDeleteRootError> {
if self.id() == self.database.root().id() {
return Err(CannotDeleteRootError);
}
if let Some(parent_id) = self.parent {
if let Some(mut parent) = self.database.group_mut(parent_id) {
parent.groups.remove(&self.id);
}
}
let entry_ids: Vec<EntryId> = self.entries.iter().cloned().collect();
for entry_id in entry_ids {
if let Some(mut entry) = self.database.entry_mut(entry_id) {
entry.track_changes().remove();
}
}
let child_group_ids: Vec<GroupId> = self.groups.iter().cloned().collect();
for child_id in child_group_ids {
if let Some(mut child_group) = self.database.group_mut(child_id) {
child_group.track_changes().remove()?;
}
}
self.database.groups.remove(&self.id);
self.database
.deleted_objects
.insert(self.id.uuid(), Some(Times::now()));
Ok(())
}
pub fn edit(&mut self, f: impl FnOnce(&mut GroupTrack<'_>)) -> &mut Self {
f(self);
self.as_mut().times.last_modification = Some(Times::now());
self
}
}
impl Deref for GroupTrack<'_> {
type Target = Group;
#[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref(&self) -> &Self::Target {
self.database.groups.get(&self.id).expect("Group not found")
}
}
impl DerefMut for GroupTrack<'_> {
#[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref_mut(&mut self) -> &mut Self::Target {
self.database.groups.get_mut(&self.id).expect("Group not found")
}
}
#[derive(Debug, Error)]
#[error("Cannot delete the root group")]
pub struct CannotDeleteRootError;
#[cfg(test)]
mod group_tests {
use crate::db::fields;
use crate::Database;
#[test]
fn get() {
let mut db = Database::new();
let general_group_id = db.root_mut().add_group().edit(|g| g.name = "General".into()).id();
let sample_entry_id = db
.group_mut(general_group_id)
.unwrap()
.add_entry()
.edit(|e| {
e.set_unprotected(fields::TITLE, "Sample Entry #2");
})
.id();
assert_eq!(
db.entry(sample_entry_id).unwrap().get(fields::TITLE),
Some("Sample Entry #2")
);
let root = db.root();
assert!(root.group(general_group_id).is_some());
assert!(db
.group(general_group_id)
.unwrap()
.entry(sample_entry_id)
.is_some());
let grp = root.group_by_path(&["General"]).unwrap();
assert!(grp.entry_by_name("Sample Entry #2").is_some());
assert!(root.group_by_name("General").is_some());
assert!(root.group_by_name("Invalid Group").is_none());
assert!(root.group_by_path(&[]).is_some());
}
#[test]
fn get_mut() {
let mut db = Database::new();
let general_group_id = db.root_mut().add_group().edit(|g| g.name = "General".into()).id();
let sample_entry_id = db
.group_mut(general_group_id)
.unwrap()
.add_entry()
.edit(|e| {
e.set_unprotected(fields::TITLE, "Sample Entry #2");
})
.id();
assert_eq!(
db.entry_mut(sample_entry_id).unwrap().get(fields::TITLE),
Some("Sample Entry #2")
);
let mut root = db.root_mut();
assert!(root.group_mut(general_group_id).is_some());
let mut grp = root.group_by_path_mut(&["General"]).unwrap();
assert!(grp.entry_by_name_mut("Sample Entry #2").is_some());
assert!(root.group_by_name_mut("General").is_some());
assert!(root.group_by_name_mut("Invalid Group").is_none());
assert!(root.group_by_path_mut(&[]).is_some());
assert!(db
.group_mut(general_group_id)
.unwrap()
.entry_mut(sample_entry_id)
.is_some());
}
}