use serde::{Deserialize, Serialize};
use super::{DiagnosticStage, Severity};
use crate::diagnostics::ValidationReport;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct Diagnostic {
pub id: String,
pub severity: Severity,
pub stage: DiagnosticStage,
pub category: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub object_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remediation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_location: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub related_diagnostics: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<std::collections::BTreeMap<String, String>>,
}
impl Diagnostic {
pub fn error(
id: impl Into<String>,
category: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
id: id.into(),
severity: Severity::Error,
stage: DiagnosticStage::Validation,
category: category.into(),
message: message.into(),
object_ref: None,
remediation: None,
source_location: None,
related_diagnostics: Vec::new(),
metadata: None,
}
}
pub fn parse_error(id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
id: id.into(),
severity: Severity::Error,
stage: DiagnosticStage::Parse,
category: crate::diagnostics::categories::SYNTAX.to_owned(),
message: message.into(),
object_ref: None,
remediation: None,
source_location: None,
related_diagnostics: Vec::new(),
metadata: None,
}
}
pub fn warning(
id: impl Into<String>,
category: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
id: id.into(),
severity: Severity::Warning,
stage: DiagnosticStage::Validation,
category: category.into(),
message: message.into(),
object_ref: None,
remediation: None,
source_location: None,
related_diagnostics: Vec::new(),
metadata: None,
}
}
pub fn information(
id: impl Into<String>,
category: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
id: id.into(),
severity: Severity::Information,
stage: DiagnosticStage::Validation,
category: category.into(),
message: message.into(),
object_ref: None,
remediation: None,
source_location: None,
related_diagnostics: Vec::new(),
metadata: None,
}
}
pub fn planning_error(
id: impl Into<String>,
category: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
id: id.into(),
severity: Severity::Error,
stage: DiagnosticStage::Planning,
category: category.into(),
message: message.into(),
object_ref: None,
remediation: None,
source_location: None,
related_diagnostics: Vec::new(),
metadata: None,
}
}
pub fn capability_error(
id: impl Into<String>,
category: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
id: id.into(),
severity: Severity::Error,
stage: DiagnosticStage::CapabilityEvaluation,
category: category.into(),
message: message.into(),
object_ref: None,
remediation: None,
source_location: None,
related_diagnostics: Vec::new(),
metadata: None,
}
}
pub fn capability_warning(
id: impl Into<String>,
category: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
id: id.into(),
severity: Severity::Warning,
stage: DiagnosticStage::CapabilityEvaluation,
category: category.into(),
message: message.into(),
object_ref: None,
remediation: None,
source_location: None,
related_diagnostics: Vec::new(),
metadata: None,
}
}
pub fn binding_error(
id: impl Into<String>,
category: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
id: id.into(),
severity: Severity::Error,
stage: DiagnosticStage::OrchestratorBinding,
category: category.into(),
message: message.into(),
object_ref: None,
remediation: None,
source_location: None,
related_diagnostics: Vec::new(),
metadata: None,
}
}
pub fn compatibility_error(
id: impl Into<String>,
category: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
id: id.into(),
severity: Severity::Error,
stage: DiagnosticStage::CompatibilityAnalysis,
category: category.into(),
message: message.into(),
object_ref: None,
remediation: None,
source_location: None,
related_diagnostics: Vec::new(),
metadata: None,
}
}
pub fn compatibility_warning(
id: impl Into<String>,
category: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
id: id.into(),
severity: Severity::Warning,
stage: DiagnosticStage::CompatibilityAnalysis,
category: category.into(),
message: message.into(),
object_ref: None,
remediation: None,
source_location: None,
related_diagnostics: Vec::new(),
metadata: None,
}
}
pub fn with_object_ref(mut self, object_ref: impl Into<String>) -> Self {
self.object_ref = Some(object_ref.into());
self
}
pub fn with_remediation(mut self, remediation: impl Into<String>) -> Self {
self.remediation = Some(remediation.into());
self
}
pub fn with_source_location(mut self, source_location: impl Into<String>) -> Self {
self.source_location = Some(source_location.into());
self
}
pub fn with_related(mut self, related: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.related_diagnostics = related.into_iter().map(Into::into).collect();
self
}
}
pub fn validate_diagnostic(diagnostic: &Diagnostic) -> ValidationReport {
let mut report = ValidationReport::new();
if diagnostic.id.trim().is_empty() {
report.push(
Diagnostic::error(
"DPCS-DIAG-001",
crate::diagnostics::categories::DOCUMENT,
"diagnostic identifier must not be empty",
)
.with_remediation("Provide a stable diagnostic id such as `DPCS-…`"),
);
} else if !diagnostic.id.starts_with("DPCS-") {
report.push(
Diagnostic::warning(
"DPCS-DIAG-002",
crate::diagnostics::categories::DOCUMENT,
format!(
"diagnostic identifier `{}` does not use the DPCS- prefix",
diagnostic.id
),
)
.with_object_ref(&diagnostic.id)
.with_remediation("Prefer identifiers of the form `DPCS-<AREA>-<NNN>`"),
);
}
if diagnostic.category.trim().is_empty() {
report.push(
Diagnostic::error(
"DPCS-DIAG-003",
crate::diagnostics::categories::DOCUMENT,
"diagnostic category must not be empty",
)
.with_object_ref(&diagnostic.id),
);
}
if diagnostic.message.trim().is_empty() {
report.push(
Diagnostic::error(
"DPCS-DIAG-004",
crate::diagnostics::categories::DOCUMENT,
"diagnostic message must not be empty",
)
.with_object_ref(&diagnostic.id),
);
}
report
}