Skip to main content

rgx/config/
workspace.rs

1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5use crate::app::App;
6use crate::engine::{self, EngineFlags, EngineKind};
7
8#[derive(Serialize, Deserialize)]
9pub struct Workspace {
10    pub pattern: String,
11    pub test_string: String,
12    pub replacement: String,
13    pub engine: String,
14    pub case_insensitive: bool,
15    pub multiline: bool,
16    pub dotall: bool,
17    pub unicode: bool,
18    pub extended: bool,
19    pub show_whitespace: bool,
20    #[serde(default, skip_serializing_if = "Vec::is_empty")]
21    pub tests: Vec<TestCase>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TestCase {
26    pub input: String,
27    pub should_match: bool,
28}
29
30#[derive(Debug)]
31pub struct TestResult {
32    pub input: String,
33    pub should_match: bool,
34    pub did_match: bool,
35}
36
37impl TestResult {
38    pub fn passed(&self) -> bool {
39        self.did_match == self.should_match
40    }
41}
42
43impl Workspace {
44    fn engine_kind(&self) -> EngineKind {
45        match self.engine.as_str() {
46            "fancy" => EngineKind::FancyRegex,
47            #[cfg(feature = "pcre2-engine")]
48            "pcre2" => EngineKind::Pcre2,
49            _ => EngineKind::RustRegex,
50        }
51    }
52
53    fn flags(&self) -> EngineFlags {
54        EngineFlags {
55            case_insensitive: self.case_insensitive,
56            multi_line: self.multiline,
57            dot_matches_newline: self.dotall,
58            unicode: self.unicode,
59            extended: self.extended,
60        }
61    }
62
63    pub fn from_app(app: &App) -> Self {
64        let engine = match app.engine_kind {
65            EngineKind::RustRegex => "rust",
66            EngineKind::FancyRegex => "fancy",
67            #[cfg(feature = "pcre2-engine")]
68            EngineKind::Pcre2 => "pcre2",
69        };
70        Self {
71            pattern: app.regex_editor.content().to_string(),
72            test_string: app.test_editor.content().to_string(),
73            replacement: app.replace_editor.content().to_string(),
74            engine: engine.to_string(),
75            case_insensitive: app.flags.case_insensitive,
76            multiline: app.flags.multi_line,
77            dotall: app.flags.dot_matches_newline,
78            unicode: app.flags.unicode,
79            extended: app.flags.extended,
80            show_whitespace: app.show_whitespace,
81            tests: Vec::new(),
82        }
83    }
84
85    pub fn apply(&self, app: &mut App) {
86        let engine_kind = self.engine_kind();
87        if app.engine_kind != engine_kind {
88            app.engine_kind = engine_kind;
89            app.switch_engine_to(engine_kind);
90        }
91        app.flags = self.flags();
92        app.show_whitespace = self.show_whitespace;
93        app.set_test_string(&self.test_string);
94        if !self.replacement.is_empty() {
95            app.set_replacement(&self.replacement);
96        }
97        app.set_pattern(&self.pattern);
98    }
99
100    pub fn save(&self, path: &Path) -> anyhow::Result<()> {
101        let content = toml::to_string_pretty(self)?;
102        std::fs::write(path, content)?;
103        Ok(())
104    }
105
106    pub fn load(path: &Path) -> anyhow::Result<Self> {
107        let content = std::fs::read_to_string(path)?;
108        let ws: Self = toml::from_str(&content)?;
109        Ok(ws)
110    }
111
112    /// Run test assertions and return results.
113    pub fn run_tests(&self) -> anyhow::Result<Vec<TestResult>> {
114        let eng = engine::create_engine(self.engine_kind());
115        let compiled = eng
116            .compile(&self.pattern, &self.flags())
117            .map_err(|e| anyhow::anyhow!("{e}"))?;
118
119        let mut results = Vec::with_capacity(self.tests.len());
120        for tc in &self.tests {
121            let did_match = match compiled.find_matches(&tc.input) {
122                Ok(m) => !m.is_empty(),
123                Err(_) => false,
124            };
125            results.push(TestResult {
126                input: tc.input.clone(),
127                should_match: tc.should_match,
128                did_match,
129            });
130        }
131        Ok(results)
132    }
133}
134
135use crate::ansi::{BOLD, GREEN, RED, RESET};
136
137/// Print test results to stdout. Returns true if all passed.
138pub fn print_test_results(path: &str, pattern: &str, results: &[TestResult], color: bool) -> bool {
139    let total = results.len();
140    let passed = results.iter().filter(|r| r.passed()).count();
141    let failed = total - passed;
142
143    if color {
144        println!("{BOLD}Testing:{RESET} {path}");
145        println!("{BOLD}Pattern:{RESET} {pattern}");
146    } else {
147        println!("Testing: {path}");
148        println!("Pattern: {pattern}");
149    }
150    println!();
151
152    for (i, r) in results.iter().enumerate() {
153        let status = if r.passed() {
154            if color {
155                format!("{GREEN}PASS{RESET}")
156            } else {
157                "PASS".to_string()
158            }
159        } else if color {
160            format!("{RED}FAIL{RESET}")
161        } else {
162            "FAIL".to_string()
163        };
164        let expect = if r.should_match { "match" } else { "no match" };
165        let got = if r.did_match { "matched" } else { "no match" };
166        println!(
167            "  {status} [{:>2}] {:?} (expect {expect}, got {got})",
168            i + 1,
169            r.input
170        );
171    }
172
173    println!();
174    if failed == 0 {
175        if color {
176            println!("{GREEN}{BOLD}{passed}/{total} passed{RESET}");
177        } else {
178            println!("{passed}/{total} passed");
179        }
180    } else if color {
181        println!("{RED}{BOLD}{failed}/{total} failed{RESET}");
182    } else {
183        println!("{failed}/{total} failed");
184    }
185
186    failed == 0
187}