Skip to main content

oximedia_clips/group/
folder.rs

1//! Hierarchical folder organization.
2
3use crate::clip::ClipId;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7use uuid::Uuid;
8
9/// Unique identifier for a folder.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct FolderId(Uuid);
12
13impl FolderId {
14    /// Creates a new random folder ID.
15    #[must_use]
16    pub fn new() -> Self {
17        Self(Uuid::new_v4())
18    }
19
20    /// Creates a folder ID from a UUID.
21    #[must_use]
22    pub const fn from_uuid(uuid: Uuid) -> Self {
23        Self(uuid)
24    }
25
26    /// Returns the inner UUID.
27    #[must_use]
28    pub const fn as_uuid(&self) -> &Uuid {
29        &self.0
30    }
31}
32
33impl Default for FolderId {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl std::fmt::Display for FolderId {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        write!(f, "{}", self.0)
42    }
43}
44
45/// A hierarchical folder for organizing clips.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Folder {
48    /// Unique identifier.
49    pub id: FolderId,
50
51    /// Folder name.
52    pub name: String,
53
54    /// Parent folder ID.
55    pub parent_id: Option<FolderId>,
56
57    /// Optional description.
58    pub description: Option<String>,
59
60    /// Clips in this folder.
61    clip_ids: HashSet<ClipId>,
62
63    /// Child folder IDs.
64    child_folder_ids: HashSet<FolderId>,
65
66    /// Creation timestamp.
67    pub created_at: DateTime<Utc>,
68
69    /// Last modified timestamp.
70    pub modified_at: DateTime<Utc>,
71}
72
73impl Folder {
74    /// Creates a new root folder.
75    #[must_use]
76    pub fn new(name: impl Into<String>) -> Self {
77        let now = Utc::now();
78        Self {
79            id: FolderId::new(),
80            name: name.into(),
81            parent_id: None,
82            description: None,
83            clip_ids: HashSet::new(),
84            child_folder_ids: HashSet::new(),
85            created_at: now,
86            modified_at: now,
87        }
88    }
89
90    /// Creates a new child folder.
91    #[must_use]
92    pub fn new_child(name: impl Into<String>, parent_id: FolderId) -> Self {
93        let mut folder = Self::new(name);
94        folder.parent_id = Some(parent_id);
95        folder
96    }
97
98    /// Adds a clip to the folder.
99    pub fn add_clip(&mut self, clip_id: ClipId) -> bool {
100        if self.clip_ids.insert(clip_id) {
101            self.modified_at = Utc::now();
102            true
103        } else {
104            false
105        }
106    }
107
108    /// Removes a clip from the folder.
109    pub fn remove_clip(&mut self, clip_id: &ClipId) -> bool {
110        if self.clip_ids.remove(clip_id) {
111            self.modified_at = Utc::now();
112            true
113        } else {
114            false
115        }
116    }
117
118    /// Adds a child folder.
119    pub fn add_child_folder(&mut self, folder_id: FolderId) -> bool {
120        if self.child_folder_ids.insert(folder_id) {
121            self.modified_at = Utc::now();
122            true
123        } else {
124            false
125        }
126    }
127
128    /// Removes a child folder.
129    pub fn remove_child_folder(&mut self, folder_id: &FolderId) -> bool {
130        if self.child_folder_ids.remove(folder_id) {
131            self.modified_at = Utc::now();
132            true
133        } else {
134            false
135        }
136    }
137
138    /// Returns all clip IDs in the folder.
139    #[must_use]
140    pub fn clips(&self) -> Vec<ClipId> {
141        self.clip_ids.iter().copied().collect()
142    }
143
144    /// Returns all child folder IDs.
145    #[must_use]
146    pub fn child_folders(&self) -> Vec<FolderId> {
147        self.child_folder_ids.iter().copied().collect()
148    }
149
150    /// Returns the number of clips in the folder.
151    #[must_use]
152    pub fn clip_count(&self) -> usize {
153        self.clip_ids.len()
154    }
155
156    /// Returns the number of child folders.
157    #[must_use]
158    pub fn child_count(&self) -> usize {
159        self.child_folder_ids.len()
160    }
161
162    /// Checks if this is a root folder.
163    #[must_use]
164    pub const fn is_root(&self) -> bool {
165        self.parent_id.is_none()
166    }
167
168    /// Sets the description.
169    pub fn set_description(&mut self, description: impl Into<String>) {
170        self.description = Some(description.into());
171        self.modified_at = Utc::now();
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_folder_creation() {
181        let folder = Folder::new("Root Folder");
182        assert_eq!(folder.name, "Root Folder");
183        assert!(folder.is_root());
184    }
185
186    #[test]
187    fn test_child_folder() {
188        let parent = Folder::new("Parent");
189        let child = Folder::new_child("Child", parent.id);
190        assert_eq!(child.parent_id, Some(parent.id));
191        assert!(!child.is_root());
192    }
193
194    #[test]
195    fn test_folder_clips() {
196        let mut folder = Folder::new("Test");
197        let clip = ClipId::new();
198
199        assert!(folder.add_clip(clip));
200        assert_eq!(folder.clip_count(), 1);
201
202        assert!(folder.remove_clip(&clip));
203        assert_eq!(folder.clip_count(), 0);
204    }
205
206    #[test]
207    fn test_child_folders() {
208        let mut parent = Folder::new("Parent");
209        let child = Folder::new_child("Child", parent.id);
210
211        assert!(parent.add_child_folder(child.id));
212        assert_eq!(parent.child_count(), 1);
213    }
214}