matchpick/
lib.rs

1use anyhow::{bail, Result};
2
3pub fn process(
4    utf8_data: &str,
5    match_against: Vec<String>,
6    enter_pattern: &str,
7    exit_pattern: &str,
8    ignore_pattern: Option<String>,
9) -> Result<String> {
10    let mut matcher = MultilineMatch::new(
11        match_against,
12        enter_pattern.to_owned(),
13        exit_pattern.to_owned(),
14        ignore_pattern,
15    );
16    let mut outputs: Vec<String> = Vec::new();
17    for (i, line) in utf8_data.lines().enumerate() {
18        if let Some(line) = matcher
19            .check_line(line)
20            .map_err(|e| anyhow::anyhow!("parse failed at line {}: {e}", i + 1))?
21        {
22            outputs.push(line);
23        };
24    }
25    Ok(outputs.join("\n"))
26}
27
28#[derive(Debug, Clone)]
29struct MultilineMatch {
30    enter_pattern: String,
31    exit_pattern: String,
32    ignore_pattern: Option<String>,
33    match_against: Vec<String>,
34    default_case_buffer: Vec<String>,
35    state: State,
36}
37
38impl MultilineMatch {
39    fn new(
40        match_against: Vec<String>,
41        enter_pattern: String,
42        exit_pattern: String,
43        ignore_pattern: Option<String>,
44    ) -> Self {
45        Self {
46            enter_pattern,
47            exit_pattern,
48            ignore_pattern,
49            match_against,
50            default_case_buffer: Vec::new(),
51            state: State::Normal,
52        }
53    }
54
55    fn check_line(&mut self, line: &str) -> Result<Option<String>> {
56        let output = match self.check_new_state(line) {
57            Some(new_state) => self.handle_new_state(new_state)?,
58            None => self.handle_normal_line(line),
59        };
60        Ok(output)
61    }
62
63    fn handle_normal_line(&mut self, line: &str) -> Option<String> {
64        match &self.state {
65            State::Normal | State::Matched => Some(line.to_owned()),
66            State::Default => {
67                self.default_case_buffer.push(line.to_owned());
68                None
69            }
70            State::Other | State::Done => None,
71        }
72    }
73
74    fn check_new_state(&self, line: &str) -> Option<NewState> {
75        if let Some(ignore_pattern) = &self.ignore_pattern {
76            if line.contains(ignore_pattern) {
77                return None;
78            }
79        }
80        if let Some((_pat, names)) = line.split_once(&self.enter_pattern) {
81            let names = names.trim();
82            if names.is_empty() {
83                Some(NewState::Enter)
84            } else {
85                let names = names
86                    .split_whitespace()
87                    .map(std::borrow::ToOwned::to_owned)
88                    .collect();
89                Some(NewState::Switch(names))
90            }
91        } else if line.contains(&self.exit_pattern) {
92            Some(NewState::Exit)
93        } else {
94            None
95        }
96    }
97
98    // Allow same arms for the sake of comments explaining each state change in the state machine
99    #[allow(clippy::match_same_arms)]
100    fn handle_new_state(&mut self, new_state: NewState) -> Result<Option<String>> {
101        let mut result_value = None;
102        self.state = match (&self.state, new_state) {
103            // Enter matching
104            (State::Normal, NewState::Enter) => State::Default,
105            // Entering new switch case (check if matched)
106            (State::Default | State::Other, NewState::Switch(names)) => {
107                if self.match_against.is_empty() {
108                    // Wanted default
109                    result_value = Some(self.default_case_buffer.join("\n"));
110                    State::Done
111                } else if self.match_against.iter().any(|m| names.contains(m)) {
112                    // Found case
113                    State::Matched
114                } else {
115                    // Switch case
116                    State::Other
117                }
118            }
119            // Leaving matched case for another (no further action needed)
120            (State::Matched, NewState::Switch(_)) => State::Done,
121            // Leaving switch case for another (already done)
122            (State::Done, NewState::Switch(_)) => State::Done,
123            // Exiting normally
124            (State::Matched | State::Done, NewState::Exit) => {
125                self.default_case_buffer.clear();
126                State::Normal
127            }
128            // Exiting without match (use default buffer)
129            (State::Other, NewState::Exit) => {
130                result_value = Some(self.default_case_buffer.join("\n"));
131                self.default_case_buffer.clear();
132                State::Normal
133            }
134            // Invalid state changes
135            (State::Normal, NewState::Switch(_)) => {
136                bail!("cannot start new case: need default first")
137            }
138            (State::Normal, NewState::Exit) => bail!("cannot end match: not in match"),
139            (State::Default, NewState::Enter) => {
140                bail!("cannot start new match: in default of previous match")
141            }
142            (State::Default, NewState::Exit) => bail!("ended match without alternatives"),
143            (State::Other, NewState::Enter) => {
144                bail!("cannot start new match: switching previous match")
145            }
146            (State::Matched, NewState::Enter) => {
147                bail!("cannot start new match: in matched case of previous match")
148            }
149            (State::Done, NewState::Enter) => {
150                bail!("cannot start new match: no exit of previous match")
151            }
152        };
153        Ok(result_value)
154    }
155}
156
157#[derive(Debug, Clone, Copy)]
158enum State {
159    // Not matching
160    Normal,
161    // Buffering the default case
162    Default,
163    // In the matched case
164    Matched,
165    // In another case
166    Other,
167    // Post-match, no further action required
168    Done,
169}
170
171#[derive(Debug)]
172enum NewState {
173    // Start new match
174    Enter,
175    // New switch case
176    Switch(Vec<String>),
177    // Finish match
178    Exit,
179}