merman-analysis 0.8.0-alpha.3

Diagnostics-first analysis contracts and source mapping for Merman.
Documentation
use serde::{Deserialize, Serialize};

pub const ANALYSIS_PAYLOAD_VERSION: u32 = 1;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceKind {
    Diagram,
    Markdown,
    Mdx,
}

impl SourceKind {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Diagram => "diagram",
            Self::Markdown => "markdown",
            Self::Mdx => "mdx",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceDescriptor {
    pub kind: SourceKind,
    pub path: Option<String>,
    pub diagram_index: Option<usize>,
    pub language: String,
}

impl SourceDescriptor {
    pub fn diagram() -> Self {
        Self {
            kind: SourceKind::Diagram,
            path: None,
            diagram_index: None,
            language: "mermaid".to_string(),
        }
    }

    pub fn with_path(mut self, path: impl Into<String>) -> Self {
        self.path = Some(path.into());
        self
    }

    pub fn with_diagram_index(mut self, diagram_index: usize) -> Self {
        self.diagram_index = Some(diagram_index);
        self
    }

    pub fn with_language(mut self, language: impl Into<String>) -> Self {
        self.language = language.into();
        self
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticSeverity {
    Error,
    Warning,
    Info,
    Hint,
}

impl DiagnosticSeverity {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Error => "error",
            Self::Warning => "warning",
            Self::Info => "info",
            Self::Hint => "hint",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticCategory {
    Parse,
    Semantic,
    Config,
    Resource,
    Compatibility,
    Layout,
    Render,
    Internal,
}

impl DiagnosticCategory {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Parse => "parse",
            Self::Semantic => "semantic",
            Self::Config => "config",
            Self::Resource => "resource",
            Self::Compatibility => "compatibility",
            Self::Layout => "layout",
            Self::Render => "render",
            Self::Internal => "internal",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Utf16Position {
    pub line: usize,
    pub character: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct LspRange {
    pub start: Utf16Position,
    pub end: Utf16Position,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiagnosticSpan {
    pub byte_start: usize,
    pub byte_end: usize,
    pub line: usize,
    pub column: usize,
    pub end_line: usize,
    pub end_column: usize,
    pub lsp_range: LspRange,
}

impl DiagnosticSpan {
    pub const fn new(
        byte_start: usize,
        byte_end: usize,
        line: usize,
        column: usize,
        end_line: usize,
        end_column: usize,
        lsp_start: Utf16Position,
        lsp_end: Utf16Position,
    ) -> Self {
        Self {
            byte_start,
            byte_end,
            line,
            column,
            end_line,
            end_column,
            lsp_range: LspRange {
                start: lsp_start,
                end: lsp_end,
            },
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiagnosticRelated {
    pub message: String,
    pub span: Option<DiagnosticSpan>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiagnosticFixEdit {
    pub span: DiagnosticSpan,
    pub replacement: String,
}

impl DiagnosticFixEdit {
    pub fn new(span: DiagnosticSpan, replacement: impl Into<String>) -> Self {
        Self {
            span,
            replacement: replacement.into(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiagnosticFix {
    pub title: String,
    pub edits: Vec<DiagnosticFixEdit>,
    #[serde(default, skip_serializing_if = "is_false")]
    pub is_preferred: bool,
}

impl DiagnosticFix {
    pub fn new(title: impl Into<String>, edits: Vec<DiagnosticFixEdit>) -> Self {
        Self {
            title: title.into(),
            edits,
            is_preferred: false,
        }
    }

    pub fn preferred(mut self) -> Self {
        self.is_preferred = true;
        self
    }
}

const fn is_false(value: &bool) -> bool {
    !*value
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnalysisDiagnostic {
    pub id: String,
    pub severity: DiagnosticSeverity,
    pub category: DiagnosticCategory,
    pub message: String,
    pub code: Option<i32>,
    pub code_name: Option<String>,
    pub diagram_type: Option<String>,
    pub span: Option<DiagnosticSpan>,
    #[serde(default)]
    pub related: Vec<DiagnosticRelated>,
    pub help: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub fixes: Vec<DiagnosticFix>,
}

impl AnalysisDiagnostic {
    pub fn new(
        id: impl Into<String>,
        severity: DiagnosticSeverity,
        category: DiagnosticCategory,
        message: impl Into<String>,
    ) -> Self {
        Self {
            id: id.into(),
            severity,
            category,
            message: message.into(),
            code: None,
            code_name: None,
            diagram_type: None,
            span: None,
            related: Vec::new(),
            help: None,
            fixes: Vec::new(),
        }
    }

    pub fn error(
        id: impl Into<String>,
        category: DiagnosticCategory,
        message: impl Into<String>,
    ) -> Self {
        Self::new(id, DiagnosticSeverity::Error, category, message)
    }

    pub fn with_code(mut self, code: i32, code_name: impl Into<String>) -> Self {
        self.code = Some(code);
        self.code_name = Some(code_name.into());
        self
    }

    pub fn with_diagram_type(mut self, diagram_type: impl Into<String>) -> Self {
        self.diagram_type = Some(diagram_type.into());
        self
    }

    pub const fn with_span(mut self, span: DiagnosticSpan) -> Self {
        self.span = Some(span);
        self
    }

    pub fn with_help(mut self, help: impl Into<String>) -> Self {
        self.help = Some(help.into());
        self
    }

    pub fn with_fix(mut self, fix: DiagnosticFix) -> Self {
        self.fixes.push(fix);
        self
    }

    pub fn with_fixes(mut self, fixes: impl IntoIterator<Item = DiagnosticFix>) -> Self {
        self.fixes.extend(fixes);
        self
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Summary {
    pub errors: usize,
    pub warnings: usize,
    pub infos: usize,
    pub hints: usize,
}

impl Summary {
    pub fn from_diagnostics(diagnostics: &[AnalysisDiagnostic]) -> Self {
        diagnostics
            .iter()
            .fold(Self::default(), |mut summary, diagnostic| {
                match diagnostic.severity {
                    DiagnosticSeverity::Error => summary.errors += 1,
                    DiagnosticSeverity::Warning => summary.warnings += 1,
                    DiagnosticSeverity::Info => summary.infos += 1,
                    DiagnosticSeverity::Hint => summary.hints += 1,
                }
                summary
            })
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnalysisPayload {
    pub version: u32,
    pub valid: bool,
    pub summary: Summary,
    pub source: SourceDescriptor,
    pub diagnostics: Vec<AnalysisDiagnostic>,
}

impl AnalysisPayload {
    pub fn new(source: SourceDescriptor, diagnostics: Vec<AnalysisDiagnostic>) -> Self {
        let summary = Summary::from_diagnostics(&diagnostics);
        Self {
            version: ANALYSIS_PAYLOAD_VERSION,
            valid: summary.errors == 0,
            summary,
            source,
            diagnostics,
        }
    }

    pub fn valid(source: SourceDescriptor) -> Self {
        Self::new(source, Vec::new())
    }

    pub fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
        serde_json::to_vec(self)
    }

    pub fn to_pretty_json_string(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(self)
    }
}