speki_backend/
categories.rs

1use serde::de::Visitor;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3
4use crate::cache::CardCache;
5use crate::card::{CardLocation, SavedCard};
6use crate::common::get_last_modified;
7use crate::paths::{self, get_cards_path};
8use crate::Id;
9use std::collections::{BTreeSet, HashSet};
10use std::fs::{self, File};
11use std::io::{self, BufRead};
12use std::path::Path;
13use std::path::PathBuf;
14
15pub type CardFilter = Box<dyn FnMut(Id, &mut CardCache) -> bool>;
16
17// Represent the category that a card is in, can be nested
18#[derive(Ord, PartialOrd, Eq, Hash, Debug, Clone, Default, PartialEq)]
19pub struct Category(pub Vec<String>);
20
21fn read_lines<P>(filename: P) -> io::Result<Vec<String>>
22where
23    P: AsRef<Path>,
24{
25    let file = File::open(filename)?;
26    let reader = io::BufReader::new(file);
27    reader.lines().collect::<Result<_, _>>()
28}
29
30impl Category {
31    pub fn get_all_tags() -> BTreeSet<String> {
32        let cats = Category::get_following_categories(&Category::root());
33        let mut tags = BTreeSet::new();
34
35        for cat in cats {
36            let path = cat.as_path().join("tags");
37            if let Ok(lines) = read_lines(path) {
38                tags.extend(lines);
39            }
40        }
41        tags.remove("");
42        tags
43    }
44
45    pub fn get_tags(&self) -> BTreeSet<String> {
46        let mut tags = BTreeSet::new();
47        let mut cat = self.clone();
48
49        loop {
50            let path = cat.as_path().join("tags");
51            if let Ok(lines) = read_lines(path) {
52                tags.extend(lines);
53            }
54            if cat.0.is_empty() {
55                break;
56            }
57            cat.0.pop();
58        }
59        tags
60    }
61
62    pub fn root() -> Self {
63        Self::default()
64    }
65
66    pub fn joined(&self) -> String {
67        self.0.join("/")
68    }
69
70    pub fn from_dir_path(path: &Path) -> Self {
71        let folder = path.strip_prefix(paths::get_cards_path()).unwrap();
72
73        let components: Vec<String> = Path::new(folder)
74            .components()
75            .filter_map(|component| component.as_os_str().to_str().map(String::from))
76            .collect();
77
78        let categories = Self(components);
79
80        if categories.as_path().exists() {
81            categories
82        } else {
83            panic!();
84        }
85    }
86
87    pub fn from_card_path(path: &Path) -> Self {
88        let without_prefix = path.strip_prefix(paths::get_cards_path()).unwrap();
89        let folder = without_prefix.parent().unwrap();
90
91        let components: Vec<String> = Path::new(folder)
92            .components()
93            .filter_map(|component| component.as_os_str().to_str().map(String::from))
94            .collect();
95
96        let categories = Self(components);
97
98        if categories.as_path().exists() {
99            categories
100        } else {
101            panic!();
102        }
103    }
104
105    pub fn rec_get_containing_cards(&self) -> Vec<SavedCard> {
106        let categories = self.get_following_categories();
107        let mut cards = HashSet::new();
108        for category in &categories {
109            cards.extend(category.get_containing_cards());
110        }
111        cards.into_iter().collect()
112    }
113
114    pub fn get_containing_cards(&self) -> HashSet<SavedCard> {
115        let directory = self.as_path();
116        let mut cards = HashSet::new();
117
118        for entry in std::fs::read_dir(directory).unwrap() {
119            let entry = entry.unwrap();
120            let path = entry.path();
121
122            if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("toml") {
123                let card = SavedCard::from_path(path.as_path()).into_card();
124                let location = CardLocation::new(&path);
125                let last_modified = get_last_modified(path);
126
127                let full_card = SavedCard::new(card, location, last_modified);
128                cards.insert(full_card);
129            }
130        }
131        cards
132    }
133
134    pub fn get_containing_card_ids(&self) -> HashSet<Id> {
135        self.get_containing_cards()
136            .into_iter()
137            .map(|card| card.id().to_owned())
138            .collect()
139    }
140
141    pub fn sort_categories(categories: &mut [Category]) {
142        categories.sort_by(|a, b| {
143            let a_str = a.0.join("/");
144            let b_str = b.0.join("/");
145            a_str.cmp(&b_str)
146        });
147    }
148    pub fn get_following_categories(&self) -> HashSet<Self> {
149        let categories = Category::load_all().unwrap();
150        let catlen = self.0.len();
151        categories
152            .into_iter()
153            .filter(|cat| cat.0.len() >= catlen && cat.0[0..catlen] == self.0[0..catlen])
154            .collect()
155    }
156
157    pub fn print_it(&self) -> String {
158        self.0.last().unwrap_or(&"root".to_string()).clone()
159    }
160
161    pub fn print_full(&self) -> String {
162        let mut s = "/".to_string();
163        s.push_str(&self.joined());
164        s
165    }
166
167    pub fn print_it_with_depth(&self) -> String {
168        let mut s = String::new();
169        for _ in 0..self.0.len() {
170            s.push_str("  ");
171        }
172        format!("{}{}", s, self.print_it())
173    }
174
175    pub fn import_category() -> Self {
176        let cat = Self(vec!["imports".into()]);
177        std::fs::create_dir_all(cat.as_path()).unwrap();
178        dbg!(cat.as_path());
179        cat
180    }
181
182    pub fn load_all() -> io::Result<Vec<Self>> {
183        let root = get_cards_path();
184        let root = root.as_path();
185        let mut folders = Vec::new();
186        Self::collect_folders_inner(root, root, &mut folders)?;
187        folders.push(Category::default());
188        Category::sort_categories(&mut folders);
189        Ok(folders)
190    }
191
192    pub fn _append(mut self, category: &str) -> Self {
193        self.0.push(category.into());
194        self
195    }
196
197    fn collect_folders_inner(
198        root: &Path,
199        current: &Path,
200        folders: &mut Vec<Category>,
201    ) -> io::Result<()> {
202        if current.is_dir() {
203            for entry in fs::read_dir(current)? {
204                let entry = entry?;
205                let path = entry.path();
206                if path.is_dir() {
207                    // Compute the relative path from root to the current directory
208                    let rel_path = path
209                        .strip_prefix(root)
210                        .expect("Failed to compute relative path")
211                        .components()
212                        .map(|c| c.as_os_str().to_string_lossy().into_owned())
213                        .collect::<Vec<String>>();
214                    if !rel_path.last().unwrap().starts_with('_') {
215                        folders.push(Self(rel_path));
216                        Self::collect_folders_inner(root, &path, folders)?;
217                    }
218                }
219            }
220        }
221        Ok(())
222    }
223
224    pub fn as_path(&self) -> PathBuf {
225        let categories = self.0.join("/");
226        let path = format!("{}/{}", get_cards_path().to_string_lossy(), categories);
227        PathBuf::from(path)
228    }
229
230    fn get_cards_with_filter(&self, mut filter: CardFilter, cache: &mut CardCache) -> Vec<Id> {
231        self.get_containing_card_ids()
232            .into_iter()
233            .filter(|card| filter(*card, cache))
234            .collect()
235    }
236
237    pub fn get_unfinished_cards(&self, cache: &mut CardCache) -> Vec<Id> {
238        self.get_cards_with_filter(Box::new(SavedCard::unfinished_filter), cache)
239    }
240
241    pub fn get_pending_cards(&self, cache: &mut CardCache) -> Vec<Id> {
242        self.get_cards_with_filter(Box::new(SavedCard::pending_filter), cache)
243    }
244
245    pub fn get_random_review_cards(&self, cache: &mut CardCache) -> Vec<Id> {
246        self.get_cards_with_filter(Box::new(SavedCard::random_filter), cache)
247    }
248
249    pub fn get_review_cards(&self, cache: &mut CardCache) -> Vec<Id> {
250        self.get_cards_with_filter(Box::new(SavedCard::review_filter), cache)
251    }
252}
253
254impl Serialize for Category {
255    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
256    where
257        S: Serializer,
258    {
259        let s = self.0.join("/");
260        serializer.serialize_str(&s)
261    }
262}
263
264impl<'de> Deserialize<'de> for Category {
265    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
266    where
267        D: Deserializer<'de>,
268    {
269        struct StringVisitor;
270
271        impl<'de> Visitor<'de> for StringVisitor {
272            type Value = Category;
273
274            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
275                formatter.write_str("a string representing a category")
276            }
277
278            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
279            where
280                E: serde::de::Error,
281            {
282                Ok(Category(value.split('/').map(|s| s.to_string()).collect()))
283            }
284        }
285
286        deserializer.deserialize_str(StringVisitor)
287    }
288}
289
290#[cfg(test)]
291mod tests {
292
293    use crate::categories::Category;
294
295    use super::*;
296
297    #[test]
298    fn test_load_all() {
299        let root = Path::new("./testing");
300        let mut folders = vec![];
301        Category::collect_folders_inner(root, root, &mut folders).unwrap();
302
303        insta::assert_debug_snapshot!(folders);
304    }
305
306    #[test]
307    fn test_joined() {
308        let category = Category(vec!["foo".into(), "bar".into()]);
309        let joined = category.joined();
310        insta::assert_debug_snapshot!(joined);
311    }
312
313    #[test]
314    fn test_as_card_path() {
315        let cards_path = paths::get_cards_path()
316            .join("foo")
317            .join("bar")
318            .join("guten tag.toml");
319        Category::from_card_path(cards_path.as_path());
320    }
321
322    /*
323    #[test]
324    fn test_from_card_path() {
325        let card_path = "./testing/maths/calculus/491f8b92-c943-4c4b-b7bf-f7d483208eb0.toml";
326        let card_path = Path::new(card_path);
327        let x = Category::from_card_path(card_path);
328        insta::assert_debug_snapshot!(x);
329    }
330
331    #[test]
332    fn test_as_path() {
333        let category = Category(vec!["foo".into(), "bar".into()]);
334        let x = category.as_path();
335        insta::assert_debug_snapshot!(x);
336    }
337
338    #[test]
339    fn test_as_path_with_id() {
340        let id = uuid!("8bc35fe2-f02b-4633-8f1b-306eb4e09cd2");
341        let category = Category(vec!["foo".into(), "bar".into()]);
342        let x = category.as_path_with_id(id);
343        insta::assert_debug_snapshot!(x);
344    }
345    */
346}