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