use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Info,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PathSegment {
Key(String),
Index(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ByteRange {
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SchemaDiagnostic {
pub code: String,
pub severity: Severity,
pub path: Vec<PathSegment>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub range: Option<ByteRange>,
}
impl SchemaDiagnostic {
pub fn new(
code: impl Into<String>,
severity: Severity,
path: Vec<PathSegment>,
message: impl Into<String>,
) -> Self {
Self {
code: code.into(),
severity,
path,
message: message.into(),
range: None,
}
}
pub fn is_error(&self) -> bool {
self.severity == Severity::Error
}
pub fn message(&self) -> &str {
&self.message
}
}
pub(crate) struct RemovedField {
pub key: &'static str,
pub advice: &'static str,
pub discarded: bool,
}
impl RemovedField {
pub(crate) const fn replaced(key: &'static str, advice: &'static str) -> Self {
Self {
key,
advice,
discarded: false,
}
}
pub(crate) const fn discarded(key: &'static str, advice: &'static str) -> Self {
Self {
key,
advice,
discarded: true,
}
}
}
pub(crate) struct RemovedValue {
pub key: &'static str,
pub value: &'static str,
pub replacement: &'static str,
}
impl RemovedValue {
pub(crate) const fn new(
key: &'static str,
value: &'static str,
replacement: &'static str,
) -> Self {
Self {
key,
value,
replacement,
}
}
pub(crate) fn find<'a>(removed: &'a [Self], key: &str, value: &str) -> Option<&'a Self> {
removed
.iter()
.find(|entry| entry.key == key && entry.value == value)
}
pub(crate) fn diagnostic(&self) -> SchemaDiagnostic {
SchemaDiagnostic::new(
"field.removed",
Severity::Error,
key_path(self.key),
format!(
"{} value `{}` was removed; use `{}`",
self.key, self.value, self.replacement
),
)
}
}
pub(crate) fn unrecognized_field(removed: &[RemovedField], key: &str) -> SchemaDiagnostic {
match removed.iter().find(|field| field.key == key) {
Some(field) => SchemaDiagnostic::new(
"field.removed",
Severity::Error,
key_path(key),
format!("`{key}` was removed; {}", field.advice),
),
None => SchemaDiagnostic::new(
"field.unknown",
Severity::Warning,
key_path(key),
format!(
"unknown field `{key}` is kept in the file but written to no native file; put harness-specific keys under `harness-overrides.<harness>`"
),
),
}
}
pub(crate) fn push_invalid(
kind: &str,
diags: &mut Vec<SchemaDiagnostic>,
path: Vec<PathSegment>,
value: impl Into<String>,
allowed: &'static str,
) {
let value = value.into();
let severity = if matches!(path.first(), Some(PathSegment::Key(key)) if key == "harness-overrides")
{
Severity::Warning
} else {
Severity::Error
};
let display_field = if matches!(path.first(), Some(PathSegment::Key(key)) if key == "model-policies")
{
path.iter()
.map(|segment| match segment {
PathSegment::Key(key) => key.clone(),
PathSegment::Index(index) => format!("[{}]", index),
})
.fold(String::new(), |mut display, segment| {
if !display.is_empty() && !segment.starts_with('[') {
display.push('.');
}
display.push_str(&segment);
display
})
} else {
display_path(&path)
};
diags.push(SchemaDiagnostic::new(
"field.value",
severity,
path,
format!("{kind} field `{display_field}` has invalid value `{value}`; allowed: {allowed}"),
));
}
pub(crate) fn key_path(field: impl Into<String>) -> Vec<PathSegment> {
vec![PathSegment::Key(field.into())]
}
pub(crate) fn child_path(parent: impl Into<String>, child: impl Into<String>) -> Vec<PathSegment> {
vec![
PathSegment::Key(parent.into()),
PathSegment::Key(child.into()),
]
}
pub fn display_path(path: &[PathSegment]) -> String {
let mut display = String::new();
for segment in path {
match segment {
PathSegment::Key(key) => {
if !display.is_empty() {
display.push('.');
}
display.push_str(key);
}
PathSegment::Index(index) => {
use std::fmt::Write;
let _ = write!(display, "[{index}]");
}
}
}
display
}
pub(crate) fn frontmatter_error(error: &crate::frontmatter::FrontmatterError) -> SchemaDiagnostic {
let code = match error {
crate::frontmatter::FrontmatterError::TooLarge(_) => "frontmatter.too-large",
crate::frontmatter::FrontmatterError::TooDeep => "frontmatter.too-deep",
crate::frontmatter::FrontmatterError::Unclosed => "frontmatter.unclosed",
crate::frontmatter::FrontmatterError::Anchor => "yaml.anchor",
crate::frontmatter::FrontmatterError::DocumentEnd => "yaml.document-end",
crate::frontmatter::FrontmatterError::UnsupportedSyntax(_) => "yaml.unsupported-syntax",
crate::frontmatter::FrontmatterError::NotAMapping => "frontmatter.not-mapping",
crate::frontmatter::FrontmatterError::MalformedYaml(inner) => {
let message = inner.to_string();
if message.contains("duplicate entry") || message.contains("duplicate key") {
"yaml.duplicate-key"
} else {
"yaml.invalid"
}
}
};
SchemaDiagnostic::new(code, Severity::Error, Vec::new(), error.to_string())
}