Skip to main content

oo_ide/log_matcher/
schema.rs

1//! Serde-Deserialize types for the `log_matchers` section of `extension.yaml`.
2//!
3//! These types are **input-only**: they mirror the YAML structure and are fed
4//! directly to the compiler. The compiler validates and transforms them into
5//! [`crate::log_matcher::types::CompiledMatcher`].
6
7use serde::Deserialize;
8
9fn default_schema_version() -> u32 {
10    1
11}
12
13// ---------------------------------------------------------------------------
14// LogMatcherDef
15// ---------------------------------------------------------------------------
16
17/// Top-level log matcher definition from `extension.yaml`.
18#[derive(Debug, Clone, Deserialize)]
19pub struct LogMatcherDef {
20    /// Unique matcher id, e.g. `"rust.cargo.error"`.
21    pub id: String,
22    /// Task source string, e.g. `"cargo"`.
23    pub source: String,
24    /// Dispatch priority; higher values take precedence. Default: `0`.
25    #[serde(default)]
26    pub priority: u32,
27    /// Schema version for forward-compatibility. Default: `1`.
28    #[serde(default = "default_schema_version")]
29    pub schema_version: u32,
30    /// Pattern that marks the start of a new match block.
31    pub start: StartDef,
32    /// Optional body capture rules applied after `start`.
33    #[serde(default)]
34    pub body: Vec<BodyRuleDef>,
35    /// Safety cap: emit after accumulating this many lines regardless of end condition.
36    #[serde(default)]
37    pub max_lines: Option<u32>,
38    /// Condition that terminates a match block.
39    pub end: EndDef,
40    /// Template describing the diagnostic to emit.
41    pub emit: EmitDef,
42}
43
44// ---------------------------------------------------------------------------
45// StartDef
46// ---------------------------------------------------------------------------
47
48#[derive(Debug, Clone, Deserialize)]
49pub struct StartDef {
50    /// Regular expression string. Named captures become template variables.
51    #[serde(rename = "match")]
52    pub pattern: String,
53}
54
55// ---------------------------------------------------------------------------
56// BodyRuleDef
57// ---------------------------------------------------------------------------
58
59#[derive(Debug, Clone, Deserialize)]
60pub struct BodyRuleDef {
61    /// Regular expression string for this body line.
62    #[serde(rename = "match")]
63    pub pattern: String,
64    /// If `true`, this rule may be skipped entirely. Default: `false`.
65    #[serde(default)]
66    pub optional: bool,
67    /// If `true`, this rule may match zero or more consecutive lines. Default: `false`.
68    #[serde(default)]
69    pub repeat: bool,
70}
71
72// ---------------------------------------------------------------------------
73// EndDef
74// ---------------------------------------------------------------------------
75
76#[derive(Debug, Clone, Deserialize)]
77pub struct EndDef {
78    /// `"next_start"` or `"blank_line"`.
79    pub condition: String,
80}
81
82// ---------------------------------------------------------------------------
83// EmitDef
84// ---------------------------------------------------------------------------
85
86#[derive(Debug, Clone, Deserialize)]
87pub struct EmitDef {
88    /// Diagnostic severity: `"error"`, `"warning"`, `"info"`, or `"hint"`.
89    pub severity: String,
90    /// Template for the primary diagnostic message (required, must be non-empty).
91    pub message: String,
92    /// Template for the file path (optional).
93    #[serde(default)]
94    pub file: Option<String>,
95    /// Template for the line number (optional).
96    #[serde(default)]
97    pub line: Option<String>,
98    /// Template for the column number (optional).
99    #[serde(default)]
100    pub column: Option<String>,
101    /// Template for the diagnostic code (optional).
102    #[serde(default)]
103    pub code: Option<String>,
104}
105
106// ---------------------------------------------------------------------------
107// Tests
108// ---------------------------------------------------------------------------
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    fn parse(yaml: &str) -> LogMatcherDef {
115        serde_saphyr::from_str(yaml).expect("should parse")
116    }
117
118    #[test]
119    fn parse_minimal_matcher() {
120        let yaml = r#"
121id: rust.cargo.error
122source: cargo
123start:
124  match: '^error'
125end:
126  condition: next_start
127emit:
128  severity: error
129  message: "{{ message }}"
130"#;
131        let def = parse(yaml);
132        assert_eq!(def.id, "rust.cargo.error");
133        assert_eq!(def.source, "cargo");
134        assert_eq!(def.priority, 0);
135        assert_eq!(def.schema_version, 1);
136        assert_eq!(def.start.pattern, "^error");
137        assert!(def.body.is_empty());
138        assert!(def.max_lines.is_none());
139        assert_eq!(def.end.condition, "next_start");
140        assert_eq!(def.emit.severity, "error");
141        assert_eq!(def.emit.message, "{{ message }}");
142    }
143
144    #[test]
145    fn parse_with_body_rules() {
146        let yaml = r#"
147id: my.matcher
148source: test
149start:
150  match: '^start'
151body:
152  - match: '^\s+\|'
153    repeat: true
154    optional: true
155end:
156  condition: blank_line
157emit:
158  severity: warning
159  message: "found"
160"#;
161        let def = parse(yaml);
162        assert_eq!(def.body.len(), 1);
163        assert!(def.body[0].repeat);
164        assert!(def.body[0].optional);
165    }
166
167    #[test]
168    fn parse_all_emit_fields() {
169        let yaml = r#"
170id: full.emit
171source: test
172start:
173  match: '^e'
174end:
175  condition: next_start
176emit:
177  severity: error
178  message: "{{ msg }}"
179  file: "{{ file }}"
180  line: "{{ line }}"
181  column: "{{ col }}"
182  code: "E{{ code }}"
183"#;
184        let def = parse(yaml);
185        assert!(def.emit.file.is_some());
186        assert!(def.emit.line.is_some());
187        assert!(def.emit.column.is_some());
188        assert!(def.emit.code.is_some());
189    }
190
191    #[test]
192    fn parse_max_lines() {
193        let yaml = r#"
194id: capped
195source: test
196start:
197  match: '^e'
198max_lines: 50
199end:
200  condition: next_start
201emit:
202  severity: error
203  message: "err"
204"#;
205        let def = parse(yaml);
206        assert_eq!(def.max_lines, Some(50));
207    }
208}