use std::fmt::Display;
use serde::Serialize;
use crate::config::WorkflowSchema;
macro_rules! diagnose_fields {
($diagnostics:ident, $receiver:tt, $schema:expr, $( $pointer:literal => $field:ident ),+ $(,)?) => {
$(
$diagnostics.extends_with_pointer($pointer, $receiver.$field.diagnose($schema));
)+
};
}
pub(crate) use diagnose_fields;
#[derive(Debug, Clone, Serialize)]
pub struct Diagnostics {
pub errors: Vec<Diagnostic>,
pub warnings: Vec<Diagnostic>,
}
impl Display for Diagnostics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for diag in self.errors.iter().chain(self.warnings.iter()) {
writeln!(f, "{}", diag)?;
}
Ok(())
}
}
impl Diagnostics {
pub fn new() -> Self {
Self {
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn extends_with_pointer(&mut self, pointer: &str, other: Diagnostics) {
self.errors.extend(other.errors.into_iter().map(|diag| {
let extended_pointer = extend_pointer(pointer, &diag.pointer);
diag.with_pointer(extended_pointer)
}));
self.warnings.extend(other.warnings.into_iter().map(|diag| {
let extended_pointer = extend_pointer(pointer, &diag.pointer);
diag.with_pointer(extended_pointer)
}));
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn push(&mut self, diag: Diagnostic) {
match diag.severity {
DiagnosticSeverity::Error => self.errors.push(diag),
DiagnosticSeverity::Warning => self.warnings.push(diag),
}
}
pub fn error_if_empty_str(&mut self, pointer: &str, value: &str) {
if value.trim().is_empty() {
self.push(Diagnostic::error(pointer, DiagnosticCode::EmptyStr));
}
}
pub fn error_if_empty_map(&mut self, pointer: &str, is_empty: bool) {
if is_empty {
self.push(Diagnostic::error(pointer, DiagnosticCode::EmptyMap));
}
}
pub fn error_if_empty_map_here(&mut self, is_empty: bool) {
self.error_if_empty_map("", is_empty);
}
pub fn error_if_non_positive(&mut self, pointer: &str, value: usize) {
if value == 0 {
self.push(Diagnostic::error(pointer, DiagnosticCode::NonPositiveNumber(value)));
}
}
pub fn warn_unknown_fields(&mut self, fields: &serde_yaml::Mapping) {
self.extend(
fields
.keys()
.filter_map(|key| key.as_str().map(|key| Diagnostic::warning(key, DiagnosticCode::UnknownField))),
);
}
pub fn error_if_empty_path(&mut self, pointer: &str, path: &std::path::Path) {
if path.as_os_str().is_empty() {
self.push(Diagnostic::error(pointer, DiagnosticCode::EmptyStr));
}
}
pub fn extend<I: IntoIterator<Item = Diagnostic>>(&mut self, diags: I) {
for diag in diags {
self.push(diag);
}
}
}
fn extend_pointer(parent: &str, child: &str) -> String {
match (parent.is_empty(), child.is_empty()) {
(true, true) => String::new(),
(true, false) => child.to_string(),
(false, true) => parent.to_string(),
(false, false) => format!("{parent}.{child}"),
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Diagnostic {
pub severity: DiagnosticSeverity,
pub pointer: String,
pub code: DiagnosticCode,
}
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticSeverity {
Error,
Warning,
}
impl Display for DiagnosticSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
DiagnosticSeverity::Error => "error",
DiagnosticSeverity::Warning => "warning",
}
)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticCode {
NonPositiveNumber(usize),
UnknownField,
EmptyStr,
EmptyMap,
UnknownAgent(String),
}
impl Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.code {
DiagnosticCode::NonPositiveNumber(num) => {
write!(f, "'{}' expected to be great than 0, got {}", self.pointer, num)
},
DiagnosticCode::UnknownField => write!(f, "unknown field '{}'", self.pointer),
DiagnosticCode::EmptyStr => write!(f, "'{}' cannot be empty string", self.pointer),
DiagnosticCode::EmptyMap => write!(f, "'{}' cannot be empty map", self.pointer),
DiagnosticCode::UnknownAgent(agent) => write!(
f,
"agent profile '{}' set for '{}' is not defined in agents configuration section",
agent, self.pointer
),
}
}
}
impl Diagnostic {
pub fn error(field: &str, code: DiagnosticCode) -> Self {
Self {
severity: DiagnosticSeverity::Error,
code,
pointer: field.to_string(),
}
}
pub fn warning(field: &str, code: DiagnosticCode) -> Self {
Self {
severity: DiagnosticSeverity::Warning,
code,
pointer: field.to_string(),
}
}
pub fn with_pointer<S: Into<String>>(mut self, pointer: S) -> Self {
self.pointer = pointer.into();
self
}
}
pub trait Diagnose {
fn diagnose(&self, workflow: &WorkflowSchema) -> Diagnostics;
}