use std::fmt::{Display, Formatter};
use camino::Utf8PathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Severity {
Info,
Warning,
Error,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SourceSpan {
pub line: usize,
pub column: usize,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticSource {
pub path: Box<str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub span: Option<SourceSpan>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Diagnostic {
pub code: Box<str>,
pub severity: Severity,
pub message: Box<str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Box<DiagnosticSource>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project: Option<Box<str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<Box<str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<Box<str>>,
}
impl Diagnostic {
pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into().into_boxed_str(),
severity: Severity::Error,
message: message.into().into_boxed_str(),
source: None,
project: None,
workspace: None,
help: None,
}
}
pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into().into_boxed_str(),
severity: Severity::Warning,
message: message.into().into_boxed_str(),
source: None,
project: None,
workspace: None,
help: None,
}
}
#[must_use]
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.source = Some(Box::new(DiagnosticSource {
path: path.into().into_boxed_str(),
span: None,
}));
self
}
#[must_use]
pub fn with_project(mut self, project: impl Into<String>) -> Self {
self.project = Some(project.into().into_boxed_str());
self
}
#[must_use]
pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
self.workspace = Some(workspace.into().into_boxed_str());
self
}
#[must_use]
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into().into_boxed_str());
self
}
}
impl Display for Diagnostic {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(source) = &self.source {
write!(
f,
"{} [{}] {}: {}",
self.code,
severity_label(&self.severity),
source.path,
self.message
)
} else {
write!(
f,
"{} [{}] {}",
self.code,
severity_label(&self.severity),
self.message
)
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidationReport {
pub diagnostics: Vec<Diagnostic>,
}
impl ValidationReport {
pub fn new(diagnostics: Vec<Diagnostic>) -> Self {
Self { diagnostics }
}
pub fn is_success(&self) -> bool {
!self
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == Severity::Error)
}
pub fn push(&mut self, diagnostic: Diagnostic) {
self.diagnostics.push(diagnostic);
}
}
#[derive(Debug, Error)]
pub enum RepoctlError {
#[error("{diagnostic}")]
Diagnostic {
diagnostic: Box<Diagnostic>,
},
#[error("repoctl produced {} diagnostics", diagnostics.len())]
Diagnostics {
diagnostics: Vec<Diagnostic>,
},
#[error("I/O error at {path}: {source}")]
Io {
path: Utf8PathBuf,
#[source]
source: std::io::Error,
},
#[error("environment error: {0}")]
Environment(String),
#[error("internal error: {0}")]
Internal(String),
}
impl RepoctlError {
pub fn diagnostic(diagnostic: Diagnostic) -> Self {
Self::Diagnostic {
diagnostic: Box::new(diagnostic),
}
}
pub fn io(path: impl Into<Utf8PathBuf>, source: std::io::Error) -> Self {
Self::Io {
path: path.into(),
source,
}
}
pub fn diagnostics(&self) -> Vec<Diagnostic> {
match self {
Self::Diagnostic { diagnostic } => vec![(**diagnostic).clone()],
Self::Diagnostics { diagnostics } => diagnostics.clone(),
Self::Io { path, source } => vec![Diagnostic::error(
"repoctl.io",
format!("I/O error at {path}: {source}"),
)],
Self::Environment(message) => {
vec![Diagnostic::error("repoctl.environment", message.clone())]
}
Self::Internal(message) => vec![Diagnostic::error("repoctl.internal", message.clone())],
}
}
}
fn severity_label(severity: &Severity) -> &'static str {
match severity {
Severity::Info => "info",
Severity::Warning => "warning",
Severity::Error => "error",
}
}