speki_backend/
categories.rs1use 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#[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 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 }