use super::result::ValidationResult;
use super::validation_report_error::ReportError;
use colored::*;
use iri_s::IriS;
use prefixmap::PrefixMap;
use rudof_rdf::rdf_core::vocabs::ShaclVocab;
use rudof_rdf::rdf_core::{
BuildRDF, FocusRDF, Rdf, SHACLPath,
term::{IriOrBlankNode, Object},
};
use shacl_ir::severity::CompiledSeverity;
use std::{
fmt::{Debug, Display},
io::{Error, Write},
};
use tabled::{
builder::Builder,
settings::{Modify, Style, Width, object::Segment},
};
#[derive(Debug, Clone)]
pub struct ValidationReport {
results: Vec<ValidationResult>,
nodes_prefixmap: PrefixMap,
shapes_prefixmap: PrefixMap,
ok_color: Option<Color>,
info_color: Option<Color>,
warning_color: Option<Color>,
debug_color: Option<Color>,
trace_color: Option<Color>,
fail_color: Option<Color>,
display_with_colors: bool,
}
impl ValidationReport {
pub fn new() -> Self {
Self::default()
}
pub fn with_results(mut self, results: Vec<ValidationResult>) -> Self {
self.results = results;
self
}
pub fn with_prefixmap(mut self, prefixmap: PrefixMap) -> Self {
self.nodes_prefixmap = prefixmap.clone();
self.shapes_prefixmap = prefixmap;
self
}
pub fn with_nodes_prefixmap(mut self, prefixmap: PrefixMap) -> Self {
self.nodes_prefixmap = prefixmap;
self
}
pub fn with_shapes_prefixmap(mut self, prefixmap: PrefixMap) -> Self {
self.shapes_prefixmap = prefixmap;
self
}
pub fn without_colors(mut self) -> Self {
self.ok_color = None;
self.fail_color = None;
self
}
pub fn with_ok_color(mut self, color: Color) -> Self {
self.ok_color = Some(color);
self
}
pub fn with_fail_color(mut self, color: Color) -> Self {
self.fail_color = Some(color);
self
}
pub fn results(&self) -> &Vec<ValidationResult> {
&self.results
}
}
impl ValidationReport {
pub fn parse<S: FocusRDF>(store: &mut S, subject: S::Term) -> Result<Self, ReportError> {
let mut results = Vec::new();
for result in store
.objects_for(&subject, &ShaclVocab::sh_result().clone().into())
.map_err(|e| ReportError::ObjectsFor {
subject: subject.to_string(),
predicate: ShaclVocab::sh_result().to_string(),
error: e.to_string(),
})?
{
results.push(ValidationResult::parse(store, &result)?);
}
Ok(ValidationReport::new().with_results(results))
}
pub fn conforms(&self) -> bool {
self.results.is_empty()
}
pub fn count_violations(&self) -> usize {
self.results
.iter()
.filter(|r| r.severity() == &CompiledSeverity::Violation)
.count()
}
pub fn count_warnings(&self) -> usize {
self.results
.iter()
.filter(|r| r.severity() == &CompiledSeverity::Warning)
.count()
}
pub fn to_rdf<RDF>(&self, rdf_writer: &mut RDF) -> Result<(), ReportError>
where
RDF: BuildRDF + Sized,
{
rdf_writer
.add_prefix("sh", ShaclVocab::sh())
.map_err(|e| ReportError::ValidationError {
msg: format!("Error adding prefix to RDF: {e}"),
})?;
let report_node: RDF::Subject = rdf_writer
.add_bnode()
.map_err(|e| ReportError::ValidationError {
msg: format!("Error creating bnode: {e}"),
})?
.into();
rdf_writer
.add_type(report_node.clone(), ShaclVocab::sh_validation_report().clone())
.map_err(|e| ReportError::ValidationError {
msg: format!("Error type ValidationReport to bnode: {e}"),
})?;
let conforms: <RDF as Rdf>::IRI = ShaclVocab::sh_conforms().clone().into();
let sh_result: <RDF as Rdf>::IRI = ShaclVocab::sh_result().clone().into();
if self.results.is_empty() {
let rdf_true: <RDF as Rdf>::Term = Object::boolean(true).into();
rdf_writer
.add_triple(report_node.clone(), conforms, rdf_true)
.map_err(|e| ReportError::ValidationError {
msg: format!("Error adding conforms to bnode: {e}"),
})?;
return Ok(());
} else {
let rdf_false: <RDF as Rdf>::Term = Object::boolean(false).into();
rdf_writer
.add_triple(report_node.clone(), conforms, rdf_false)
.map_err(|e| ReportError::ValidationError {
msg: format!("Error adding conforms to bnode: {e}"),
})?;
for result in self.results.iter() {
let result_node: <RDF as Rdf>::BNode =
rdf_writer.add_bnode().map_err(|e| ReportError::ValidationError {
msg: format!("Error creating bnode: {e}"),
})?;
let result_node_term: <RDF as Rdf>::Term = result_node.into();
rdf_writer
.add_triple(report_node.clone(), sh_result.clone(), result_node_term.clone())
.map_err(|e| ReportError::ValidationError {
msg: format!("Error adding conforms to bnode: {e}"),
})?;
let result_node_subject: <RDF as Rdf>::Subject = <RDF as Rdf>::Subject::try_from(result_node_term)
.map_err(|_e| ReportError::ValidationError {
msg: "Cannot convert subject to term".to_string(),
})?;
result.to_rdf(rdf_writer, result_node_subject)?;
}
}
Ok(())
}
pub fn show_as_table<W: Write>(
&self,
mut writer: W,
_sort_mode: SortModeReport,
with_details: Option<bool>,
terminal_width: Option<usize>,
) -> Result<(), Error> {
let with_details = with_details.unwrap_or(false);
let terminal_width = terminal_width.unwrap_or(80);
let mut builder = Builder::default();
if with_details {
builder.push_record([
"Severity",
"Node",
"Component",
"Path",
"Value",
"Source shape",
"Details",
]);
} else {
builder.push_record(["Severity", "node", "Component", "Path", "value", "Source shape"]);
}
if self.results.is_empty() {
let str = "No Errors found";
if self.display_with_colors {
if let Some(ok_color) = self.ok_color {
write!(writer, "{}", str.color(ok_color))?;
} else {
write!(writer, "{str}")?;
}
} else {
write!(writer, "{str}")?;
}
Ok(())
} else {
let shacl_prefixmap = if self.display_with_colors {
PrefixMap::basic()
} else {
PrefixMap::basic().with_hyperlink(true).without_default_colors()
};
for result in self.results.iter() {
let severity_str = show_severity(result.severity(), &shacl_prefixmap);
let severity = if self.display_with_colors {
let color = calculate_color(result.severity(), self);
severity_str.color(color)
} else {
ColoredString::from(severity_str)
};
let node = show_object(result.focus_node(), &self.nodes_prefixmap);
let component = show_object(result.component(), &shacl_prefixmap);
let path = show_path_opt(result.path(), &self.shapes_prefixmap);
let source = show_object_opt(result.source(), &self.shapes_prefixmap);
let value = show_object_opt(result.value(), &self.nodes_prefixmap);
let details = result.message().unwrap_or("").to_string();
if with_details {
builder.push_record([
&severity.to_string(),
&node,
&component,
&path,
&value,
&source,
&details,
]);
} else {
builder.push_record([&severity.to_string(), &node, &component, &path, &value, &source]);
}
}
let mut table = builder.build();
table.with(Style::modern_rounded());
table.with(Modify::new(Segment::all()).with(Width::wrap(terminal_width)));
writeln!(writer, "{table}")?;
Ok(())
}
}
}
impl Default for ValidationReport {
fn default() -> Self {
ValidationReport {
results: Vec::new(),
nodes_prefixmap: PrefixMap::new(),
shapes_prefixmap: PrefixMap::new(),
ok_color: Some(Color::Green),
fail_color: Some(Color::Red),
info_color: Some(Color::Blue),
warning_color: Some(Color::Yellow),
debug_color: Some(Color::Magenta),
trace_color: Some(Color::Cyan),
display_with_colors: true,
}
}
}
impl PartialEq for ValidationReport {
fn eq(&self, other: &Self) -> bool {
if self.results.len() != other.results.len() {
return false;
}
true
}
}
impl Display for ValidationReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.results.is_empty() {
let str = "No Errors found";
if self.display_with_colors {
if let Some(ok_color) = self.ok_color {
write!(f, "{}", str.color(ok_color))?;
} else {
write!(f, "{str}")?;
}
} else {
write!(f, "{str}")?;
}
Ok(())
} else {
let str = format!("{} errors found", self.results.len());
if self.display_with_colors {
if let Some(fail_color) = self.fail_color {
writeln!(f, "{}", str.color(fail_color))?;
} else {
writeln!(f, "{str}")?;
}
} else {
writeln!(f, "{str}")?;
};
let shacl_prefixmap = if self.display_with_colors {
PrefixMap::basic()
} else {
PrefixMap::basic().with_hyperlink(true).without_default_colors()
};
for result in self.results.iter() {
let severity_str = show_severity(result.severity(), &shacl_prefixmap);
if self.display_with_colors {
let color = calculate_color(result.severity(), self);
write!(f, "{}", severity_str.color(color))?;
} else {
writeln!(f, "{severity_str}")?;
};
let msg = format!(
" node: {} {}\n{}{}{}{}",
show_object(result.focus_node(), &self.nodes_prefixmap),
show_object(result.component(), &shacl_prefixmap),
result.message().unwrap_or(""),
show_path_opt(result.path(), &self.shapes_prefixmap),
show_object_opt(result.source(), &self.shapes_prefixmap),
show_object_opt(result.value(), &self.nodes_prefixmap)
);
writeln!(f, "{msg}")?;
}
Ok(())
}
}
}
fn show_severity(severity: &CompiledSeverity, shacl_prefixmap: &PrefixMap) -> String {
shacl_prefixmap.qualify(&severity.to_iri())
}
fn show_object(object: &Object, shacl_prefixmap: &PrefixMap) -> String {
match object {
Object::Iri(iri_s) => shacl_prefixmap.qualify(iri_s),
Object::BlankNode(node) => format!("_:{node}"),
Object::Literal(literal) => format!("{literal}"),
Object::Triple { .. } => todo!(),
}
}
fn show_iri(iri: &IriS, prefixmap: &PrefixMap) -> String {
prefixmap.qualify(iri)
}
fn show_subject(subject: &IriOrBlankNode, prefixmap: &PrefixMap) -> String {
match subject {
IriOrBlankNode::Iri(iri_s) => prefixmap.qualify(iri_s),
IriOrBlankNode::BlankNode(node) => format!("_:{node}"),
}
}
fn show_object_opt(object: Option<&Object>, shacl_prefixmap: &PrefixMap) -> String {
match object {
None => String::new(),
Some(Object::Iri(iri_s)) => shacl_prefixmap.qualify(iri_s),
Some(Object::BlankNode(node)) => format!("_:{node}"),
Some(Object::Literal(literal)) => format!("{literal}"),
Some(Object::Triple {
subject,
predicate,
object,
}) => format!(
"<<{} {} {}>>",
show_subject(subject, shacl_prefixmap),
show_iri(predicate, shacl_prefixmap),
show_object(object, shacl_prefixmap)
),
}
}
fn show_path_opt(object: Option<&SHACLPath>, shacl_prefixmap: &PrefixMap) -> String {
match object {
None => String::new(),
Some(SHACLPath::Predicate { pred }) => {
let path = shacl_prefixmap.qualify(pred);
path.to_string()
},
Some(path) => path.to_string(),
}
}
fn calculate_color(severity: &CompiledSeverity, report: &ValidationReport) -> Color {
match severity {
CompiledSeverity::Violation => report.fail_color.unwrap_or(Color::Red),
CompiledSeverity::Info => report.info_color.unwrap_or(Color::Blue),
CompiledSeverity::Warning => report.warning_color.unwrap_or(Color::Yellow),
CompiledSeverity::Debug => report.debug_color.unwrap_or(Color::Magenta),
CompiledSeverity::Trace => report.trace_color.unwrap_or(Color::Cyan),
CompiledSeverity::Generic(_) => Color::White,
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
pub enum SortModeReport {
#[default]
Node,
Severity,
Shape,
Component,
Source,
Path,
Value,
Details,
}