lowfat_runner/
lf_filter.rs1use anyhow::{Context, Result};
2use lowfat_core::lf::{self, ExecCtx};
3use lowfat_plugin::plugin::{FilterInput, FilterOutput, FilterPlugin, PluginInfo};
4use std::path::PathBuf;
5
6pub struct LfFilter {
10 pub info: PluginInfo,
11 pub ruleset: lf::RuleSet,
12 pub entry: PathBuf,
13}
14
15impl LfFilter {
16 pub fn load(info: PluginInfo, entry: PathBuf) -> Result<Self> {
17 let source = std::fs::read_to_string(&entry)
18 .with_context(|| format!("reading {}", entry.display()))?;
19 let ruleset =
20 lf::parse(&source).with_context(|| format!("parsing {}", entry.display()))?;
21 Ok(Self {
22 info,
23 ruleset,
24 entry,
25 })
26 }
27}
28
29impl FilterPlugin for LfFilter {
30 fn info(&self) -> PluginInfo {
31 self.info.clone()
32 }
33
34 fn filter(&self, input: &FilterInput) -> Result<FilterOutput> {
35 let ctx = ExecCtx {
36 sub: &input.subcommand,
37 level: input.level,
38 exit_code: input.exit_code,
39 args: &input.args,
40 };
41 match lf::execute(&self.ruleset, &ctx, &input.raw) {
44 Ok(text) => Ok(FilterOutput {
45 passthrough: text.is_empty(),
46 text,
47 }),
48 Err(e) => {
49 eprintln!("[lowfat] {} filter error: {e:#}", self.info.name);
50 Ok(FilterOutput {
51 passthrough: true,
52 text: input.raw.clone(),
53 })
54 }
55 }
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use lowfat_core::level::Level;
63 use std::io::Write;
64
65 fn make_input(raw: &str, sub: &str, level: Level) -> FilterInput {
66 FilterInput {
67 raw: raw.to_string(),
68 command: "test".into(),
69 subcommand: sub.into(),
70 args: vec![],
71 level,
72 head_limit: 30,
73 exit_code: 0,
74 }
75 }
76
77 fn write_lf(name: &str, body: &str) -> PathBuf {
78 let dir = std::env::temp_dir().join(format!(
79 "lowfat-lf-test-{name}-{}",
80 std::process::id()
81 ));
82 let _ = std::fs::remove_dir_all(&dir);
83 std::fs::create_dir_all(&dir).unwrap();
84 let path = dir.join("filter.lf");
85 let mut f = std::fs::File::create(&path).unwrap();
86 f.write_all(body.as_bytes()).unwrap();
87 path
88 }
89
90 fn info() -> PluginInfo {
91 PluginInfo {
92 name: "test".into(),
93 version: "0.0.0".into(),
94 commands: vec!["test".into()],
95 subcommands: vec![],
96 }
97 }
98
99 #[test]
100 fn lf_filter_runs_keep_head() {
101 let path = write_lf(
102 "kh",
103 r#"
104status:
105 keep /^M /
106 head 2
107"#,
108 );
109 let f = LfFilter::load(info(), path).unwrap();
110 let out = f
111 .filter(&make_input(
112 "M one\n?? two\nM three\nM four\nM five\n",
113 "status",
114 Level::Full,
115 ))
116 .unwrap();
117 assert_eq!(out.text, "M one\nM three\n");
118 }
119
120 #[test]
121 fn lf_filter_passthrough_on_parse_error_falls_back() {
122 let path = write_lf("bad", "this is not valid syntax @!#\n");
124 let res = LfFilter::load(info(), path);
125 assert!(res.is_err(), "expected parse error");
126 }
127
128 #[test]
129 fn lf_filter_no_match_passes_through() {
130 let path = write_lf(
131 "nm",
132 r#"
133specific:
134 head 1
135"#,
136 );
137 let f = LfFilter::load(info(), path).unwrap();
138 let out = f
139 .filter(&make_input("a\nb\nc\n", "other", Level::Full))
140 .unwrap();
141 assert_eq!(out.text, "a\nb\nc\n");
142 }
143}