shacl_validation/validation_report/
report.rs

1use 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    /// Sets the same prefixmap for nodes and shapes
33    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    /// Sets the prefixmap for nodes
40    pub fn with_nodes_prefixmap(mut self, prefixmap: PrefixMap) -> Self {
41        self.nodes_prefixmap = prefixmap;
42        self
43    }
44
45    /// Sets the prefixmap for shapes
46    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    // TODO: This way to compare validation report results is wrong
168    // Comparing only the len() is very weak
169    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}