1use std::collections::{BTreeSet, HashSet};
9
10use thiserror::Error;
11
12use crate::{Rete, TermTriple};
13
14const RDF_TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
15const RDF_FIRST: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#first>";
16const RDF_REST: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#rest>";
17const RDF_NIL: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#nil>";
18const RDFS_CLASS: &str = "<http://www.w3.org/2000/01/rdf-schema#Class>";
19const RDFS_SUBCLASS_OF: &str = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
20const OWL_CLASS: &str = "<http://www.w3.org/2002/07/owl#Class>";
21const XSD_STRING: &str = "<http://www.w3.org/2001/XMLSchema#string>";
22const RDF_LANG_STRING: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#langString>";
23
24const SH: &str = "http://www.w3.org/ns/shacl#";
25
26macro_rules! sh {
27 ($local:literal) => {
28 concat!("<http://www.w3.org/ns/shacl#", $local, ">")
29 };
30}
31
32type Triple = (String, String, String);
33
34#[derive(Debug, Error)]
35#[non_exhaustive]
36pub enum ShaclError {
37 #[error("failed to parse SHACL shapes Turtle: {0}")]
38 Parse(String),
39 #[error("malformed RDF list at {0}")]
40 MalformedList(String),
41}
42
43pub trait GraphView {
52 fn objects(&self, subject: &str, predicate: &str) -> Vec<String>;
54 fn subjects_with(&self, predicate: &str, object: &str) -> Vec<String>;
56 fn subjects_of(&self, predicate: &str) -> Vec<String>;
58 fn objects_of(&self, predicate: &str) -> Vec<String>;
60 fn predicates_for_subject(&self, subject: &str) -> Vec<String>;
62 fn all_nodes(&self) -> Vec<String>;
66
67 fn is_subclass_of(&self, child: &str, parent: &str) -> bool {
69 if child == parent {
70 return true;
71 }
72 let mut seen = HashSet::new();
73 let mut stack = vec![child.to_string()];
74 while let Some(c) = stack.pop() {
75 if !seen.insert(c.clone()) {
76 continue;
77 }
78 for sup in self.objects(&c, RDFS_SUBCLASS_OF) {
79 if sup == parent {
80 return true;
81 }
82 stack.push(sup);
83 }
84 }
85 false
86 }
87
88 fn subclasses_of(&self, parent: &str) -> BTreeSet<String> {
90 self.subjects_of(RDFS_SUBCLASS_OF)
91 .into_iter()
92 .filter(|s| self.is_subclass_of(s, parent))
93 .collect()
94 }
95
96 fn instances_of(&self, class: &str) -> Vec<String> {
98 let mut classes = self.subclasses_of(class);
99 classes.insert(class.to_string());
100 let mut out = Vec::new();
101 for c in &classes {
102 out.extend(self.subjects_with(RDF_TYPE, c));
103 }
104 unique(out)
105 }
106
107 fn is_instance_of(&self, node: &str, class: &str) -> bool {
109 self.objects(node, RDF_TYPE)
110 .iter()
111 .any(|c| self.is_subclass_of(c, class))
112 }
113}
114
115#[derive(Debug, Clone, Default)]
119pub struct DataGraph {
120 triples: Vec<Triple>,
121}
122
123impl DataGraph {
124 pub fn from_triples(triples: Vec<TermTriple>) -> Self {
125 let mut triples = triples;
126 triples.sort();
127 triples.dedup();
128 Self { triples }
129 }
130
131 pub fn from_rete(rete: &Rete, graph: Option<&str>) -> Self {
132 Self::from_triples(rete.dump(graph))
133 }
134
135 fn has(&self, s: &str, p: &str, o: &str) -> bool {
136 self.triples
137 .iter()
138 .any(|(ts, tp, to)| ts == s && tp == p && to == o)
139 }
140}
141
142impl GraphView for DataGraph {
143 fn objects(&self, subject: &str, predicate: &str) -> Vec<String> {
144 self.triples
145 .iter()
146 .filter(|(s, p, _)| s == subject && p == predicate)
147 .map(|(_, _, o)| o.clone())
148 .collect()
149 }
150
151 fn subjects_with(&self, predicate: &str, object: &str) -> Vec<String> {
152 unique(
153 self.triples
154 .iter()
155 .filter(|(_, p, o)| p == predicate && o == object)
156 .map(|(s, _, _)| s.clone())
157 .collect(),
158 )
159 }
160
161 fn subjects_of(&self, predicate: &str) -> Vec<String> {
162 unique(
163 self.triples
164 .iter()
165 .filter(|(_, p, _)| p == predicate)
166 .map(|(s, _, _)| s.clone())
167 .collect(),
168 )
169 }
170
171 fn objects_of(&self, predicate: &str) -> Vec<String> {
172 unique(
173 self.triples
174 .iter()
175 .filter(|(_, p, _)| p == predicate)
176 .map(|(_, _, o)| o.clone())
177 .collect(),
178 )
179 }
180
181 fn predicates_for_subject(&self, subject: &str) -> Vec<String> {
182 unique(
183 self.triples
184 .iter()
185 .filter(|(s, _, _)| s == subject)
186 .map(|(_, p, _)| p.clone())
187 .collect(),
188 )
189 }
190
191 fn all_nodes(&self) -> Vec<String> {
192 let mut out = Vec::new();
193 for (s, _, o) in &self.triples {
194 out.push(s.clone());
195 out.push(o.clone());
196 }
197 unique(out)
198 }
199}
200
201pub struct ReteGraph<'a> {
208 rete: &'a Rete,
209}
210
211impl<'a> ReteGraph<'a> {
212 pub fn new(rete: &'a Rete) -> Self {
213 Self { rete }
214 }
215}
216
217impl GraphView for ReteGraph<'_> {
218 fn objects(&self, subject: &str, predicate: &str) -> Vec<String> {
219 self.rete
220 .query(Some(subject), Some(predicate), None)
221 .into_iter()
222 .map(|(_, _, o)| o)
223 .collect()
224 }
225
226 fn subjects_with(&self, predicate: &str, object: &str) -> Vec<String> {
227 self.rete
228 .query(None, Some(predicate), Some(object))
229 .into_iter()
230 .map(|(s, _, _)| s)
231 .collect()
232 }
233
234 fn subjects_of(&self, predicate: &str) -> Vec<String> {
235 unique(
236 self.rete
237 .query(None, Some(predicate), None)
238 .into_iter()
239 .map(|(s, _, _)| s)
240 .collect(),
241 )
242 }
243
244 fn objects_of(&self, predicate: &str) -> Vec<String> {
245 unique(
246 self.rete
247 .query(None, Some(predicate), None)
248 .into_iter()
249 .map(|(_, _, o)| o)
250 .collect(),
251 )
252 }
253
254 fn predicates_for_subject(&self, subject: &str) -> Vec<String> {
255 unique(
256 self.rete
257 .query(Some(subject), None, None)
258 .into_iter()
259 .map(|(_, p, _)| p)
260 .collect(),
261 )
262 }
263
264 fn all_nodes(&self) -> Vec<String> {
265 let mut out = Vec::new();
266 for (s, _, o) in self.rete.query(None, None, None) {
267 out.push(s);
268 out.push(o);
269 }
270 unique(out)
271 }
272}
273
274#[derive(Debug, Clone)]
276pub struct ShaclShapes {
277 graph: DataGraph,
278}
279
280impl ShaclShapes {
281 pub fn parse_turtle(text: &str) -> Result<Self, ShaclError> {
282 let mut triples = Vec::new();
283 for r in oxttl::TurtleParser::new().for_reader(text.as_bytes()) {
284 let t = r.map_err(|e| ShaclError::Parse(e.to_string()))?;
285 triples.push((
286 t.subject.to_string(),
287 t.predicate.to_string(),
288 t.object.to_string(),
289 ));
290 }
291 Ok(Self {
292 graph: DataGraph::from_triples(triples),
293 })
294 }
295
296 fn objects(&self, subject: &str, predicate: &str) -> Vec<String> {
297 self.graph.objects(subject, predicate)
298 }
299
300 fn subjects(&self, predicate: &str, object: &str) -> Vec<String> {
301 unique(
302 self.graph
303 .triples
304 .iter()
305 .filter(|(_, p, o)| p == predicate && o == object)
306 .map(|(s, _, _)| s.clone())
307 .collect(),
308 )
309 }
310
311 fn has(&self, s: &str, p: &str, o: &str) -> bool {
312 self.graph.has(s, p, o)
313 }
314
315 fn list(&self, head: &str) -> Result<Vec<String>, ShaclError> {
316 if head == RDF_NIL {
317 return Ok(Vec::new());
318 }
319 let mut out = Vec::new();
320 let mut cur = head.to_string();
321 let mut seen = HashSet::new();
322 loop {
323 if cur == RDF_NIL {
324 break;
325 }
326 if !seen.insert(cur.clone()) {
327 return Err(ShaclError::MalformedList(head.to_string()));
328 }
329 let first = self.objects(&cur, RDF_FIRST);
330 let rest = self.objects(&cur, RDF_REST);
331 if first.len() != 1 || rest.len() != 1 {
332 return Err(ShaclError::MalformedList(head.to_string()));
333 }
334 out.push(first[0].clone());
335 cur = rest[0].clone();
336 }
337 Ok(out)
338 }
339
340 fn target_shapes(&self) -> Vec<String> {
341 let mut ids = Vec::new();
342 for (s, p, o) in &self.graph.triples {
343 if matches!(
344 p.as_str(),
345 sh!("targetNode")
346 | sh!("targetClass")
347 | sh!("targetSubjectsOf")
348 | sh!("targetObjectsOf")
349 ) || (p == RDF_TYPE
350 && matches!(
351 o.as_str(),
352 sh!("NodeShape") | sh!("PropertyShape") | RDFS_CLASS | OWL_CLASS
353 ))
354 {
355 ids.push(s.clone());
356 }
357 }
358 unique(ids)
359 }
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
363#[non_exhaustive]
364pub enum Severity {
365 Info,
366 Warning,
367 Violation,
368 Other(String),
369}
370
371impl Severity {
372 fn from_token(token: Option<String>) -> Self {
373 match token.as_deref() {
374 Some(sh!("Info")) => Severity::Info,
375 Some(sh!("Warning")) => Severity::Warning,
376 Some(sh!("Violation")) | None => Severity::Violation,
377 Some(other) => Severity::Other(strip_iri(other).unwrap_or(other).to_string()),
378 }
379 }
380
381 pub fn iri(&self) -> String {
382 match self {
383 Severity::Info => format!("{SH}Info"),
384 Severity::Warning => format!("{SH}Warning"),
385 Severity::Violation => format!("{SH}Violation"),
386 Severity::Other(iri) => iri.clone(),
387 }
388 }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
392pub struct ValidationResult {
393 pub focus_node: String,
394 pub value_node: Option<String>,
395 pub result_path: Option<String>,
396 pub source_shape: String,
397 pub source_constraint_component: String,
398 pub severity: Severity,
399 pub messages: Vec<String>,
400}
401
402#[derive(Debug, Clone, Default, PartialEq, Eq)]
403#[must_use]
404pub struct ValidationReport {
405 pub conforms: bool,
406 pub results: Vec<ValidationResult>,
407}
408
409impl ValidationReport {
410 pub fn to_json(&self) -> String {
411 use serde_json::json;
412 let results: Vec<_> = self
413 .results
414 .iter()
415 .map(|r| {
416 json!({
417 "focusNode": term_json_string(&r.focus_node),
418 "valueNode": r.value_node.as_deref().map(term_json_string),
419 "resultPath": r.result_path,
420 "sourceShape": term_json_string(&r.source_shape),
421 "sourceConstraintComponent": r.source_constraint_component,
422 "resultSeverity": r.severity.iri(),
423 "resultMessage": r.messages,
424 })
425 })
426 .collect();
427 serde_json::to_string_pretty(&json!({
428 "schemaVersion": 1,
429 "conforms": self.conforms,
430 "results": results,
431 }))
432 .unwrap_or_default()
433 }
434
435 pub fn to_turtle(&self) -> String {
436 let mut out = String::new();
437 out.push_str("@prefix sh: <http://www.w3.org/ns/shacl#> .\n\n");
438 out.push_str("[] a <http://www.w3.org/ns/shacl#ValidationReport> ;\n");
439 out.push_str(&format!(
440 " <http://www.w3.org/ns/shacl#conforms> {} ",
441 self.conforms
442 ));
443 if self.results.is_empty() {
444 out.push_str(".\n");
445 return out;
446 }
447 out.push_str(";\n");
448 for (i, r) in self.results.iter().enumerate() {
449 out.push_str(" <http://www.w3.org/ns/shacl#result> [\n");
450 out.push_str(" a <http://www.w3.org/ns/shacl#ValidationResult> ;\n");
451 out.push_str(&format!(
452 " <http://www.w3.org/ns/shacl#focusNode> {} ;\n",
453 r.focus_node
454 ));
455 if let Some(v) = &r.value_node {
456 out.push_str(&format!(" <http://www.w3.org/ns/shacl#value> {v} ;\n"));
457 }
458 if let Some(path) = &r.result_path {
459 out.push_str(&format!(
460 " <http://www.w3.org/ns/shacl#resultPath> \"{}\" ;\n",
461 escape_string(path)
462 ));
463 }
464 out.push_str(&format!(
465 " <http://www.w3.org/ns/shacl#sourceShape> {} ;\n",
466 r.source_shape
467 ));
468 out.push_str(&format!(
469 " <http://www.w3.org/ns/shacl#sourceConstraintComponent> <{}> ;\n",
470 r.source_constraint_component
471 ));
472 out.push_str(&format!(
473 " <http://www.w3.org/ns/shacl#resultSeverity> <{}>",
474 r.severity.iri()
475 ));
476 for msg in &r.messages {
477 out.push_str(&format!(
478 " ;\n <http://www.w3.org/ns/shacl#resultMessage> \"{}\"",
479 escape_string(msg)
480 ));
481 }
482 out.push_str("\n ]");
483 out.push_str(if i + 1 == self.results.len() {
484 " .\n"
485 } else {
486 " ;\n"
487 });
488 }
489 out
490 }
491}
492
493#[derive(Debug, Clone)]
494enum Path {
495 Predicate(String),
496 Inverse(Box<Path>),
497 Sequence(Vec<Path>),
498 Alternative(Vec<Path>),
499 ZeroOrMore(Box<Path>),
500 OneOrMore(Box<Path>),
501 ZeroOrOne(Box<Path>),
502}
503
504impl Path {
505 fn display(&self) -> String {
506 match self {
507 Path::Predicate(p) => p.clone(),
508 Path::Inverse(p) => format!("^{}", p.display()),
509 Path::Sequence(ps) => format!(
510 "({})",
511 ps.iter().map(Path::display).collect::<Vec<_>>().join(" ")
512 ),
513 Path::Alternative(ps) => format!(
514 "({})",
515 ps.iter().map(Path::display).collect::<Vec<_>>().join("|")
516 ),
517 Path::ZeroOrMore(p) => format!("{}*", p.display()),
518 Path::OneOrMore(p) => format!("{}+", p.display()),
519 Path::ZeroOrOne(p) => format!("{}?", p.display()),
520 }
521 }
522}
523
524#[derive(Debug)]
525struct ShapeView<'a> {
526 id: &'a str,
527 path: Option<Path>,
528 severity: Severity,
529 messages: Vec<String>,
530}
531
532struct Validator<'a, G: GraphView> {
533 data: &'a G,
534 shapes: &'a ShaclShapes,
535}
536
537pub fn validate_shacl<G: GraphView>(data: &G, shapes: &ShaclShapes) -> ValidationReport {
541 let validator = Validator { data, shapes };
542 let mut results = Vec::new();
543 for shape in shapes.target_shapes() {
544 let targets = validator.targets(&shape);
545 for focus in targets {
546 results.extend(validator.validate_shape(&shape, &focus, &mut Vec::new()));
547 }
548 }
549 results.sort_by(|a, b| {
550 (
551 &a.focus_node,
552 &a.result_path,
553 &a.source_constraint_component,
554 &a.value_node,
555 )
556 .cmp(&(
557 &b.focus_node,
558 &b.result_path,
559 &b.source_constraint_component,
560 &b.value_node,
561 ))
562 });
563 results.dedup();
564 ValidationReport {
565 conforms: results.is_empty(),
566 results,
567 }
568}
569
570impl<'a, G: GraphView> Validator<'a, G> {
571 fn view(&self, shape: &'a str) -> ShapeView<'a> {
572 let path = self
573 .shapes
574 .objects(shape, sh!("path"))
575 .first()
576 .and_then(|p| self.parse_path(p).ok());
577 let severity =
578 Severity::from_token(self.shapes.objects(shape, sh!("severity")).first().cloned());
579 let messages = self
580 .shapes
581 .objects(shape, sh!("message"))
582 .into_iter()
583 .filter_map(|m| literal_lexical(&m).map(|l| l.value))
584 .collect();
585 ShapeView {
586 id: shape,
587 path,
588 severity,
589 messages,
590 }
591 }
592
593 fn targets(&self, shape: &str) -> Vec<String> {
594 let mut out = Vec::new();
595 out.extend(self.shapes.objects(shape, sh!("targetNode")));
596 for class in self.shapes.objects(shape, sh!("targetClass")) {
597 out.extend(self.data.instances_of(&class));
598 }
599 for pred in self.shapes.objects(shape, sh!("targetSubjectsOf")) {
600 out.extend(self.data.subjects_of(&pred));
601 }
602 for pred in self.shapes.objects(shape, sh!("targetObjectsOf")) {
603 out.extend(self.data.objects_of(&pred));
604 }
605 if self.shapes.has(shape, RDF_TYPE, RDFS_CLASS)
606 || self.shapes.has(shape, RDF_TYPE, OWL_CLASS)
607 {
608 out.extend(self.data.instances_of(shape));
609 }
610 unique(out)
611 }
612
613 fn validate_shape(
614 &self,
615 shape: &str,
616 focus: &str,
617 stack: &mut Vec<(String, String)>,
618 ) -> Vec<ValidationResult> {
619 if stack.iter().any(|(s, f)| s == shape && f == focus) {
620 return vec![self.result(
621 &self.view(shape),
622 focus,
623 None,
624 component("RecursiveConstraintComponent"),
625 None,
626 )];
627 }
628 stack.push((shape.to_string(), focus.to_string()));
629 let view = self.view(shape);
630 if bool_param(self.shapes.objects(shape, sh!("deactivated")).first()) {
631 stack.pop();
632 return Vec::new();
633 }
634 let (values, result_path) = match &view.path {
635 Some(path) => (self.eval_path(path, focus), Some(path.display())),
636 None => (vec![focus.to_string()], None),
637 };
638 let mut out = Vec::new();
639
640 self.check_cardinality(&view, focus, &values, result_path.as_deref(), &mut out);
641 self.check_value_type(&view, focus, &values, result_path.as_deref(), &mut out);
642 self.check_value_ranges(&view, focus, &values, result_path.as_deref(), &mut out);
643 self.check_strings(&view, focus, &values, result_path.as_deref(), &mut out);
644 self.check_property_pairs(&view, focus, &values, result_path.as_deref(), &mut out);
645 self.check_has_value_and_in(&view, focus, &values, result_path.as_deref(), &mut out);
646 self.check_nested_shapes(
647 &view,
648 focus,
649 &values,
650 result_path.as_deref(),
651 stack,
652 &mut out,
653 );
654 self.check_logical(&view, focus, stack, &mut out);
655 self.check_closed(&view, focus, &mut out);
656 self.check_qualified(
657 &view,
658 focus,
659 &values,
660 result_path.as_deref(),
661 stack,
662 &mut out,
663 );
664
665 stack.pop();
666 out
667 }
668
669 fn conforms(&self, shape: &str, focus: &str, stack: &mut Vec<(String, String)>) -> bool {
670 self.validate_shape(shape, focus, stack).is_empty()
671 }
672
673 fn check_cardinality(
674 &self,
675 view: &ShapeView<'_>,
676 focus: &str,
677 values: &[String],
678 path: Option<&str>,
679 out: &mut Vec<ValidationResult>,
680 ) {
681 for min in self.shapes.objects(view.id, sh!("minCount")) {
682 if let Some(n) = int_literal(&min) {
683 if values.len() < n as usize {
684 out.push(self.result(
685 view,
686 focus,
687 None,
688 component("MinCountConstraintComponent"),
689 path,
690 ));
691 }
692 }
693 }
694 for max in self.shapes.objects(view.id, sh!("maxCount")) {
695 if let Some(n) = int_literal(&max) {
696 if values.len() > n as usize {
697 out.push(self.result(
698 view,
699 focus,
700 None,
701 component("MaxCountConstraintComponent"),
702 path,
703 ));
704 }
705 }
706 }
707 }
708
709 fn check_value_type(
710 &self,
711 view: &ShapeView<'_>,
712 focus: &str,
713 values: &[String],
714 path: Option<&str>,
715 out: &mut Vec<ValidationResult>,
716 ) {
717 for kind in self.shapes.objects(view.id, sh!("nodeKind")) {
718 for v in values {
719 if !node_kind(v, &kind) {
720 out.push(self.result(
721 view,
722 focus,
723 Some(v.clone()),
724 component("NodeKindConstraintComponent"),
725 path,
726 ));
727 }
728 }
729 }
730 for class in self.shapes.objects(view.id, sh!("class")) {
731 for v in values {
732 if !self.data.is_instance_of(v, &class) {
733 out.push(self.result(
734 view,
735 focus,
736 Some(v.clone()),
737 component("ClassConstraintComponent"),
738 path,
739 ));
740 }
741 }
742 }
743 for datatype in self.shapes.objects(view.id, sh!("datatype")) {
744 for v in values {
745 if !datatype_matches(v, &datatype) {
746 out.push(self.result(
747 view,
748 focus,
749 Some(v.clone()),
750 component("DatatypeConstraintComponent"),
751 path,
752 ));
753 }
754 }
755 }
756 }
757
758 fn check_value_ranges(
759 &self,
760 view: &ShapeView<'_>,
761 focus: &str,
762 values: &[String],
763 path: Option<&str>,
764 out: &mut Vec<ValidationResult>,
765 ) {
766 let checks = [
767 (sh!("minExclusive"), "MinExclusiveConstraintComponent", 0_u8),
768 (sh!("minInclusive"), "MinInclusiveConstraintComponent", 1),
769 (sh!("maxExclusive"), "MaxExclusiveConstraintComponent", 2),
770 (sh!("maxInclusive"), "MaxInclusiveConstraintComponent", 3),
771 ];
772 for (pred, comp, mode) in checks {
773 for bound in self.shapes.objects(view.id, pred) {
774 for v in values {
775 let ok = compare_terms(v, &bound).is_some_and(|ord| match mode {
776 0 => ord.is_gt(),
777 1 => !ord.is_lt(),
778 2 => ord.is_lt(),
779 _ => !ord.is_gt(),
780 });
781 if !ok {
782 out.push(self.result(view, focus, Some(v.clone()), component(comp), path));
783 }
784 }
785 }
786 }
787 }
788
789 fn check_strings(
790 &self,
791 view: &ShapeView<'_>,
792 focus: &str,
793 values: &[String],
794 path: Option<&str>,
795 out: &mut Vec<ValidationResult>,
796 ) {
797 for min in self.shapes.objects(view.id, sh!("minLength")) {
798 if let Some(n) = int_literal(&min) {
799 for v in values {
800 if string_value(v).chars().count() < n as usize {
801 out.push(self.result(
802 view,
803 focus,
804 Some(v.clone()),
805 component("MinLengthConstraintComponent"),
806 path,
807 ));
808 }
809 }
810 }
811 }
812 for max in self.shapes.objects(view.id, sh!("maxLength")) {
813 if let Some(n) = int_literal(&max) {
814 for v in values {
815 if string_value(v).chars().count() > n as usize {
816 out.push(self.result(
817 view,
818 focus,
819 Some(v.clone()),
820 component("MaxLengthConstraintComponent"),
821 path,
822 ));
823 }
824 }
825 }
826 }
827 for pattern in self.shapes.objects(view.id, sh!("pattern")) {
828 let flags = self
829 .shapes
830 .objects(view.id, sh!("flags"))
831 .first()
832 .and_then(|f| literal_lexical(f).map(|l| l.value))
833 .unwrap_or_default();
834 let pat = literal_lexical(&pattern)
835 .map(|l| l.value)
836 .unwrap_or(pattern);
837 let inline: String = ['i', 'm', 's', 'x']
838 .iter()
839 .filter(|c| flags.contains(**c))
840 .collect();
841 let full = if inline.is_empty() {
842 pat
843 } else {
844 format!("(?{inline}){pat}")
845 };
846 let re = regex_lite::Regex::new(&full);
847 for v in values {
848 if re.as_ref().map_or(true, |r| !r.is_match(&string_value(v))) {
849 out.push(self.result(
850 view,
851 focus,
852 Some(v.clone()),
853 component("PatternConstraintComponent"),
854 path,
855 ));
856 }
857 }
858 }
859 for head in self.shapes.objects(view.id, sh!("languageIn")) {
860 let allowed = self
861 .shapes
862 .list(&head)
863 .unwrap_or_default()
864 .into_iter()
865 .filter_map(|t| literal_lexical(&t).map(|l| l.value.to_ascii_lowercase()))
866 .collect::<BTreeSet<_>>();
867 for v in values {
868 let lang = literal_lexical(v)
869 .and_then(|l| l.lang)
870 .map(|l| l.to_ascii_lowercase());
871 if lang.is_none_or(|l| !allowed.contains("*") && !allowed.contains(&l)) {
872 out.push(self.result(
873 view,
874 focus,
875 Some(v.clone()),
876 component("LanguageInConstraintComponent"),
877 path,
878 ));
879 }
880 }
881 }
882 if bool_param(self.shapes.objects(view.id, sh!("uniqueLang")).first()) {
883 let mut seen = BTreeSet::new();
884 let mut duplicate = false;
885 for v in values {
886 if let Some(lang) = literal_lexical(v).and_then(|l| l.lang) {
887 if !seen.insert(lang.to_ascii_lowercase()) {
888 duplicate = true;
889 }
890 }
891 }
892 if duplicate {
893 out.push(self.result(
894 view,
895 focus,
896 None,
897 component("UniqueLangConstraintComponent"),
898 path,
899 ));
900 }
901 }
902 }
903
904 fn check_property_pairs(
905 &self,
906 view: &ShapeView<'_>,
907 focus: &str,
908 values: &[String],
909 path: Option<&str>,
910 out: &mut Vec<ValidationResult>,
911 ) {
912 for other in self.shapes.objects(view.id, sh!("equals")) {
913 let other_values = self.eval_path(&Path::Predicate(other), focus);
914 if set(values) != set(&other_values) {
915 out.push(self.result(
916 view,
917 focus,
918 None,
919 component("EqualsConstraintComponent"),
920 path,
921 ));
922 }
923 }
924 for other in self.shapes.objects(view.id, sh!("disjoint")) {
925 let other_values = self.eval_path(&Path::Predicate(other), focus);
926 if values.iter().any(|v| other_values.contains(v)) {
927 out.push(self.result(
928 view,
929 focus,
930 None,
931 component("DisjointConstraintComponent"),
932 path,
933 ));
934 }
935 }
936 for other in self.shapes.objects(view.id, sh!("lessThan")) {
937 let other_values = self.eval_path(&Path::Predicate(other), focus);
938 for v in values {
939 if other_values
940 .iter()
941 .any(|o| compare_terms(v, o).is_none_or(|ord| !ord.is_lt()))
942 {
943 out.push(self.result(
944 view,
945 focus,
946 Some(v.clone()),
947 component("LessThanConstraintComponent"),
948 path,
949 ));
950 }
951 }
952 }
953 for other in self.shapes.objects(view.id, sh!("lessThanOrEquals")) {
954 let other_values = self.eval_path(&Path::Predicate(other), focus);
955 for v in values {
956 if other_values
957 .iter()
958 .any(|o| compare_terms(v, o).is_none_or(|ord| ord.is_gt()))
959 {
960 out.push(self.result(
961 view,
962 focus,
963 Some(v.clone()),
964 component("LessThanOrEqualsConstraintComponent"),
965 path,
966 ));
967 }
968 }
969 }
970 }
971
972 fn check_has_value_and_in(
973 &self,
974 view: &ShapeView<'_>,
975 focus: &str,
976 values: &[String],
977 path: Option<&str>,
978 out: &mut Vec<ValidationResult>,
979 ) {
980 for required in self.shapes.objects(view.id, sh!("hasValue")) {
981 if !values.contains(&required) {
982 out.push(self.result(
983 view,
984 focus,
985 Some(required),
986 component("HasValueConstraintComponent"),
987 path,
988 ));
989 }
990 }
991 for head in self.shapes.objects(view.id, sh!("in")) {
992 let allowed = self.shapes.list(&head).unwrap_or_default();
993 for v in values {
994 if !allowed.contains(v) {
995 out.push(self.result(
996 view,
997 focus,
998 Some(v.clone()),
999 component("InConstraintComponent"),
1000 path,
1001 ));
1002 }
1003 }
1004 }
1005 }
1006
1007 fn check_nested_shapes(
1008 &self,
1009 view: &ShapeView<'_>,
1010 focus: &str,
1011 values: &[String],
1012 path: Option<&str>,
1013 stack: &mut Vec<(String, String)>,
1014 out: &mut Vec<ValidationResult>,
1015 ) {
1016 for node_shape in self.shapes.objects(view.id, sh!("node")) {
1017 for v in values {
1018 if !self.conforms(&node_shape, v, stack) {
1019 out.push(self.result(
1020 view,
1021 focus,
1022 Some(v.clone()),
1023 component("NodeConstraintComponent"),
1024 path,
1025 ));
1026 }
1027 }
1028 }
1029 for property_shape in self.shapes.objects(view.id, sh!("property")) {
1030 out.extend(self.validate_shape(&property_shape, focus, stack));
1031 }
1032 }
1033
1034 fn check_logical(
1035 &self,
1036 view: &ShapeView<'_>,
1037 focus: &str,
1038 stack: &mut Vec<(String, String)>,
1039 out: &mut Vec<ValidationResult>,
1040 ) {
1041 for s in self.shapes.objects(view.id, sh!("not")) {
1042 if self.conforms(&s, focus, stack) {
1043 out.push(self.result(
1044 view,
1045 focus,
1046 Some(focus.to_string()),
1047 component("NotConstraintComponent"),
1048 None,
1049 ));
1050 }
1051 }
1052 for head in self.shapes.objects(view.id, sh!("and")) {
1053 let shapes = self.shapes.list(&head).unwrap_or_default();
1054 if shapes.iter().any(|s| !self.conforms(s, focus, stack)) {
1055 out.push(self.result(
1056 view,
1057 focus,
1058 Some(focus.to_string()),
1059 component("AndConstraintComponent"),
1060 None,
1061 ));
1062 }
1063 }
1064 for head in self.shapes.objects(view.id, sh!("or")) {
1065 let shapes = self.shapes.list(&head).unwrap_or_default();
1066 if !shapes.iter().any(|s| self.conforms(s, focus, stack)) {
1067 out.push(self.result(
1068 view,
1069 focus,
1070 Some(focus.to_string()),
1071 component("OrConstraintComponent"),
1072 None,
1073 ));
1074 }
1075 }
1076 for head in self.shapes.objects(view.id, sh!("xone")) {
1077 let shapes = self.shapes.list(&head).unwrap_or_default();
1078 let n = shapes
1079 .iter()
1080 .filter(|s| self.conforms(s, focus, stack))
1081 .count();
1082 if n != 1 {
1083 out.push(self.result(
1084 view,
1085 focus,
1086 Some(focus.to_string()),
1087 component("XoneConstraintComponent"),
1088 None,
1089 ));
1090 }
1091 }
1092 }
1093
1094 fn check_closed(&self, view: &ShapeView<'_>, focus: &str, out: &mut Vec<ValidationResult>) {
1095 if !bool_param(self.shapes.objects(view.id, sh!("closed")).first()) {
1096 return;
1097 }
1098 let mut allowed = BTreeSet::new();
1099 for prop_shape in self.shapes.objects(view.id, sh!("property")) {
1100 if let Some(path_node) = self.shapes.objects(&prop_shape, sh!("path")).first() {
1101 if is_iri(path_node) {
1102 allowed.insert(path_node.clone());
1103 }
1104 }
1105 }
1106 for head in self.shapes.objects(view.id, sh!("ignoredProperties")) {
1107 for pred in self.shapes.list(&head).unwrap_or_default() {
1108 allowed.insert(pred);
1109 }
1110 }
1111 for pred in self.data.predicates_for_subject(focus) {
1112 if !allowed.contains(&pred) {
1113 out.push(self.result(
1114 view,
1115 focus,
1116 Some(pred),
1117 component("ClosedConstraintComponent"),
1118 None,
1119 ));
1120 }
1121 }
1122 }
1123
1124 fn check_qualified(
1125 &self,
1126 view: &ShapeView<'_>,
1127 focus: &str,
1128 values: &[String],
1129 path: Option<&str>,
1130 stack: &mut Vec<(String, String)>,
1131 out: &mut Vec<ValidationResult>,
1132 ) {
1133 let Some(qshape) = self
1134 .shapes
1135 .objects(view.id, sh!("qualifiedValueShape"))
1136 .first()
1137 .cloned()
1138 else {
1139 return;
1140 };
1141 let sibling_shapes = self.qualified_sibling_shapes(view.id, &qshape);
1142 let mut count = 0;
1143 for value in values {
1144 if !self.conforms(&qshape, value, stack) {
1145 continue;
1146 }
1147 if sibling_shapes
1148 .iter()
1149 .any(|sibling| self.conforms(sibling, value, stack))
1150 {
1151 continue;
1152 }
1153 count += 1;
1154 }
1155 for min in self.shapes.objects(view.id, sh!("qualifiedMinCount")) {
1156 if let Some(n) = int_literal(&min) {
1157 if count < n as usize {
1158 out.push(self.result(
1159 view,
1160 focus,
1161 None,
1162 component("QualifiedMinCountConstraintComponent"),
1163 path,
1164 ));
1165 }
1166 }
1167 }
1168 for max in self.shapes.objects(view.id, sh!("qualifiedMaxCount")) {
1169 if let Some(n) = int_literal(&max) {
1170 if count > n as usize {
1171 out.push(self.result(
1172 view,
1173 focus,
1174 None,
1175 component("QualifiedMaxCountConstraintComponent"),
1176 path,
1177 ));
1178 }
1179 }
1180 }
1181 }
1182
1183 fn qualified_sibling_shapes(&self, property_shape: &str, qshape: &str) -> Vec<String> {
1184 if !bool_param(
1185 self.shapes
1186 .objects(property_shape, sh!("qualifiedValueShapesDisjoint"))
1187 .first(),
1188 ) {
1189 return Vec::new();
1190 }
1191
1192 let mut siblings = Vec::new();
1193 for parent_shape in self.shapes.subjects(sh!("property"), property_shape) {
1194 for sibling_property_shape in self.shapes.objects(&parent_shape, sh!("property")) {
1195 siblings.extend(
1196 self.shapes
1197 .objects(&sibling_property_shape, sh!("qualifiedValueShape"))
1198 .into_iter()
1199 .filter(|sibling| sibling != qshape),
1200 );
1201 }
1202 }
1203 unique(siblings)
1204 }
1205
1206 fn result(
1207 &self,
1208 view: &ShapeView<'_>,
1209 focus: &str,
1210 value: Option<String>,
1211 component: String,
1212 path: Option<&str>,
1213 ) -> ValidationResult {
1214 ValidationResult {
1215 focus_node: focus.to_string(),
1216 value_node: value,
1217 result_path: path.map(str::to_string),
1218 source_shape: view.id.to_string(),
1219 source_constraint_component: component,
1220 severity: view.severity.clone(),
1221 messages: view.messages.clone(),
1222 }
1223 }
1224
1225 fn parse_path(&self, node: &str) -> Result<Path, ShaclError> {
1226 if is_iri(node) {
1227 return Ok(Path::Predicate(node.to_string()));
1228 }
1229 if let Some(p) = self.shapes.objects(node, sh!("inversePath")).first() {
1230 return Ok(Path::Inverse(Box::new(self.parse_path(p)?)));
1231 }
1232 if let Some(head) = self.shapes.objects(node, sh!("alternativePath")).first() {
1233 let paths = self
1234 .shapes
1235 .list(head)?
1236 .iter()
1237 .map(|n| self.parse_path(n))
1238 .collect::<Result<Vec<_>, _>>()?;
1239 return Ok(Path::Alternative(paths));
1240 }
1241 if let Some(p) = self.shapes.objects(node, sh!("zeroOrMorePath")).first() {
1242 return Ok(Path::ZeroOrMore(Box::new(self.parse_path(p)?)));
1243 }
1244 if let Some(p) = self.shapes.objects(node, sh!("oneOrMorePath")).first() {
1245 return Ok(Path::OneOrMore(Box::new(self.parse_path(p)?)));
1246 }
1247 if let Some(p) = self.shapes.objects(node, sh!("zeroOrOnePath")).first() {
1248 return Ok(Path::ZeroOrOne(Box::new(self.parse_path(p)?)));
1249 }
1250 let paths = self
1251 .shapes
1252 .list(node)?
1253 .iter()
1254 .map(|n| self.parse_path(n))
1255 .collect::<Result<Vec<_>, _>>()?;
1256 Ok(Path::Sequence(paths))
1257 }
1258
1259 fn eval_path(&self, path: &Path, start: &str) -> Vec<String> {
1260 match path {
1261 Path::Predicate(p) => unique(self.data.objects(start, p)),
1262 Path::Inverse(inner) => match inner.as_ref() {
1266 Path::Predicate(p) => unique(self.data.subjects_with(p, start)),
1267 _ => unique(
1268 self.data
1269 .all_nodes()
1270 .into_iter()
1271 .filter(|n| self.eval_path(inner, n).contains(&start.to_string()))
1272 .collect(),
1273 ),
1274 },
1275 Path::Sequence(paths) => {
1276 let mut frontier = vec![start.to_string()];
1277 for p in paths {
1278 let mut next = Vec::new();
1279 for n in &frontier {
1280 next.extend(self.eval_path(p, n));
1281 }
1282 frontier = unique(next);
1283 }
1284 frontier
1285 }
1286 Path::Alternative(paths) => unique(
1287 paths
1288 .iter()
1289 .flat_map(|p| self.eval_path(p, start))
1290 .collect::<Vec<_>>(),
1291 ),
1292 Path::ZeroOrOne(p) => {
1293 let mut out = vec![start.to_string()];
1294 out.extend(self.eval_path(p, start));
1295 unique(out)
1296 }
1297 Path::ZeroOrMore(p) => {
1298 let mut out = vec![start.to_string()];
1299 out.extend(self.transitive_path(p, start));
1300 unique(out)
1301 }
1302 Path::OneOrMore(p) => self.transitive_path(p, start),
1303 }
1304 }
1305
1306 fn transitive_path(&self, path: &Path, start: &str) -> Vec<String> {
1307 let mut out = Vec::new();
1308 let mut seen = HashSet::new();
1309 let mut stack = self.eval_path(path, start);
1310 while let Some(n) = stack.pop() {
1311 if !seen.insert(n.clone()) {
1312 continue;
1313 }
1314 out.push(n.clone());
1315 stack.extend(self.eval_path(path, &n));
1316 }
1317 unique(out)
1318 }
1319}
1320
1321fn component(local: &str) -> String {
1322 format!("{SH}{local}")
1323}
1324
1325fn unique(mut v: Vec<String>) -> Vec<String> {
1326 v.sort();
1327 v.dedup();
1328 v
1329}
1330
1331fn set(values: &[String]) -> BTreeSet<String> {
1332 values.iter().cloned().collect()
1333}
1334
1335use crate::terms::{iri_content as strip_iri, is_iri};
1336
1337fn bool_param(v: Option<&String>) -> bool {
1338 v.is_some_and(|t| {
1339 literal_lexical(t)
1340 .map(|l| l.value == "true" || l.value == "1")
1341 .unwrap_or(false)
1342 })
1343}
1344
1345fn int_literal(t: &str) -> Option<i64> {
1346 literal_lexical(t)?.value.parse().ok()
1347}
1348
1349#[derive(Debug, Clone)]
1350struct Lit {
1351 value: String,
1352 datatype: Option<String>,
1353 lang: Option<String>,
1354}
1355
1356fn literal_lexical(token: &str) -> Option<Lit> {
1357 if !token.starts_with('"') {
1358 return None;
1359 }
1360 let bytes = token.as_bytes();
1361 let mut i = 1;
1362 while i < bytes.len() {
1363 match bytes[i] {
1364 b'\\' => i += 2,
1365 b'"' => break,
1366 _ => i += 1,
1367 }
1368 }
1369 let value = unescape_nt(&token[1..i.min(token.len())]);
1370 let rest = token.get(i + 1..).unwrap_or("");
1371 let datatype = rest
1372 .strip_prefix("^^<")
1373 .and_then(|s| s.strip_suffix('>'))
1374 .map(|s| format!("<{s}>"));
1375 let lang = rest.strip_prefix('@').map(str::to_string);
1376 Some(Lit {
1377 value,
1378 datatype,
1379 lang,
1380 })
1381}
1382
1383fn literal_datatype(token: &str) -> Option<String> {
1384 let lit = literal_lexical(token)?;
1385 if lit.lang.is_some() {
1386 Some(RDF_LANG_STRING.to_string())
1387 } else {
1388 Some(lit.datatype.unwrap_or_else(|| XSD_STRING.to_string()))
1389 }
1390}
1391
1392fn datatype_matches(value: &str, datatype: &str) -> bool {
1393 literal_datatype(value).is_some_and(|dt| dt == datatype)
1394}
1395
1396fn node_kind(value: &str, kind: &str) -> bool {
1397 match kind {
1398 sh!("IRI") => is_iri(value),
1399 sh!("BlankNode") => value.starts_with("_:"),
1400 sh!("Literal") => value.starts_with('"'),
1401 sh!("BlankNodeOrIRI") => value.starts_with("_:") || is_iri(value),
1402 sh!("BlankNodeOrLiteral") => value.starts_with("_:") || value.starts_with('"'),
1403 sh!("IRIOrLiteral") => is_iri(value) || value.starts_with('"'),
1404 _ => true,
1405 }
1406}
1407
1408fn string_value(value: &str) -> String {
1409 if let Some(l) = literal_lexical(value) {
1410 l.value
1411 } else if let Some(iri) = strip_iri(value) {
1412 iri.to_string()
1413 } else {
1414 value.to_string()
1415 }
1416}
1417
1418fn compare_terms(a: &str, b: &str) -> Option<std::cmp::Ordering> {
1419 let av = literal_lexical(a)
1420 .map(|l| l.value)
1421 .unwrap_or_else(|| string_value(a));
1422 let bv = literal_lexical(b)
1423 .map(|l| l.value)
1424 .unwrap_or_else(|| string_value(b));
1425 match (av.parse::<f64>(), bv.parse::<f64>()) {
1426 (Ok(x), Ok(y)) => x.partial_cmp(&y),
1427 _ => Some(av.cmp(&bv)),
1428 }
1429}
1430
1431fn term_json_string(token: &str) -> String {
1432 strip_iri(token).unwrap_or(token).to_string()
1433}
1434
1435fn escape_string(s: &str) -> String {
1436 s.replace('\\', "\\\\").replace('"', "\\\"")
1437}
1438
1439fn unescape_nt(s: &str) -> String {
1440 crate::terms::unescape_literal(s)
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445 use super::*;
1446
1447 fn graph(triples: &[(&str, &str, &str)]) -> DataGraph {
1448 DataGraph::from_triples(
1449 triples
1450 .iter()
1451 .map(|(s, p, o)| (s.to_string(), p.to_string(), o.to_string()))
1452 .collect(),
1453 )
1454 }
1455
1456 #[test]
1457 fn graph_view_covers_subclasses_instances_and_all_lookup_shapes() {
1458 let data = graph(&[
1459 ("<alice>", RDF_TYPE, "<Child>"),
1460 ("<Child>", RDFS_SUBCLASS_OF, "<Parent>"),
1461 ("<Parent>", RDFS_SUBCLASS_OF, "<Ancestor>"),
1462 ("<Ancestor>", RDFS_SUBCLASS_OF, "<Child>"),
1463 ("<alice>", "<p>", "<bob>"),
1464 ("<alice>", "<q>", "\"value\""),
1465 ("<bob>", "<p>", "<carol>"),
1466 ]);
1467 assert!(data.has("<alice>", "<p>", "<bob>"));
1468 assert!(!data.has("<bob>", "<q>", "<alice>"));
1469 assert_eq!(data.objects("<alice>", "<p>"), ["<bob>"]);
1470 assert_eq!(data.subjects_with("<p>", "<bob>"), ["<alice>"]);
1471 assert_eq!(data.subjects_of("<p>"), ["<alice>", "<bob>"]);
1472 assert_eq!(data.objects_of("<p>"), ["<bob>", "<carol>"]);
1473 assert_eq!(data.predicates_for_subject("<bob>"), ["<p>"]);
1474 assert!(data.all_nodes().contains(&"<alice>".to_string()));
1475 assert!(data.is_subclass_of("<Child>", "<Child>"));
1476 assert!(data.is_subclass_of("<Child>", "<Ancestor>"));
1477 assert!(!data.is_subclass_of("<Unrelated>", "<Ancestor>"));
1478 assert!(data.subclasses_of("<Parent>").contains("<Child>"));
1479 assert_eq!(data.instances_of("<Ancestor>"), ["<alice>"]);
1480 assert!(data.is_instance_of("<alice>", "<Parent>"));
1481 assert!(!data.is_instance_of("<bob>", "<Parent>"));
1482 }
1483
1484 #[test]
1485 fn shapes_lists_targets_severity_and_parse_errors_are_explicit() {
1486 assert!(matches!(
1487 ShaclShapes::parse_turtle("@prefix sh: <http://www.w3.org/ns/shacl#> . ["),
1488 Err(ShaclError::Parse(_))
1489 ));
1490 let shapes = ShaclShapes {
1491 graph: graph(&[
1492 ("<shape-node>", sh!("targetNode"), "<alice>"),
1493 ("<shape-class>", sh!("targetClass"), "<Person>"),
1494 ("<shape-subjects>", sh!("targetSubjectsOf"), "<p>"),
1495 ("<shape-objects>", sh!("targetObjectsOf"), "<q>"),
1496 ("<shape-type>", RDF_TYPE, sh!("NodeShape")),
1497 ("<shape-property>", RDF_TYPE, sh!("PropertyShape")),
1498 ("<shape-rdfs>", RDF_TYPE, RDFS_CLASS),
1499 ("<shape-owl>", RDF_TYPE, OWL_CLASS),
1500 ("_:one", RDF_FIRST, "\"a\""),
1501 ("_:one", RDF_REST, "_:two"),
1502 ("_:two", RDF_FIRST, "\"b\""),
1503 ("_:two", RDF_REST, RDF_NIL),
1504 ]),
1505 };
1506 assert_eq!(
1507 shapes.objects("<shape-node>", sh!("targetNode")),
1508 ["<alice>"]
1509 );
1510 assert_eq!(
1511 shapes.subjects(sh!("targetNode"), "<alice>"),
1512 ["<shape-node>"]
1513 );
1514 assert!(shapes.has("<shape-type>", RDF_TYPE, sh!("NodeShape")));
1515 assert_eq!(shapes.list(RDF_NIL).unwrap(), Vec::<String>::new());
1516 assert_eq!(shapes.list("_:one").unwrap(), ["\"a\"", "\"b\""]);
1517 assert_eq!(shapes.target_shapes().len(), 8);
1518
1519 let missing = ShaclShapes {
1520 graph: graph(&[("_:bad", RDF_FIRST, "\"a\"")]),
1521 };
1522 assert!(matches!(
1523 missing.list("_:bad"),
1524 Err(ShaclError::MalformedList(_))
1525 ));
1526 let cyclic = ShaclShapes {
1527 graph: graph(&[
1528 ("_:cycle", RDF_FIRST, "\"a\""),
1529 ("_:cycle", RDF_REST, "_:cycle"),
1530 ]),
1531 };
1532 assert!(matches!(
1533 cyclic.list("_:cycle"),
1534 Err(ShaclError::MalformedList(_))
1535 ));
1536
1537 for (token, expected) in [
1538 (Some(sh!("Info").to_string()), Severity::Info),
1539 (Some(sh!("Warning").to_string()), Severity::Warning),
1540 (Some(sh!("Violation").to_string()), Severity::Violation),
1541 (None, Severity::Violation),
1542 (
1543 Some("<http://ex/custom>".to_string()),
1544 Severity::Other("http://ex/custom".into()),
1545 ),
1546 ] {
1547 assert_eq!(Severity::from_token(token), expected);
1548 }
1549 assert_eq!(Severity::Info.iri(), format!("{SH}Info"));
1550 assert_eq!(
1551 Severity::Other("http://ex/custom".into()).iri(),
1552 "http://ex/custom"
1553 );
1554 }
1555
1556 #[test]
1557 fn reports_serialize_empty_and_detailed_results() {
1558 let empty = ValidationReport {
1559 conforms: true,
1560 results: vec![],
1561 };
1562 assert!(empty.to_json().contains("\"schemaVersion\": 1"));
1563 assert!(empty.to_turtle().contains("conforms> true ."));
1564
1565 let report = ValidationReport {
1566 conforms: false,
1567 results: vec![ValidationResult {
1568 focus_node: "<http://ex/alice>".into(),
1569 value_node: Some("\"bad\"".into()),
1570 result_path: Some("<http://ex/p>\"quoted".into()),
1571 source_shape: "_:shape".into(),
1572 source_constraint_component: component("PatternConstraintComponent"),
1573 severity: Severity::Warning,
1574 messages: vec!["line \\\"quoted\\\"".into(), "second".into()],
1575 }],
1576 };
1577 let json = report.to_json();
1578 assert!(json.contains("http://ex/alice"));
1579 assert!(json.contains("PatternConstraintComponent"));
1580 let turtle = report.to_turtle();
1581 assert!(turtle.contains("ValidationResult"));
1582 assert!(turtle.contains("resultPath"));
1583 assert!(turtle.contains("resultMessage"));
1584 assert!(turtle.contains("Warning"));
1585 }
1586
1587 #[test]
1588 fn path_display_parse_and_evaluation_cover_every_path_form() {
1589 let data = graph(&[
1590 ("<A>", "<p>", "<B>"),
1591 ("<B>", "<p>", "<C>"),
1592 ("<C>", "<p>", "<A>"),
1593 ("<A>", "<q>", "<C>"),
1594 ]);
1595 let shapes = ShaclShapes::parse_turtle(
1596 r#"
1597 @prefix sh: <http://www.w3.org/ns/shacl#> .
1598 @prefix ex: <http://ex/> .
1599 ex:inverse sh:path [ sh:inversePath <http://data/p> ] .
1600 ex:alternative sh:path [ sh:alternativePath ( <http://data/p> <http://data/q> ) ] .
1601 ex:zeroMore sh:path [ sh:zeroOrMorePath <http://data/p> ] .
1602 ex:oneMore sh:path [ sh:oneOrMorePath <http://data/p> ] .
1603 ex:zeroOne sh:path [ sh:zeroOrOnePath <http://data/p> ] .
1604 ex:sequence sh:path ( <http://data/p> <http://data/q> ) .
1605 "#,
1606 )
1607 .unwrap();
1608 let validator = Validator {
1609 data: &data,
1610 shapes: &shapes,
1611 };
1612 for id in [
1613 "inverse",
1614 "alternative",
1615 "zeroMore",
1616 "oneMore",
1617 "zeroOne",
1618 "sequence",
1619 ] {
1620 let shape = format!("<http://ex/{id}>");
1621 let node = shapes.objects(&shape, sh!("path")).remove(0);
1622 assert!(!validator.parse_path(&node).unwrap().display().is_empty());
1623 }
1624
1625 let p = Path::Predicate("<p>".into());
1626 let q = Path::Predicate("<q>".into());
1627 assert_eq!(p.display(), "<p>");
1628 assert_eq!(Path::Inverse(Box::new(p.clone())).display(), "^<p>");
1629 assert_eq!(
1630 Path::Sequence(vec![p.clone(), q.clone()]).display(),
1631 "(<p> <q>)"
1632 );
1633 assert_eq!(
1634 Path::Alternative(vec![p.clone(), q.clone()]).display(),
1635 "(<p>|<q>)"
1636 );
1637 assert_eq!(Path::ZeroOrMore(Box::new(p.clone())).display(), "<p>*");
1638 assert_eq!(Path::OneOrMore(Box::new(p.clone())).display(), "<p>+");
1639 assert_eq!(Path::ZeroOrOne(Box::new(p.clone())).display(), "<p>?");
1640
1641 assert_eq!(validator.eval_path(&p, "<A>"), ["<B>"]);
1642 assert_eq!(
1643 validator.eval_path(&Path::Inverse(Box::new(p.clone())), "<B>"),
1644 ["<A>"]
1645 );
1646 assert!(validator
1647 .eval_path(
1648 &Path::Inverse(Box::new(Path::Sequence(vec![p.clone(), p.clone()]))),
1649 "<C>"
1650 )
1651 .contains(&"<A>".to_string()));
1652 assert_eq!(
1653 validator.eval_path(&Path::Sequence(vec![p.clone(), p.clone()]), "<A>"),
1654 ["<C>"]
1655 );
1656 assert_eq!(
1657 validator.eval_path(&Path::Alternative(vec![p.clone(), q]), "<A>"),
1658 ["<B>", "<C>"]
1659 );
1660 assert!(validator
1661 .eval_path(&Path::ZeroOrOne(Box::new(p.clone())), "<A>")
1662 .contains(&"<A>".into()));
1663 assert!(validator
1664 .eval_path(&Path::ZeroOrMore(Box::new(p.clone())), "<A>")
1665 .contains(&"<C>".into()));
1666 assert!(validator
1667 .eval_path(&Path::OneOrMore(Box::new(p)), "<A>")
1668 .contains(&"<B>".into()));
1669 }
1670
1671 #[test]
1672 fn term_helpers_cover_literals_node_kinds_ordering_and_escaping() {
1673 assert!(bool_param(Some(
1674 &"\"true\"^^<http://www.w3.org/2001/XMLSchema#boolean>".into()
1675 )));
1676 assert!(bool_param(Some(&"\"1\"".into())));
1677 assert!(!bool_param(Some(&"<iri>".into())));
1678 assert_eq!(int_literal("\"-12\""), Some(-12));
1679 assert_eq!(int_literal("\"nope\""), None);
1680 assert!(literal_lexical("<iri>").is_none());
1681 let escaped = literal_lexical("\"a\\\"b\\n\"@EN").unwrap();
1682 assert_eq!(escaped.value, "a\"b\n");
1683 assert_eq!(escaped.lang.as_deref(), Some("EN"));
1684 assert_eq!(
1685 literal_datatype("\"x\"@en").as_deref(),
1686 Some(RDF_LANG_STRING)
1687 );
1688 assert_eq!(literal_datatype("\"x\"").as_deref(), Some(XSD_STRING));
1689 assert!(datatype_matches("\"x\"", XSD_STRING));
1690 assert!(!datatype_matches("<iri>", XSD_STRING));
1691
1692 for (kind, value, expected) in [
1693 (sh!("IRI"), "<iri>", true),
1694 (sh!("BlankNode"), "_:b", true),
1695 (sh!("Literal"), "\"x\"", true),
1696 (sh!("BlankNodeOrIRI"), "<iri>", true),
1697 (sh!("BlankNodeOrLiteral"), "\"x\"", true),
1698 (sh!("IRIOrLiteral"), "\"x\"", true),
1699 ("<unknown-kind>", "anything", true),
1700 (sh!("IRI"), "\"x\"", false),
1701 ] {
1702 assert_eq!(node_kind(value, kind), expected);
1703 }
1704 assert_eq!(string_value("\"hello\"@en"), "hello");
1705 assert_eq!(string_value("<http://ex/a>"), "http://ex/a");
1706 assert_eq!(string_value("_:b"), "_:b");
1707 assert_eq!(
1708 compare_terms("\"2\"", "\"10\""),
1709 Some(std::cmp::Ordering::Less)
1710 );
1711 assert_eq!(
1712 compare_terms("\"z\"", "\"a\""),
1713 Some(std::cmp::Ordering::Greater)
1714 );
1715 assert_eq!(term_json_string("<http://ex/a>"), "http://ex/a");
1716 assert_eq!(term_json_string("_:b"), "_:b");
1717 assert_eq!(escape_string("a\\\"b"), "a\\\\\\\"b");
1718 assert_eq!(unescape_nt("a\\tb"), "a\tb");
1719 assert_eq!(set(&["b".into(), "a".into(), "b".into()]).len(), 2);
1720 assert_eq!(unique(vec!["b".into(), "a".into(), "b".into()]), ["a", "b"]);
1721 }
1722}