Skip to main content

double_o/pattern/
toml.rs

1use regex::Regex;
2use serde::Deserialize;
3use std::path::Path;
4
5use super::{FailurePattern, FailureStrategy, Pattern, SuccessPattern};
6use crate::error::Error;
7
8// ---------------------------------------------------------------------------
9// TOML deserialization types
10// ---------------------------------------------------------------------------
11
12/// TOML representation of a pattern file.
13///
14/// This struct deserializes from user-defined TOML pattern files
15/// loaded from `~/.config/oo/patterns/`. Each file defines a single pattern
16/// with optional success and failure configurations.
17#[derive(Deserialize)]
18pub struct PatternFile {
19    /// Regex that matches the command line.
20    pub command_match: String,
21
22    /// Optional success pattern configuration.
23    pub success: Option<SuccessSection>,
24
25    /// Optional failure pattern configuration.
26    pub failure: Option<FailureSection>,
27}
28
29#[derive(Deserialize)]
30pub struct SuccessSection {
31    /// Regex pattern with named capture groups.
32    pub pattern: String,
33
34    /// Summary template with {name} placeholders.
35    pub summary: String,
36}
37
38/// TOML configuration for failure output filtering.
39///
40/// Defines how to extract relevant error information from failed command output.
41/// Multiple strategies are supported: tail, head, grep, and between.
42#[derive(Deserialize)]
43pub struct FailureSection {
44    /// Strategy name: "tail", "head", "grep", or "between".
45    pub(crate) strategy: Option<String>,
46
47    /// Number of lines (for tail/head strategies).
48    pub(crate) lines: Option<usize>,
49
50    /// Grep pattern (for grep strategy).
51    #[serde(rename = "grep")]
52    pub(crate) grep_pattern: Option<String>,
53
54    /// Start delimiter (for between strategy).
55    pub(crate) start: Option<String>,
56
57    /// End delimiter (for between strategy).
58    pub(crate) end: Option<String>,
59}
60
61// ---------------------------------------------------------------------------
62// User patterns (TOML on disk)
63// ---------------------------------------------------------------------------
64
65/// Load user-defined patterns from a directory of TOML files.
66///
67/// Invalid files are silently skipped.
68pub fn load_user_patterns(dir: &Path) -> Vec<Pattern> {
69    let entries = match std::fs::read_dir(dir) {
70        Ok(e) => e,
71        Err(_) => return Vec::new(),
72    };
73
74    let mut patterns = Vec::new();
75    for entry in entries.flatten() {
76        let path = entry.path();
77        if path.extension().is_some_and(|e| e == "toml") {
78            if let Ok(p) = load_pattern_file(&path) {
79                patterns.push(p);
80            }
81        }
82    }
83    patterns
84}
85
86fn load_pattern_file(path: &Path) -> Result<Pattern, Error> {
87    let content =
88        std::fs::read_to_string(path).map_err(|e| Error::Pattern(format!("{path:?}: {e}")))?;
89    parse_pattern_str(&content)
90}
91
92/// Parse a pattern definition from TOML string content.
93///
94/// Deserializes a TOML pattern definition into a `Pattern` struct,
95/// validating regex patterns and strategy configurations.
96///
97/// # Arguments
98///
99/// * `content` - TOML-formatted pattern definition
100///
101/// # Returns
102///
103/// A `Pattern` struct if parsing and validation succeed, or an `Error`
104/// if TOML is malformed, regex is invalid, or strategy configuration is incomplete.
105///
106/// # Errors
107///
108/// Returns `Error::Pattern` for:
109/// - TOML parsing failures
110/// - Invalid regular expressions
111/// - Missing required fields (e.g., grep pattern for grep strategy)
112/// - Unknown strategy names
113///
114/// # Examples
115///
116/// ```
117/// use double_o::pattern::parse_pattern_str;
118///
119/// let toml = r#"
120/// command_match = "myapp test"
121///
122/// [success]
123/// pattern = "(?P<passed>\\d+) passed"
124/// summary = "{passed} tests passed"
125/// "#;
126/// let pattern = parse_pattern_str(toml).unwrap();
127/// ```
128pub fn parse_pattern_str(content: &str) -> Result<Pattern, Error> {
129    let pf: PatternFile =
130        toml::from_str(content).map_err(|e| Error::Pattern(format!("TOML parse: {e}")))?;
131
132    let command_match =
133        Regex::new(&pf.command_match).map_err(|e| Error::Pattern(format!("regex: {e}")))?;
134
135    let success = pf
136        .success
137        .map(|s| -> Result<SuccessPattern, Error> {
138            let pattern =
139                Regex::new(&s.pattern).map_err(|e| Error::Pattern(format!("regex: {e}")))?;
140            Ok(SuccessPattern {
141                pattern,
142                summary: s.summary,
143            })
144        })
145        .transpose()?;
146
147    let failure = pf
148        .failure
149        .map(|f| -> Result<FailurePattern, Error> {
150            let strategy = match f.strategy.as_deref().unwrap_or("tail") {
151                "tail" => FailureStrategy::Tail {
152                    lines: f.lines.unwrap_or(30),
153                },
154                "head" => FailureStrategy::Head {
155                    lines: f.lines.unwrap_or(20),
156                },
157                "grep" => {
158                    let pat = f.grep_pattern.ok_or_else(|| {
159                        Error::Pattern("grep strategy requires 'grep' field".into())
160                    })?;
161                    let pattern =
162                        Regex::new(&pat).map_err(|e| Error::Pattern(format!("regex: {e}")))?;
163                    FailureStrategy::Grep { pattern }
164                }
165                "between" => {
166                    let start = f.start.ok_or_else(|| {
167                        Error::Pattern("between strategy requires 'start'".into())
168                    })?;
169                    let end = f
170                        .end
171                        .ok_or_else(|| Error::Pattern("between strategy requires 'end'".into()))?;
172                    FailureStrategy::Between { start, end }
173                }
174                other => {
175                    return Err(Error::Pattern(format!("unknown strategy: {other}")));
176                }
177            };
178            Ok(FailurePattern { strategy })
179        })
180        .transpose()?;
181
182    Ok(Pattern {
183        command_match,
184        success,
185        failure,
186    })
187}