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}