Skip to main content

yuki_cli/
folders.rs

1use crate::error::YukiError;
2
3/// The archive folders Yuki exposes, as (canonical name, aliases, id).
4const FOLDERS: &[(&str, &[&str], i32)] = &[
5    ("inkoop", &["purchase"], 1),
6    ("verkoop", &["sales"], 2),
7    ("bank", &[], 3),
8    ("personeel", &["personnel"], 4),
9    ("belasting", &["tax"], 5),
10    ("uitzoeken", &[], 7),
11    ("overig-financieel", &["other"], 8),
12];
13
14/// Comma-separated list of accepted folder names, for help text and errors.
15pub fn known_folders() -> String {
16    FOLDERS
17        .iter()
18        .map(|(name, _, _)| *name)
19        .collect::<Vec<_>>()
20        .join(", ")
21}
22
23/// Resolve a folder name or numeric ID to a Yuki archive folder ID.
24///
25/// Accepts a canonical name, a known alias, or a bare integer ID. An unrecognised
26/// name is an error rather than a fallback: silently resolving to folder 0 would
27/// return the whole archive and read as a legitimate result.
28pub fn folder_id(name: &str) -> Result<i32, YukiError> {
29    let trimmed = name.trim();
30    if let Ok(id) = trimmed.parse::<i32>() {
31        return Ok(id);
32    }
33    let lowered = trimmed.to_ascii_lowercase();
34    FOLDERS
35        .iter()
36        .find(|(canonical, aliases, _)| {
37            *canonical == lowered || aliases.contains(&lowered.as_str())
38        })
39        .map(|(_, _, id)| *id)
40        .ok_or_else(|| {
41            YukiError::Config(format!(
42                "unknown folder: {name} (expected one of: {}, or a numeric folder ID)",
43                known_folders()
44            ))
45        })
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn resolves_canonical_names() {
54        assert_eq!(folder_id("inkoop").unwrap(), 1);
55        assert_eq!(folder_id("verkoop").unwrap(), 2);
56        assert_eq!(folder_id("bank").unwrap(), 3);
57        assert_eq!(folder_id("personeel").unwrap(), 4);
58        assert_eq!(folder_id("belasting").unwrap(), 5);
59        assert_eq!(folder_id("uitzoeken").unwrap(), 7);
60        assert_eq!(folder_id("overig-financieel").unwrap(), 8);
61    }
62
63    #[test]
64    fn resolves_english_aliases() {
65        assert_eq!(folder_id("purchase").unwrap(), 1);
66        assert_eq!(folder_id("sales").unwrap(), 2);
67        assert_eq!(folder_id("personnel").unwrap(), 4);
68        assert_eq!(folder_id("tax").unwrap(), 5);
69        assert_eq!(folder_id("other").unwrap(), 8);
70    }
71
72    #[test]
73    fn resolves_case_insensitively_and_trims() {
74        assert_eq!(folder_id("Verkoop").unwrap(), 2);
75        assert_eq!(folder_id("  VERKOOP  ").unwrap(), 2);
76    }
77
78    #[test]
79    fn passes_through_numeric_ids() {
80        // The portal exposes folder IDs directly in its URLs (folder-contents/201),
81        // including subfolders that have no name in this table.
82        assert_eq!(folder_id("2").unwrap(), 2);
83        assert_eq!(folder_id("201").unwrap(), 201);
84        assert_eq!(folder_id("0").unwrap(), 0);
85    }
86
87    #[test]
88    fn unknown_name_is_an_error_not_folder_zero() {
89        // Regression: `unwrap_or(0)` made every unrecognised name silently return
90        // the root folder, so `--folder bogusname` looked like a successful query.
91        let err = folder_id("bogusname").unwrap_err();
92        let msg = err.to_string();
93        assert!(msg.contains("unknown folder"), "got: {msg}");
94        assert!(
95            msg.contains("verkoop"),
96            "error should list valid names: {msg}"
97        );
98    }
99
100    #[test]
101    fn empty_name_is_an_error() {
102        assert!(folder_id("").is_err());
103    }
104}