shacl_validation/validation_report/
report.rs1use std::fmt::{Debug, Display};
2
3use colored::*;
4use prefixmap::PrefixMap;
5use srdf::{Object, Query, Rdf, SRDFBuilder};
6
7use crate::helpers::srdf::get_objects_for;
8
9use super::result::ValidationResult;
10use super::validation_report_error::ReportError;
11
12#[derive(Debug, Clone)]
13pub struct ValidationReport {
14 results: Vec<ValidationResult>,
15 nodes_prefixmap: PrefixMap,
16 shapes_prefixmap: PrefixMap,
17 ok_color: Option<Color>,
18 fail_color: Option<Color>,
19 display_with_colors: bool,
20}
21
22impl ValidationReport {
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 pub fn with_results(mut self, results: Vec<ValidationResult>) -> Self {
28 self.results = results;
29 self
30 }
31
32 pub fn with_prefixmap(mut self, prefixmap: PrefixMap) -> Self {
34 self.nodes_prefixmap = prefixmap.clone();
35 self.shapes_prefixmap = prefixmap;
36 self
37 }
38
39 pub fn with_nodes_prefixmap(mut self, prefixmap: PrefixMap) -> Self {
41 self.nodes_prefixmap = prefixmap;
42 self
43 }
44
45 pub fn with_shapes_prefixmap(mut self, prefixmap: PrefixMap) -> Self {
47 self.shapes_prefixmap = prefixmap;
48 self
49 }
50
51 pub fn without_colors(mut self) -> Self {
52 self.ok_color = None;
53 self.fail_color = None;
54 self
55 }
56
57 pub fn with_ok_color(mut self, color: Color) -> Self {
58 self.ok_color = Some(color);
59 self
60 }
61
62 pub fn with_fail_color(mut self, color: Color) -> Self {
63 self.fail_color = Some(color);
64 self
65 }
66
67 pub fn results(&self) -> &Vec<ValidationResult> {
68 &self.results
69 }
70}
71
72impl ValidationReport {
73 pub fn parse<S: Query>(store: &S, subject: S::Term) -> Result<Self, ReportError> {
74 let mut results = Vec::new();
75 for result in get_objects_for(store, &subject, &shacl_ast::SH_RESULT.clone().into())? {
76 results.push(ValidationResult::parse(store, &result)?);
77 }
78 Ok(ValidationReport::new().with_results(results))
79 }
80
81 pub fn conforms(&self) -> bool {
82 self.results.is_empty()
83 }
84
85 pub fn to_rdf<RDF>(&self, rdf_writer: &mut RDF) -> Result<(), ReportError>
86 where
87 RDF: SRDFBuilder + Sized,
88 {
89 rdf_writer.add_prefix("sh", &shacl_ast::SH).map_err(|e| {
90 ReportError::ValidationReportError {
91 msg: format!("Error adding prefix to RDF: {e}"),
92 }
93 })?;
94 let report_node: RDF::Subject = rdf_writer
95 .add_bnode()
96 .map_err(|e| ReportError::ValidationReportError {
97 msg: format!("Error creating bnode: {e}"),
98 })?
99 .into();
100 rdf_writer
101 .add_type(report_node.clone(), shacl_ast::SH_VALIDATION_REPORT.clone())
102 .map_err(|e| ReportError::ValidationReportError {
103 msg: format!("Error type ValidationReport to bnode: {e}"),
104 })?;
105
106 let conforms: <RDF as Rdf>::IRI = shacl_ast::SH_CONFORMS.clone().into();
107 let sh_result: <RDF as Rdf>::IRI = shacl_ast::SH_RESULT.clone().into();
108 if self.results.is_empty() {
109 let rdf_true: <RDF as Rdf>::Term = Object::boolean(true).into();
110 rdf_writer
111 .add_triple(report_node.clone(), conforms, rdf_true)
112 .map_err(|e| ReportError::ValidationReportError {
113 msg: format!("Error adding conforms to bnode: {e}"),
114 })?;
115 return Ok(());
116 } else {
117 let rdf_false: <RDF as Rdf>::Term = Object::boolean(false).into();
118 rdf_writer
119 .add_triple(report_node.clone(), conforms, rdf_false)
120 .map_err(|e| ReportError::ValidationReportError {
121 msg: format!("Error adding conforms to bnode: {e}"),
122 })?;
123 for result in self.results.iter() {
124 let result_node: <RDF as Rdf>::BNode =
125 rdf_writer
126 .add_bnode()
127 .map_err(|e| ReportError::ValidationReportError {
128 msg: format!("Error creating bnode: {e}"),
129 })?;
130 let result_node_term: <RDF as Rdf>::Term = result_node.into();
131 rdf_writer
132 .add_triple(
133 report_node.clone(),
134 sh_result.clone(),
135 result_node_term.clone(),
136 )
137 .map_err(|e| ReportError::ValidationReportError {
138 msg: format!("Error adding conforms to bnode: {e}"),
139 })?;
140 let result_node_subject: <RDF as Rdf>::Subject =
141 <RDF as Rdf>::Subject::try_from(result_node_term).map_err(|_e| {
142 ReportError::ValidationReportError {
143 msg: "Cannot convert subject to term".to_string(),
144 }
145 })?;
146 result.to_rdf(rdf_writer, result_node_subject)?;
147 }
148 }
149 Ok(())
150 }
151}
152
153impl Default for ValidationReport {
154 fn default() -> Self {
155 ValidationReport {
156 results: Vec::new(),
157 nodes_prefixmap: PrefixMap::new(),
158 shapes_prefixmap: PrefixMap::new(),
159 ok_color: Some(Color::Green),
160 fail_color: Some(Color::Red),
161 display_with_colors: true,
162 }
163 }
164}
165
166impl PartialEq for ValidationReport {
167 fn eq(&self, other: &Self) -> bool {
170 if self.results.len() != other.results.len() {
171 return false;
172 }
173 true
174 }
175}
176
177impl Display for ValidationReport {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 if self.results.is_empty() {
180 let str = "No Errors found";
181 if self.display_with_colors {
182 if let Some(ok_color) = self.ok_color {
183 write!(f, "{}", str.color(ok_color))?;
184 } else {
185 write!(f, "{str}")?;
186 }
187 } else {
188 write!(f, "{str}")?;
189 }
190 Ok(())
191 } else {
192 let str = format!("{} errors found", self.results.len());
193 if self.display_with_colors {
194 if let Some(fail_color) = self.fail_color {
195 writeln!(f, "{}", str.color(fail_color))?;
196 } else {
197 writeln!(f, "{str}")?;
198 }
199 } else {
200 writeln!(f, "{str}")?;
201 };
202 let shacl_prefixmap = if self.display_with_colors {
203 PrefixMap::basic()
204 } else {
205 PrefixMap::basic()
206 .with_hyperlink(true)
207 .without_default_colors()
208 };
209 for result in self.results.iter() {
210 writeln!(
211 f,
212 "Focus node {}, Component: {},{}{} severity: {}",
213 show_object(result.focus_node(), &self.nodes_prefixmap),
214 show_object(result.component(), &shacl_prefixmap),
215 show_object_opt("source shape", result.source(), &shacl_prefixmap),
216 show_object_opt("value", result.value(), &shacl_prefixmap),
217 show_object(result.severity(), &shacl_prefixmap)
218 )?;
219 }
220 Ok(())
221 }
222 }
223}
224
225fn show_object(object: &Object, shacl_prefixmap: &PrefixMap) -> String {
226 match object {
227 Object::Iri(iri_s) => shacl_prefixmap.qualify(iri_s),
228 Object::BlankNode(node) => format!("_:{node}"),
229 Object::Literal(literal) => format!("{literal}"),
230 }
231}
232
233fn show_object_opt(msg: &str, object: Option<&Object>, shacl_prefixmap: &PrefixMap) -> String {
234 match object {
235 None => String::new(),
236 Some(Object::Iri(iri_s)) => shacl_prefixmap.qualify(iri_s),
237 Some(Object::BlankNode(node)) => format!(" {msg}: _:{node}, "),
238 Some(Object::Literal(literal)) => format!(" {msg}: {literal}, "),
239 }
240}