Skip to main content

harn_rules/
loader.rs

1//! Load rules from disk: a single TOML rule file, or a directory of them.
2
3use std::path::Path;
4
5use crate::error::RulesError;
6use crate::model::Rule;
7
8/// Load a single rule from a `.toml` file.
9pub fn load_rule_file(path: impl AsRef<Path>) -> Result<Rule, RulesError> {
10    let path = path.as_ref();
11    let text = std::fs::read_to_string(path).map_err(|source| RulesError::Read {
12        path: path.display().to_string(),
13        source,
14    })?;
15    Rule::from_toml_str(&text).map_err(|source| RulesError::Parse {
16        path: path.display().to_string(),
17        source,
18    })
19}
20
21/// Load every `.toml` rule directly under `dir`, sorted by filename for a
22/// deterministic order. Non-`.toml` files and subdirectories are ignored.
23pub fn load_rule_dir(dir: impl AsRef<Path>) -> Result<Vec<Rule>, RulesError> {
24    let dir = dir.as_ref();
25    let mut entries: Vec<_> = std::fs::read_dir(dir)
26        .map_err(|source| RulesError::Read {
27            path: dir.display().to_string(),
28            source,
29        })?
30        .filter_map(Result::ok)
31        .map(|e| e.path())
32        .filter(|p| p.is_file() && p.extension().is_some_and(|ext| ext == "toml"))
33        .collect();
34    entries.sort();
35
36    entries.into_iter().map(load_rule_file).collect()
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42    use std::io::Write;
43
44    fn write(dir: &Path, name: &str, body: &str) {
45        let mut f = std::fs::File::create(dir.join(name)).unwrap();
46        f.write_all(body.as_bytes()).unwrap();
47    }
48
49    #[test]
50    fn loads_a_single_rule_file() {
51        let dir = tempfile::tempdir().unwrap();
52        write(
53            dir.path(),
54            "r.toml",
55            r#"
56            id = "r"
57            language = "rust"
58            [rule]
59            kind = "macro_invocation"
60            "#,
61        );
62        let rule = load_rule_file(dir.path().join("r.toml")).unwrap();
63        assert_eq!(rule.id, "r");
64    }
65
66    #[test]
67    fn loads_a_directory_in_sorted_order() {
68        let dir = tempfile::tempdir().unwrap();
69        write(
70            dir.path(),
71            "b.toml",
72            "id = \"b\"\nlanguage = \"rust\"\n[rule]\nkind = \"x\"\n",
73        );
74        write(
75            dir.path(),
76            "a.toml",
77            "id = \"a\"\nlanguage = \"rust\"\n[rule]\nkind = \"x\"\n",
78        );
79        write(dir.path(), "ignore.txt", "not a rule");
80        let rules = load_rule_dir(dir.path()).unwrap();
81        let ids: Vec<_> = rules.iter().map(|r| r.id.as_str()).collect();
82        assert_eq!(ids, vec!["a", "b"]);
83    }
84
85    #[test]
86    fn parse_error_names_the_file() {
87        let dir = tempfile::tempdir().unwrap();
88        write(dir.path(), "bad.toml", "id = \nthis is not toml");
89        let err = load_rule_file(dir.path().join("bad.toml")).unwrap_err();
90        assert!(matches!(err, RulesError::Parse { .. }));
91    }
92}