oo-ide 0.0.4

∞ is a terminal IDE focused on low distraction, high usability.
Documentation
//! Serde-Deserialize types for the `log_matchers` section of `extension.yaml`.
//!
//! These types are **input-only**: they mirror the YAML structure and are fed
//! directly to the compiler. The compiler validates and transforms them into
//! [`crate::log_matcher::types::CompiledMatcher`].

use serde::Deserialize;

fn default_schema_version() -> u32 {
    1
}

// ---------------------------------------------------------------------------
// LogMatcherDef
// ---------------------------------------------------------------------------

/// Top-level log matcher definition from `extension.yaml`.
#[derive(Debug, Clone, Deserialize)]
pub struct LogMatcherDef {
    /// Unique matcher id, e.g. `"rust.cargo.error"`.
    pub id: String,
    /// Task source string, e.g. `"cargo"`.
    pub source: String,
    /// Dispatch priority; higher values take precedence. Default: `0`.
    #[serde(default)]
    pub priority: u32,
    /// Schema version for forward-compatibility. Default: `1`.
    #[serde(default = "default_schema_version")]
    pub schema_version: u32,
    /// Pattern that marks the start of a new match block.
    pub start: StartDef,
    /// Optional body capture rules applied after `start`.
    #[serde(default)]
    pub body: Vec<BodyRuleDef>,
    /// Safety cap: emit after accumulating this many lines regardless of end condition.
    #[serde(default)]
    pub max_lines: Option<u32>,
    /// Condition that terminates a match block.
    pub end: EndDef,
    /// Template describing the diagnostic to emit.
    pub emit: EmitDef,
}

// ---------------------------------------------------------------------------
// StartDef
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
pub struct StartDef {
    /// Regular expression string. Named captures become template variables.
    #[serde(rename = "match")]
    pub pattern: String,
}

// ---------------------------------------------------------------------------
// BodyRuleDef
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
pub struct BodyRuleDef {
    /// Regular expression string for this body line.
    #[serde(rename = "match")]
    pub pattern: String,
    /// If `true`, this rule may be skipped entirely. Default: `false`.
    #[serde(default)]
    pub optional: bool,
    /// If `true`, this rule may match zero or more consecutive lines. Default: `false`.
    #[serde(default)]
    pub repeat: bool,
}

// ---------------------------------------------------------------------------
// EndDef
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
pub struct EndDef {
    /// `"next_start"` or `"blank_line"`.
    pub condition: String,
}

// ---------------------------------------------------------------------------
// EmitDef
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
pub struct EmitDef {
    /// Diagnostic severity: `"error"`, `"warning"`, `"info"`, or `"hint"`.
    pub severity: String,
    /// Template for the primary diagnostic message (required, must be non-empty).
    pub message: String,
    /// Template for the file path (optional).
    #[serde(default)]
    pub file: Option<String>,
    /// Template for the line number (optional).
    #[serde(default)]
    pub line: Option<String>,
    /// Template for the column number (optional).
    #[serde(default)]
    pub column: Option<String>,
    /// Template for the diagnostic code (optional).
    #[serde(default)]
    pub code: Option<String>,
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    fn parse(yaml: &str) -> LogMatcherDef {
        serde_saphyr::from_str(yaml).expect("should parse")
    }

    #[test]
    fn parse_minimal_matcher() {
        let yaml = r#"
id: rust.cargo.error
source: cargo
start:
  match: '^error'
end:
  condition: next_start
emit:
  severity: error
  message: "{{ message }}"
"#;
        let def = parse(yaml);
        assert_eq!(def.id, "rust.cargo.error");
        assert_eq!(def.source, "cargo");
        assert_eq!(def.priority, 0);
        assert_eq!(def.schema_version, 1);
        assert_eq!(def.start.pattern, "^error");
        assert!(def.body.is_empty());
        assert!(def.max_lines.is_none());
        assert_eq!(def.end.condition, "next_start");
        assert_eq!(def.emit.severity, "error");
        assert_eq!(def.emit.message, "{{ message }}");
    }

    #[test]
    fn parse_with_body_rules() {
        let yaml = r#"
id: my.matcher
source: test
start:
  match: '^start'
body:
  - match: '^\s+\|'
    repeat: true
    optional: true
end:
  condition: blank_line
emit:
  severity: warning
  message: "found"
"#;
        let def = parse(yaml);
        assert_eq!(def.body.len(), 1);
        assert!(def.body[0].repeat);
        assert!(def.body[0].optional);
    }

    #[test]
    fn parse_all_emit_fields() {
        let yaml = r#"
id: full.emit
source: test
start:
  match: '^e'
end:
  condition: next_start
emit:
  severity: error
  message: "{{ msg }}"
  file: "{{ file }}"
  line: "{{ line }}"
  column: "{{ col }}"
  code: "E{{ code }}"
"#;
        let def = parse(yaml);
        assert!(def.emit.file.is_some());
        assert!(def.emit.line.is_some());
        assert!(def.emit.column.is_some());
        assert!(def.emit.code.is_some());
    }

    #[test]
    fn parse_max_lines() {
        let yaml = r#"
id: capped
source: test
start:
  match: '^e'
max_lines: 50
end:
  condition: next_start
emit:
  severity: error
  message: "err"
"#;
        let def = parse(yaml);
        assert_eq!(def.max_lines, Some(50));
    }
}