o7 0.1.1

O7 workflow DSL runner
Documentation
use std::fmt;

/// A parse error with source location.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
    pub file: String,
    pub line: usize,
    pub column: usize,
    pub message: String,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}:{}:{}: {}",
            self.file, self.line, self.column, self.message
        )
    }
}

impl std::error::Error for ParseError {}

impl ParseError {
    pub fn new(file: &str, line: usize, column: usize, message: impl Into<String>) -> Self {
        ParseError {
            file: file.to_string(),
            line,
            column,
            message: message.into(),
        }
    }
}

/// Severity level for lint diagnostics.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
    Error,
    Warning,
}

impl fmt::Display for DiagnosticSeverity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DiagnosticSeverity::Error => write!(f, "error"),
            DiagnosticSeverity::Warning => write!(f, "warning"),
        }
    }
}

/// A machine-readable lint diagnostic with severity and source location.
///
/// Formats as `file:line:col: severity: message` for tool-friendly output.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LintDiagnostic {
    pub file: String,
    pub line: usize,
    pub column: usize,
    pub severity: DiagnosticSeverity,
    pub message: String,
}

impl fmt::Display for LintDiagnostic {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}:{}:{}: {}: {}",
            self.file, self.line, self.column, self.severity, self.message
        )
    }
}

impl LintDiagnostic {
    pub fn error(file: &str, line: usize, column: usize, message: impl Into<String>) -> Self {
        LintDiagnostic {
            file: file.to_string(),
            line,
            column,
            severity: DiagnosticSeverity::Error,
            message: message.into(),
        }
    }

    pub fn warning(file: &str, line: usize, column: usize, message: impl Into<String>) -> Self {
        LintDiagnostic {
            file: file.to_string(),
            line,
            column,
            severity: DiagnosticSeverity::Warning,
            message: message.into(),
        }
    }
}

impl From<ParseError> for LintDiagnostic {
    fn from(err: ParseError) -> Self {
        LintDiagnostic {
            file: err.file,
            line: err.line,
            column: err.column,
            severity: DiagnosticSeverity::Error,
            message: err.message,
        }
    }
}

/// Format a list of parse errors as lint diagnostics (one per line).
pub fn format_lint_output(errors: &[ParseError]) -> String {
    errors
        .iter()
        .map(|e| e.to_string())
        .collect::<Vec<_>>()
        .join("\n")
}

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

    #[test]
    fn test_error_display_format() {
        let err = ParseError::new("test.7", 5, 3, "unexpected token");
        assert_eq!(format!("{}", err), "test.7:5:3: unexpected token");
    }

    #[test]
    fn test_error_new() {
        let err = ParseError::new("main.7", 10, 1, "missing colon");
        assert_eq!(err.file, "main.7");
        assert_eq!(err.line, 10);
        assert_eq!(err.column, 1);
        assert_eq!(err.message, "missing colon");
    }

    #[test]
    fn test_error_is_std_error() {
        let err = ParseError::new("test.7", 1, 1, "bad");
        let _: &dyn std::error::Error = &err;
    }

    #[test]
    fn test_lint_diagnostic_error() {
        let diag = LintDiagnostic::error("test.7", 5, 3, "unexpected token");
        assert_eq!(format!("{}", diag), "test.7:5:3: error: unexpected token");
    }

    #[test]
    fn test_lint_diagnostic_warning() {
        let diag = LintDiagnostic::warning("test.7", 12, 1, "unused variable");
        assert_eq!(format!("{}", diag), "test.7:12:1: warning: unused variable");
    }

    #[test]
    fn test_lint_diagnostic_from_parse_error() {
        let err = ParseError::new("test.7", 5, 3, "unexpected token");
        let diag: LintDiagnostic = err.into();
        assert_eq!(diag.severity, DiagnosticSeverity::Error);
        assert_eq!(format!("{}", diag), "test.7:5:3: error: unexpected token");
    }

    #[test]
    fn test_format_lint_output() {
        let errors = vec![
            ParseError::new("a.7", 1, 1, "first error"),
            ParseError::new("b.7", 2, 5, "second error"),
        ];
        let output = format_lint_output(&errors);
        assert_eq!(output, "a.7:1:1: first error\nb.7:2:5: second error");
    }

    #[test]
    fn test_format_lint_output_empty() {
        let errors: Vec<ParseError> = vec![];
        let output = format_lint_output(&errors);
        assert_eq!(output, "");
    }
}