post_archiver/manager/
tag.rs

1use rusqlite::{params, OptionalExtension};
2
3use crate::{PlatformId, Post, PostId, Tag, TagId};
4
5use super::{PostArchiverConnection, PostArchiverManager};
6
7//=============================================================
8// Querying
9//=============================================================
10impl<T> PostArchiverManager<T>
11where
12    T: PostArchiverConnection,
13{
14    /// Retrieve all tags in the archive.
15    ///
16    /// # Errors
17    ///
18    /// Returns `rusqlite::Error` if there was an error accessing the database.
19    pub fn list_tags(&self) -> Result<Vec<Tag>, rusqlite::Error> {
20        let mut stmt = self.conn().prepare_cached("SELECT * FROM tags")?;
21        let tags = stmt.query_map([], Tag::from_row)?;
22        tags.collect()
23    }
24
25    /// Find a tag by its name and platform.
26    ///
27    /// # Errors
28    ///
29    /// Returns `rusqlite::Error` if there was an error accessing the database.
30    ///
31    /// # Examples
32    ///
33    /// ```rust
34    /// # use post_archiver::manager::PostArchiverManager;
35    /// # use post_archiver::PlatformId;
36    /// fn example(manager: &PostArchiverManager) -> Result<(), rusqlite::Error> {
37    ///     let first_tag = manager.find_tag(&"tag1")?;
38    ///     let second_tag = manager.find_tag(&("tag2", PlatformId(2)))?;
39    ///     let third_tag = manager.find_tag(&("tag3", Some(PlatformId(3))))?;
40    ///     Ok(())
41    /// }
42    /// ```
43    pub fn find_tag<U: FindTag>(&self, tag: &U) -> Result<Option<TagId>, rusqlite::Error> {
44        let name = tag.name().to_string();
45        let platform = tag.platform();
46
47        let cache_key = (name.clone(), platform);
48        if let Some(id) = self.cache.tags.get(&cache_key) {
49            return Ok(Some(*id));
50        }
51
52        let mut stmt = self
53            .conn()
54            .prepare_cached("SELECT id FROM tags WHERE platform IS ? AND name = ?")?;
55
56        let id = stmt
57            .query_row(params![platform, name], |row| row.get(0))
58            .optional();
59
60        if let Ok(Some(id)) = id {
61            self.cache.tags.insert(cache_key, id);
62        }
63
64        id
65    }
66
67    /// Retrieve a tag by its ID.
68    ///
69    /// # Errors
70    ///
71    /// Returns `rusqlite::Error` if:
72    /// * The tag ID does not exist
73    /// * There was an error accessing the database
74    pub fn get_tag(&self, tag: &TagId) -> Result<Option<Tag>, rusqlite::Error> {
75        let mut stmt = self
76            .conn()
77            .prepare_cached("SELECT * FROM tags WHERE id = ?")?;
78
79        stmt.query_row([tag], Tag::from_row).optional()
80    }
81}
82
83pub trait FindTag {
84    fn name(&self) -> &str;
85    fn platform(&self) -> Option<PlatformId> {
86        None
87    }
88}
89
90impl FindTag for &str {
91    fn name(&self) -> &str {
92        self
93    }
94}
95
96impl FindTag for (&str, PlatformId) {
97    fn name(&self) -> &str {
98        self.0
99    }
100    fn platform(&self) -> Option<PlatformId> {
101        Some(self.1)
102    }
103}
104
105impl FindTag for (&str, Option<PlatformId>) {
106    fn name(&self) -> &str {
107        self.0
108    }
109    fn platform(&self) -> Option<PlatformId> {
110        self.1
111    }
112}
113
114//=============================================================
115// Modifying
116//=============================================================
117impl<T> PostArchiverManager<T>
118where
119    T: PostArchiverConnection,
120{
121    /// Add a new tag to the archive.
122    ///
123    /// # Errors
124    ///
125    /// Returns `rusqlite::Error` if:
126    /// * The tag already exists
127    /// * There was an error accessing the database
128    pub fn add_tag(
129        &self,
130        name: String,
131        platform: Option<PlatformId>,
132    ) -> Result<TagId, rusqlite::Error> {
133        let cache_key = (name.clone(), platform);
134        if let Some(id) = self.cache.tags.get(&cache_key) {
135            return Ok(*id);
136        }
137
138        let mut stmt = self
139            .conn()
140            .prepare_cached("INSERT INTO tags (name, platform) VALUES (?, ?) RETURNING id")?;
141
142        let id = stmt.query_row(params![name, platform], |row| row.get(0))?;
143
144        self.cache.tags.insert(cache_key, id);
145
146        Ok(id)
147    }
148
149    /// Remove a tag from the archive.
150    ///
151    /// This will delete the posts associated with the tag as well, but will not delete the posts themselves.
152    ///
153    /// # Errors
154    ///
155    /// # Returns `rusqlite::Error` if:
156    /// * The tag ID does not exist
157    /// * There was an error accessing the database
158    pub fn remove_tag(&self, tag: &TagId) -> Result<(), rusqlite::Error> {
159        let mut stmt = self
160            .conn()
161            .prepare_cached("DELETE FROM tags WHERE id = ?")?;
162
163        stmt.execute([tag])?;
164        Ok(())
165    }
166
167    /// Set the name of a tag.
168    ///
169    /// # Errors
170    ///
171    /// Returns `rusqlite::Error` if there was an error accessing the database.
172    pub fn set_tag_name(&self, tag: &TagId, name: String) -> Result<(), rusqlite::Error> {
173        let mut stmt = self
174            .conn()
175            .prepare_cached("UPDATE tags SET name = ? WHERE id = ? RETURNING platform")?;
176
177        let platform: Option<PlatformId> = stmt.query_row(params![name, tag], |row| row.get(0))?;
178        self.cache.tags.insert((name, platform), *tag);
179        Ok(())
180    }
181
182    /// Set the platform of a tag.
183    ///
184    /// # Errors
185    ///
186    /// Returns `rusqlite::Error` if there was an error accessing the database.
187    pub fn set_tag_platform(
188        &self,
189        tag: &TagId,
190        platform: Option<PlatformId>,
191    ) -> Result<(), rusqlite::Error> {
192        let mut stmt = self
193            .conn()
194            .prepare_cached("UPDATE tags SET platform = ? WHERE id = ? RETURNING name")?;
195
196        let name: String = stmt.query_row(params![platform, tag], |row| row.get(0))?;
197        self.cache.tags.insert((name, platform), *tag);
198        Ok(())
199    }
200}
201
202//=============================================================
203// Relationships
204//=============================================================
205impl<T> PostArchiverManager<T>
206where
207    T: PostArchiverConnection,
208{
209    /// List all tags associated with a post.
210    ///
211    /// # Errors
212    ///
213    /// Returns `rusqlite::Error` if there was an error accessing the database.
214    pub fn list_post_tags(&self, post: &PostId) -> Result<Vec<Tag>, rusqlite::Error> {
215        let mut stmt = self
216            .conn()
217            .prepare_cached("SELECT tags.* FROM tags INNER JOIN post_tags ON post_tags.tag = tags.id WHERE post_tags.post = ?")?;
218        let tags = stmt.query_map([post], Tag::from_row)?;
219        tags.collect()
220    }
221
222    /// List all posts associated with a tag.
223    ///
224    /// # Errors
225    ///
226    /// Returns `rusqlite::Error` if there was an error accessing the database.
227    pub fn list_tag_posts(&self, tag: &TagId) -> Result<Vec<Post>, rusqlite::Error> {
228        let mut stmt = self
229            .conn()
230            .prepare_cached("SELECT posts.* FROM posts INNER JOIN post_tags ON post_tags.post = posts.id WHERE post_tags.tag = ?")?;
231        let posts = stmt.query_map([tag], Post::from_row)?;
232        posts.collect()
233    }
234}
235
236impl Post {
237    /// Retrieve all tags associated with this post.
238    ///
239    /// # Errors
240    ///
241    /// Returns `rusqlite::Error` if there was an error accessing the database.
242    pub fn tags(&self, manager: &PostArchiverManager) -> Result<Vec<Tag>, rusqlite::Error> {
243        manager.list_post_tags(&self.id)
244    }
245}
246
247impl Tag {
248    /// Retrieve all posts associated with this tag.
249    ///
250    /// # Errors
251    ///
252    /// Returns `rusqlite::Error` if there was an error accessing the database.
253    pub fn posts(&self, manager: &PostArchiverManager) -> Result<Vec<Post>, rusqlite::Error> {
254        manager.list_tag_posts(&self.id)
255    }
256}