1use std::{
2 collections::{HashMap, HashSet},
3 ops::{Deref, DerefMut},
4};
5
6use indexmap::IndexSet;
7use thiserror::Error;
8use uuid::Uuid;
9
10use crate::{
11 db::{
12 CustomDataItem, CustomIcon, CustomIconId, CustomIconMut, CustomIconNotFoundError, CustomIconRef, Entry,
13 EntryId, EntryMut, EntryRef, Icon, Times,
14 },
15 Database,
16};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
21pub struct GroupId(Uuid);
22
23impl GroupId {
24 pub fn new() -> Self {
26 Self(Uuid::new_v4())
27 }
28
29 pub const fn from_uuid(uuid: Uuid) -> Self {
35 Self(uuid)
36 }
37
38 pub fn uuid(&self) -> Uuid {
40 self.0
41 }
42}
43
44impl From<Uuid> for GroupId {
45 fn from(uuid: Uuid) -> Self {
46 Self::from_uuid(uuid)
47 }
48}
49
50impl std::fmt::Display for GroupId {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(f, "{}", self.0)
53 }
54}
55
56#[derive(Debug, Eq, PartialEq, Clone)]
58#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
59pub struct Group {
60 pub(crate) id: GroupId,
62
63 pub(crate) parent: Option<GroupId>,
65
66 pub name: String,
68
69 pub notes: Option<String>,
71
72 pub tags: Vec<String>,
74
75 pub(crate) icon: Option<Icon>,
77
78 pub(crate) groups: IndexSet<GroupId>,
80
81 pub(crate) entries: IndexSet<EntryId>,
83
84 pub times: Times,
86
87 pub custom_data: HashMap<String, CustomDataItem>,
89
90 pub is_expanded: bool,
92
93 pub default_autotype_sequence: Option<String>,
95
96 pub enable_autotype: Option<bool>,
98
99 pub enable_searching: Option<bool>,
101
102 pub(crate) last_top_visible_entry: Option<EntryId>,
104
105 pub(crate) previous_parent_group: Option<GroupId>,
106}
107
108impl Group {
109 pub fn id(&self) -> GroupId {
111 self.id
112 }
113
114 pub(crate) fn new(parent: Option<GroupId>) -> Group {
115 Group {
116 id: GroupId::new(),
117 parent,
118 name: String::new(),
119 notes: None,
120 tags: Vec::new(),
121 icon: None,
122 groups: IndexSet::new(),
123 entries: IndexSet::new(),
124 times: Times::new(),
125 custom_data: HashMap::new(),
126 is_expanded: true,
127 default_autotype_sequence: None,
128 enable_autotype: None,
129 enable_searching: None,
130 last_top_visible_entry: None,
131 previous_parent_group: None,
132 }
133 }
134
135 pub(crate) fn with_id(id: GroupId, parent: Option<GroupId>) -> Group {
136 Group {
137 id,
138 parent,
139 name: String::new(),
140 notes: None,
141 tags: Vec::new(),
142 icon: None,
143 groups: IndexSet::new(),
144 entries: IndexSet::new(),
145 times: Times::new(),
146 custom_data: HashMap::new(),
147 is_expanded: true,
148 default_autotype_sequence: None,
149 enable_autotype: None,
150 enable_searching: None,
151 last_top_visible_entry: None,
152 previous_parent_group: None,
153 }
154 }
155
156 pub fn group_ids(&self) -> impl Iterator<Item = GroupId> + '_ {
158 self.groups.iter().cloned()
159 }
160
161 pub fn entry_ids(&self) -> impl Iterator<Item = EntryId> + '_ {
163 self.entries.iter().cloned()
164 }
165
166 pub fn icon(&self) -> Option<&Icon> {
168 self.icon.as_ref()
169 }
170}
171
172#[derive(Clone)]
174pub struct GroupRef<'a> {
175 database: &'a crate::db::Database,
176 id: GroupId,
177}
178
179impl GroupRef<'_> {
180 pub(crate) fn new(database: &Database, id: GroupId) -> GroupRef<'_> {
181 GroupRef { database, id }
182 }
183
184 pub fn group(&self, id: GroupId) -> Option<GroupRef<'_>> {
186 self.groups
187 .contains(&id)
188 .then(move || GroupRef::new(self.database, id))
189 }
190
191 pub fn entry(&self, id: EntryId) -> Option<EntryRef<'_>> {
193 self.entries
194 .contains(&id)
195 .then(move || EntryRef::new(self.database, id))
196 }
197
198 pub fn groups(&self) -> impl Iterator<Item = GroupRef<'_>> + '_ {
200 self.groups
201 .iter()
202 .map(move |id| GroupRef::new(self.database, *id))
203 }
204
205 pub fn entries(&self) -> impl Iterator<Item = EntryRef<'_>> + '_ {
207 self.entries
208 .iter()
209 .map(move |id| EntryRef::new(self.database, *id))
210 }
211
212 pub fn group_by_name(&self, name: &str) -> Option<GroupRef<'_>> {
214 self.groups().find(|g| g.name.eq_ignore_ascii_case(name))
215 }
216
217 pub fn entry_by_name(&self, title: &str) -> Option<EntryRef<'_>> {
219 self.entries().find(|e| {
220 e.get(crate::db::fields::TITLE)
221 .is_some_and(|t| t.eq_ignore_ascii_case(title))
222 })
223 }
224
225 pub fn group_by_path(&self, path: &[&str]) -> Option<GroupRef<'_>> {
227 let mut current = self.id;
228
229 for part in path {
230 current = self
231 .database
232 .groups
233 .get(¤t)?
234 .groups
235 .iter()
236 .filter_map(|id| self.database.groups.get(id))
237 .find(|g| g.name.eq_ignore_ascii_case(part))?
238 .id;
239 }
240
241 Some(GroupRef::new(self.database, current))
242 }
243
244 pub fn database(&self) -> &Database {
246 self.database
247 }
248
249 pub fn parent(&self) -> Option<GroupRef<'_>> {
251 self.parent.map(|id| GroupRef::new(self.database, id))
252 }
253
254 pub fn previous_parent(&self) -> Option<GroupRef<'_>> {
256 self.previous_parent_group
257 .and_then(|id| self.database().group(id))
258 }
259
260 pub fn custom_icon(&self) -> Option<CustomIconRef<'_>> {
262 if let Some(Icon::Custom(cid)) = self.icon {
263 Some(CustomIconRef::new(self.database, cid))
264 } else {
265 None
266 }
267 }
268}
269
270impl Deref for GroupRef<'_> {
271 type Target = Group;
272
273 #[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref(&self) -> &Self::Target {
275 self.database
276 .groups
277 .get(&self.id)
278 .expect("GroupRef points to a non-existing group")
279 }
280}
281
282pub struct GroupMut<'a> {
284 database: &'a mut crate::db::Database,
285 id: GroupId,
286}
287
288impl GroupMut<'_> {
289 pub(crate) fn new(database: &mut Database, id: GroupId) -> GroupMut<'_> {
290 GroupMut { database, id }
291 }
292
293 pub fn as_ref(&self) -> GroupRef<'_> {
295 GroupRef::new(self.database, self.id)
296 }
297
298 pub fn group_mut(&mut self, id: GroupId) -> Option<GroupMut<'_>> {
300 self.groups
301 .contains(&id)
302 .then(move || GroupMut::new(self.database, id))
303 }
304
305 pub fn entry_mut(&mut self, id: EntryId) -> Option<EntryMut<'_>> {
307 self.entries
308 .contains(&id)
309 .then(move || EntryMut::new(self.database, id))
310 }
311
312 pub fn edit(&mut self, f: impl FnOnce(&mut GroupMut<'_>)) -> &mut Self {
314 f(self);
315 self
316 }
317
318 pub fn edit_tracking(&mut self, f: impl FnOnce(&mut GroupTrack<'_>)) -> &mut Self {
320 let mut tracker = self.track_changes();
321 f(&mut tracker);
322 tracker.as_mut().times.last_modification = Some(Times::now());
323 self
324 }
325
326 #[allow(clippy::missing_panics_doc)]
328 pub fn add_group(&mut self) -> GroupMut<'_> {
329 let new_group = Group::new(Some(self.id));
330 let id = new_group.id;
331
332 #[allow(clippy::expect_used)] self.add_group_with_id(id)
337 .expect("fresh v4 UUID cannot collide with an existing group identifier")
338 }
339
340 #[allow(clippy::missing_panics_doc)]
342 pub fn add_entry(&mut self) -> EntryMut<'_> {
343 let new_entry = Entry::new(self.id);
344 let id = new_entry.id();
345
346 #[allow(clippy::expect_used)] self.add_entry_with_id(id)
351 .expect("fresh v4 UUID cannot collide with an existing entry identifier")
352 }
353
354 pub fn add_entry_with_id(&mut self, id: EntryId) -> Result<EntryMut<'_>, DuplicateEntryIdError> {
375 if self.database.entries.contains_key(&id) {
376 return Err(DuplicateEntryIdError(id));
377 }
378
379 let new_entry = Entry::with_id(id, self.id);
380 self.entries.insert(id);
381 self.database.entries.insert(id, new_entry);
382
383 Ok(EntryMut::new(self.database, id))
384 }
385
386 pub fn add_group_with_id(&mut self, id: GroupId) -> Result<GroupMut<'_>, DuplicateGroupIdError> {
406 if self.database.groups.contains_key(&id) {
407 return Err(DuplicateGroupIdError(id));
408 }
409
410 let new_group = Group::with_id(id, Some(self.id));
411 self.groups.insert(id);
412 self.database.groups.insert(id, new_group);
413
414 Ok(GroupMut::new(self.database, id))
415 }
416
417 pub fn database_mut(&mut self) -> &mut Database {
419 self.database
420 }
421
422 pub fn parent_mut(&mut self) -> Option<GroupMut<'_>> {
424 self.parent.map(move |id| GroupMut::new(self.database, id))
425 }
426
427 pub fn previous_parent_mut(&mut self) -> Option<GroupMut<'_>> {
429 self.previous_parent_group
430 .and_then(move |id| self.database_mut().group_mut(id))
431 }
432
433 pub fn group_by_name_mut(&mut self, name: &str) -> Option<GroupMut<'_>> {
435 let gid = self.as_ref().groups().find_map(|g| {
436 if g.name.eq_ignore_ascii_case(name) {
437 Some(g.id)
438 } else {
439 None
440 }
441 });
442
443 gid.map(move |id| GroupMut::new(self.database, id))
444 }
445
446 pub fn entry_by_name_mut(&mut self, title: &str) -> Option<EntryMut<'_>> {
448 let eid = self.as_ref().entries().find_map(|e| {
449 e.get(crate::db::fields::TITLE)
450 .is_some_and(|t| t.eq_ignore_ascii_case(title))
451 .then(|| e.id())
452 });
453
454 eid.map(move |id| EntryMut::new(self.database, id))
455 }
456
457 pub fn group_by_path_mut(&mut self, path: &[&str]) -> Option<GroupMut<'_>> {
460 let mut current = self.id;
461
462 for part in path {
463 current = self
464 .database
465 .groups
466 .get(¤t)?
467 .groups
468 .iter()
469 .filter_map(|id| self.database.groups.get(id))
470 .find(|g| g.name.eq_ignore_ascii_case(part))?
471 .id;
472 }
473
474 Some(GroupMut::new(self.database, current))
475 }
476
477 pub fn set_icon_none(&mut self) {
479 let id = self.id;
480
481 if let Some(Icon::Custom(custom_icon_id)) = self.icon {
482 if let Some(mut custom_icon) = self.database.custom_icon_mut(custom_icon_id) {
484 custom_icon.groups.retain(|&group_id| group_id != id);
485 }
486 }
487
488 self.icon = None;
489 }
490
491 pub fn set_icon_builtin(&mut self, icon_id: usize) {
493 self.set_icon_none();
494 self.icon = Some(Icon::BuiltIn(icon_id));
495 }
496
497 pub fn set_icon_custom(&mut self, custom_icon_id: CustomIconId) -> Result<(), CustomIconNotFoundError> {
499 self.set_icon_none();
500
501 let id = self.id;
502
503 let mut custom_icon = self
504 .database
505 .custom_icon_mut(custom_icon_id)
506 .ok_or(CustomIconNotFoundError(custom_icon_id))?;
507
508 custom_icon.groups.insert(id);
509
510 self.icon = Some(Icon::Custom(custom_icon_id));
511
512 Ok(())
513 }
514
515 pub fn set_icon_custom_new(&mut self, data: Vec<u8>) -> CustomIconMut<'_> {
518 self.set_icon_none();
519
520 let custom_icon_id = CustomIconId::new();
521
522 let id = self.id;
523
524 self.database.custom_icons.insert(
525 custom_icon_id,
526 CustomIcon {
527 id: custom_icon_id,
528 entries: HashSet::new(),
529 groups: vec![id].into_iter().collect(),
530 name: None,
531 last_modification_time: Some(Times::now()),
532 data,
533 },
534 );
535
536 self.icon = Some(Icon::Custom(custom_icon_id));
537
538 CustomIconMut::new(self.database, custom_icon_id)
539 }
540
541 pub fn custom_icon_mut(&mut self) -> Option<CustomIconMut<'_>> {
544 if let Some(Icon::Custom(custom_icon_id)) = self.icon {
545 Some(CustomIconMut::new(self.database, custom_icon_id))
546 } else {
547 None
548 }
549 }
550
551 pub fn move_to(&mut self, new_parent_id: GroupId) -> Result<(), MoveGroupError> {
557 let old_parent_id = self.parent.ok_or(MoveGroupError::CannotMoveRoot)?;
558
559 if !self.database.groups.contains_key(&new_parent_id) {
560 return Err(MoveGroupError::NotFound(new_parent_id));
561 }
562
563 let mut current = Some(new_parent_id);
565 while let Some(curr_id) = current {
566 if curr_id == self.id {
567 return Err(MoveGroupError::WouldCreateCycle);
568 }
569 current = self.database.groups.get(&curr_id).and_then(|g| g.parent);
570 }
571
572 #[allow(clippy::unwrap_used, clippy::missing_panics_doc)] let mut old_parent = self.database.group_mut(old_parent_id).unwrap();
575 old_parent.groups.shift_remove(&self.id);
576
577 #[allow(clippy::unwrap_used, clippy::missing_panics_doc)] let mut new_parent = self.database.group_mut(new_parent_id).unwrap();
580 new_parent.groups.insert(self.id);
581
582 self.parent = Some(new_parent_id);
584 self.previous_parent_group = Some(old_parent_id);
585
586 Ok(())
587 }
588
589 pub fn remove(mut self) {
591 self.set_icon_none();
594
595 if let Some(parent_id) = self.parent {
597 if let Some(mut parent) = self.database.group_mut(parent_id) {
598 parent.groups.shift_remove(&self.id);
599 }
600 }
601
602 let entry_ids: Vec<EntryId> = self.entries.iter().cloned().collect();
604 for entry_id in entry_ids {
605 if let Some(entry) = self.database.entry_mut(entry_id) {
606 entry.remove();
607 }
608 }
609
610 let child_group_ids: Vec<GroupId> = self.groups.iter().cloned().collect();
612 for child_id in child_group_ids {
613 if let Some(child_group) = self.database.group_mut(child_id) {
614 child_group.remove();
615 }
616 }
617
618 self.database.groups.remove(&self.id);
620
621 let uuid = self.id.0;
624 let meta = &mut self.database.meta;
625 if meta.recyclebin_uuid == Some(uuid) {
626 meta.recyclebin_uuid = None;
627 }
628 if meta.entry_templates_group == Some(uuid) {
629 meta.entry_templates_group = None;
630 }
631 if meta.last_selected_group == Some(uuid) {
632 meta.last_selected_group = None;
633 }
634 if meta.last_top_visible_group == Some(uuid) {
635 meta.last_top_visible_group = None;
636 }
637 }
638
639 pub fn track_changes(&mut self) -> GroupTrack<'_> {
642 GroupTrack {
643 database: self.database,
644 id: self.id,
645 }
646 }
647}
648
649#[derive(Debug, Error)]
651#[error("a group with ID {0} already exists in the database")]
652pub struct DuplicateGroupIdError(pub GroupId);
653
654#[derive(Debug, Error)]
656#[error("an entry with ID {0} already exists in the database")]
657pub struct DuplicateEntryIdError(pub EntryId);
658
659#[derive(Debug, Error)]
661#[non_exhaustive]
662pub enum MoveGroupError {
663 #[error("Cannot move the root group")]
665 CannotMoveRoot,
666
667 #[error("Destination group with ID {0} not found")]
671 NotFound(GroupId),
672
673 #[error("Cannot move a group into itself or one of its descendants")]
675 WouldCreateCycle,
676}
677
678impl Deref for GroupMut<'_> {
679 type Target = Group;
680
681 #[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref(&self) -> &Self::Target {
683 self.database
684 .groups
685 .get(&self.id)
686 .expect("GroupMut points to a non-existing group")
687 }
688}
689
690impl DerefMut for GroupMut<'_> {
691 #[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref_mut(&mut self) -> &mut Self::Target {
693 self.database
694 .groups
695 .get_mut(&self.id)
696 .expect("GroupMut points to a non-existing group")
697 }
698}
699
700pub struct GroupTrack<'a> {
703 database: &'a mut crate::db::Database,
704 id: GroupId,
705}
706
707impl GroupTrack<'_> {
708 pub fn as_mut(&mut self) -> GroupMut<'_> {
710 GroupMut::new(self.database, self.id)
711 }
712
713 pub fn move_to(&mut self, new_parent_id: GroupId) -> Result<(), MoveGroupError> {
715 self.as_mut().move_to(new_parent_id)?;
716 self.times.location_changed = Some(Times::now());
717 Ok(())
718 }
719
720 pub fn remove(self) -> Result<(), CannotDeleteRootError> {
723 if self.id() == self.database.root().id() {
724 return Err(CannotDeleteRootError);
725 }
726
727 if let Some(parent_id) = self.parent {
729 if let Some(mut parent) = self.database.group_mut(parent_id) {
730 parent.groups.shift_remove(&self.id);
731 }
732 }
733
734 let entry_ids: Vec<EntryId> = self.entries.iter().cloned().collect();
736
737 for entry_id in entry_ids {
738 if let Some(mut entry) = self.database.entry_mut(entry_id) {
739 entry.track_changes().remove();
740 }
741 }
742
743 let child_group_ids: Vec<GroupId> = self.groups.iter().cloned().collect();
745 for child_id in child_group_ids {
746 if let Some(mut child_group) = self.database.group_mut(child_id) {
747 child_group.track_changes().remove()?;
748 }
749 }
750
751 self.database.groups.remove(&self.id);
753 self.database
754 .deleted_objects
755 .insert(self.id.uuid(), Some(Times::now()));
756
757 Ok(())
758 }
759
760 pub fn edit(&mut self, f: impl FnOnce(&mut GroupTrack<'_>)) -> &mut Self {
762 f(self);
763 self.as_mut().times.last_modification = Some(Times::now());
764 self
765 }
766}
767
768impl Deref for GroupTrack<'_> {
769 type Target = Group;
770
771 #[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref(&self) -> &Self::Target {
773 self.database.groups.get(&self.id).expect("Group not found")
774 }
775}
776
777impl DerefMut for GroupTrack<'_> {
778 #[allow(clippy::expect_used, clippy::missing_panics_doc)] fn deref_mut(&mut self) -> &mut Self::Target {
780 self.database.groups.get_mut(&self.id).expect("Group not found")
781 }
782}
783
784#[derive(Debug, Error)]
786#[error("Cannot delete the root group")]
787pub struct CannotDeleteRootError;
788
789#[cfg(test)]
790#[allow(clippy::unwrap_used)]
791mod group_tests {
792 use crate::db::fields;
793 use crate::Database;
794
795 #[test]
796 fn get() {
797 let mut db = Database::new();
798
799 let general_group_id = db.root_mut().add_group().edit(|g| g.name = "General".into()).id();
800
801 let sample_entry_id = db
802 .group_mut(general_group_id)
803 .unwrap()
804 .add_entry()
805 .edit(|e| {
806 e.set_unprotected(fields::TITLE, "Sample Entry #2");
807 })
808 .id();
809
810 assert_eq!(
811 db.entry(sample_entry_id).unwrap().get(fields::TITLE),
812 Some("Sample Entry #2")
813 );
814
815 let root = db.root();
816
817 assert!(root.group(general_group_id).is_some());
818 assert!(db
819 .group(general_group_id)
820 .unwrap()
821 .entry(sample_entry_id)
822 .is_some());
823
824 let grp = root.group_by_path(&["General"]).unwrap();
825
826 assert!(grp.entry_by_name("Sample Entry #2").is_some());
827
828 assert!(root.group_by_name("General").is_some());
829
830 assert!(root.group_by_name("Invalid Group").is_none());
831
832 assert!(root.group_by_path(&[]).is_some());
833 }
834
835 #[test]
836 fn get_mut() {
837 let mut db = Database::new();
838
839 let general_group_id = db.root_mut().add_group().edit(|g| g.name = "General".into()).id();
840
841 let sample_entry_id = db
842 .group_mut(general_group_id)
843 .unwrap()
844 .add_entry()
845 .edit(|e| {
846 e.set_unprotected(fields::TITLE, "Sample Entry #2");
847 })
848 .id();
849
850 assert_eq!(
851 db.entry_mut(sample_entry_id).unwrap().get(fields::TITLE),
852 Some("Sample Entry #2")
853 );
854
855 let mut root = db.root_mut();
856 assert!(root.group_mut(general_group_id).is_some());
857
858 let mut grp = root.group_by_path_mut(&["General"]).unwrap();
859
860 assert!(grp.entry_by_name_mut("Sample Entry #2").is_some());
861
862 assert!(root.group_by_name_mut("General").is_some());
863 assert!(root.group_by_name_mut("Invalid Group").is_none());
864 assert!(root.group_by_path_mut(&[]).is_some());
865
866 assert!(db
867 .group_mut(general_group_id)
868 .unwrap()
869 .entry_mut(sample_entry_id)
870 .is_some());
871 }
872
873 #[test]
874 fn add_entry_with_id_uses_supplied_uuid() {
875 use crate::db::EntryId;
876 use uuid::uuid;
877
878 let mut db = Database::new();
879 let pinned_uuid = uuid!("00000000-0000-0000-0000-0000000000aa");
880 let pinned: EntryId = pinned_uuid.into();
881
882 let inserted_id = db
883 .root_mut()
884 .add_entry_with_id(pinned)
885 .unwrap()
886 .edit(|e| {
887 e.set_unprotected(fields::TITLE, "pinned");
888 })
889 .id();
890
891 assert_eq!(inserted_id, pinned);
892 assert_eq!(inserted_id.uuid(), pinned_uuid);
893 assert_eq!(db.entry(pinned).unwrap().get(fields::TITLE), Some("pinned"),);
894 }
895
896 #[test]
897 fn add_entry_with_id_duplicate_returns_error() {
898 use crate::db::{DuplicateEntryIdError, EntryId};
899 use uuid::uuid;
900
901 let mut db = Database::new();
902 let pinned: EntryId = uuid!("00000000-0000-0000-0000-0000000000ab").into();
903
904 db.root_mut().add_entry_with_id(pinned).unwrap();
905
906 assert!(matches!(
907 db.root_mut().add_entry_with_id(pinned),
908 Err(DuplicateEntryIdError(eid)) if eid == pinned
909 ));
910 }
911
912 #[test]
913 fn add_group_with_id_uses_supplied_uuid() {
914 use crate::db::GroupId;
915 use uuid::uuid;
916
917 let mut db = Database::new();
918 let pinned_uuid = uuid!("00000000-0000-0000-0000-0000000000ba");
919 let pinned: GroupId = pinned_uuid.into();
920
921 let inserted_id = db
922 .root_mut()
923 .add_group_with_id(pinned)
924 .unwrap()
925 .edit(|g| g.name = "pinned".into())
926 .id();
927
928 assert_eq!(inserted_id, pinned);
929 assert_eq!(inserted_id.uuid(), pinned_uuid);
930 assert_eq!(db.group(pinned).unwrap().name, "pinned");
931 }
932
933 #[test]
934 fn add_group_with_id_duplicate_returns_error() {
935 use crate::db::{DuplicateGroupIdError, GroupId};
936 use uuid::uuid;
937
938 let mut db = Database::new();
939 let pinned: GroupId = uuid!("00000000-0000-0000-0000-0000000000bb").into();
940
941 db.root_mut().add_group_with_id(pinned).unwrap();
942
943 assert!(matches!(
944 db.root_mut().add_group_with_id(pinned),
945 Err(DuplicateGroupIdError(gid)) if gid == pinned
946 ));
947 }
948
949 #[test]
950 fn group_id_new_generates_distinct_ids() {
951 use crate::db::GroupId;
952
953 assert_ne!(GroupId::new(), GroupId::new());
954 }
955
956 #[test]
957 fn from_uuid_impls_match_constructors() {
958 use crate::db::{EntryId, GroupId};
959 use uuid::uuid;
960
961 let raw = uuid!("00000000-0000-0000-0000-0000000000cc");
962 let from_entry: EntryId = raw.into();
963 let from_entry_ctor = EntryId::from_uuid(raw);
964 assert_eq!(from_entry, from_entry_ctor);
965 assert_eq!(from_entry.uuid(), raw);
966
967 let from_group: GroupId = raw.into();
968 let from_group_ctor = GroupId::from_uuid(raw);
969 assert_eq!(from_group, from_group_ctor);
970 assert_eq!(from_group.uuid(), raw);
971 }
972}