1use crate::frozen::FrozenIndexedDataset;
16use crate::path::succ;
17use crate::sparql::{FunctionDef, SparqlDiagnostic, SparqlExecutor};
18use crate::validate::{
19 UnsupportedPolicy, ValidationGraphMode, ValidationOptions, apply_message_template,
20 entry_shape_name_selected, graph_union, is_boolean_true,
21};
22use crate::value::{compare_terms, value_type_holds};
23use oxrdf::{BlankNode, Graph, Literal, NamedNode, NamedNodeRef, NamedOrBlankNode, Term, Triple};
24use shifty_algebra::value_type::{Bound, ValueType};
25use shifty_algebra::{NodeKindSet, Path, Severity, SparqlConstraint, SparqlQueryKind};
26use shifty_parse::graph::{Loaded, term_to_node};
27use shifty_parse::lower::canonical_sparql_query;
28use shifty_parse::path::parse_path;
29use shifty_parse::vocab;
30use std::cell::RefCell;
31use std::cmp::Ordering;
32use std::collections::{HashMap, HashSet};
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub struct ValidationResult {
37 pub focus: Term,
38 pub path: Option<Term>,
40 pub value: Option<Term>,
41 pub component: NamedNode,
42 pub source_shape: Term,
43 pub severity: NamedNode,
46 pub messages: Vec<Term>,
48 pub sparql_diagnostic: Option<SparqlDiagnostic>,
53}
54
55#[derive(Debug, Clone)]
56pub struct ValidationReport {
57 pub conforms: bool,
58 pub results: Vec<ValidationResult>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct PropertyWitness {
73 pub focus: Term,
74 pub shape: Term,
76 pub key: Term,
77 pub values: Vec<Term>,
83}
84
85pub fn validate_report(shapes: &Loaded, data: &Graph) -> ValidationReport {
87 validate_report_with_options(shapes, data, &ValidationOptions::default())
88}
89
90pub fn evaluate_function_expression(shapes: &Loaded, expr: &str) -> Result<Option<Term>, String> {
96 let frozen = FrozenIndexedDataset::from_graph(&shapes.graph);
97 let mut sparql = SparqlExecutor::from_frozen(frozen, false);
98 sparql.set_functions(collect_functions(shapes), UnsupportedPolicy::Ignore);
101 let mut prologue = String::new();
102 if let Some(base) = &shapes.base {
103 prologue.push_str(&format!("BASE <{base}>\n"));
104 }
105 for (prefix, namespace) in &shapes.prefixes {
106 prologue.push_str(&format!("PREFIX {prefix}: <{namespace}>\n"));
107 }
108 sparql.evaluate_expression(&prologue, expr)
109}
110
111pub fn validate_report_with_options(
113 shapes: &Loaded,
114 data: &Graph,
115 options: &ValidationOptions,
116) -> ValidationReport {
117 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
118 let frozen = if has_shapes_graph {
119 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
120 } else {
121 FrozenIndexedDataset::from_graph(data)
122 };
123 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
124}
125
126pub fn validate_report_graphs(shapes: &Loaded, data: &Graph) -> ValidationReport {
128 validate_report_graphs_with_mode_and_options(
129 shapes,
130 data,
131 ValidationGraphMode::default(),
132 &ValidationOptions::default(),
133 )
134}
135
136pub fn validate_report_graphs_with_mode(
138 shapes: &Loaded,
139 data: &Graph,
140 mode: ValidationGraphMode,
141) -> ValidationReport {
142 validate_report_graphs_with_mode_and_options(shapes, data, mode, &ValidationOptions::default())
143}
144
145pub fn validate_report_graphs_with_mode_and_options(
147 shapes: &Loaded,
148 data: &Graph,
149 mode: ValidationGraphMode,
150 options: &ValidationOptions,
151) -> ValidationReport {
152 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
153 match mode {
154 ValidationGraphMode::Data => {
155 let frozen = if has_shapes_graph {
156 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
157 } else {
158 FrozenIndexedDataset::from_graph(data)
159 };
160 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
161 }
162 ValidationGraphMode::Union => {
163 let frozen = if has_shapes_graph {
164 FrozenIndexedDataset::from_graph_union_with_shapes(data, &shapes.graph)
165 } else {
166 FrozenIndexedDataset::from_graph_union(data, &shapes.graph)
167 };
168 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
169 }
170 ValidationGraphMode::UnionAll => {
171 let union = graph_union(data, &shapes.graph);
172 let frozen = if has_shapes_graph {
173 FrozenIndexedDataset::from_graphs(&union, &shapes.graph)
174 } else {
175 FrozenIndexedDataset::from_graph(&union)
176 };
177 validate_report_context(shapes, &union, frozen, has_shapes_graph, options)
178 }
179 }
180}
181
182pub fn property_witnesses_graphs_with_mode(
190 shapes: &Loaded,
191 data: &Graph,
192 mode: ValidationGraphMode,
193 key_path: Option<&Path>,
194) -> Vec<PropertyWitness> {
195 property_witnesses_graphs_with_mode_and_options(
196 shapes,
197 data,
198 mode,
199 key_path,
200 &ValidationOptions::default(),
201 )
202}
203
204pub fn property_witnesses_graphs_with_mode_and_options(
208 shapes: &Loaded,
209 data: &Graph,
210 mode: ValidationGraphMode,
211 key_path: Option<&Path>,
212 options: &ValidationOptions,
213) -> Vec<PropertyWitness> {
214 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
215 let (focus_data, frozen, union_owner);
216 match mode {
217 ValidationGraphMode::Data => {
218 frozen = if has_shapes_graph {
219 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
220 } else {
221 FrozenIndexedDataset::from_graph(data)
222 };
223 focus_data = data;
224 }
225 ValidationGraphMode::Union => {
226 frozen = if has_shapes_graph {
227 FrozenIndexedDataset::from_graph_union_with_shapes(data, &shapes.graph)
228 } else {
229 FrozenIndexedDataset::from_graph_union(data, &shapes.graph)
230 };
231 focus_data = data;
232 }
233 ValidationGraphMode::UnionAll => {
234 union_owner = graph_union(data, &shapes.graph);
235 frozen = if has_shapes_graph {
236 FrozenIndexedDataset::from_graphs(&union_owner, &shapes.graph)
237 } else {
238 FrozenIndexedDataset::from_graph(&union_owner)
239 };
240 focus_data = &union_owner;
241 }
242 }
243 let r = build_reporter(shapes, focus_data, frozen, has_shapes_graph, options);
244 let mut out = Vec::new();
245 for shape in r.target_shapes() {
246 let foci = r.focus_nodes(&shape);
247 r.prefetch_sparql(&shape, &foci);
248 for focus in &foci {
249 let mut check = HashSet::new();
250 let mut results = Vec::new();
251 r.collect(&shape, focus, &mut results, &mut check, &[]);
252 let conforms = !results.iter().any(|result| {
253 Severity::from_named_node(result.severity.clone()).meets(&options.minimum_severity)
254 });
255 if !conforms {
256 continue;
257 }
258 let mut visited = HashSet::new();
259 r.collect_property_witnesses(&shape, focus, &shape, key_path, &mut visited, &mut out);
260 }
261 }
262 out
263}
264
265fn build_reporter<'a>(
269 shapes: &'a Loaded,
270 focus_data: &'a Graph,
271 frozen: FrozenIndexedDataset,
272 has_shapes_graph: bool,
273 options: &'a ValidationOptions,
274) -> Reporter<'a> {
275 let needs_sparql = shapes
278 .graph
279 .triples_for_predicate(vocab::SH_SPARQL)
280 .next()
281 .is_some()
282 || shapes
283 .graph
284 .triples_for_predicate(vocab::SH_TARGET)
285 .next()
286 .is_some();
287 let mut sparql = SparqlExecutor::from_frozen(frozen, needs_sparql && has_shapes_graph);
288 sparql.set_functions(collect_functions(shapes), options.engine.unsupported);
289 let has_explicit_class_target = shapes
293 .graph
294 .triples_for_predicate(vocab::SH_TARGET_CLASS)
295 .next()
296 .is_some();
297 let has_implicit_class_target = shapes.graph.iter().any(|triple| {
298 let subject = triple.subject.into_owned();
299 is_shape_node(shapes, &subject)
300 && (shapes.is_instance_of(&subject, vocab::RDFS_CLASS)
301 || shapes.is_instance_of(&subject, vocab::OWL_CLASS))
302 });
303 let needs_class_index = has_explicit_class_target || has_implicit_class_target;
304 let class_index = if needs_class_index {
305 build_class_index(
306 focus_data,
307 sparql
308 .frozen()
309 .expect("report validation always has a frozen dataset"),
310 )
311 } else {
312 HashMap::new()
313 };
314 Reporter {
315 shapes,
316 focus_data,
317 sparql,
318 needs_sparql,
319 class_index,
320 path_cache: RefCell::new(HashMap::new()),
321 components: build_components(shapes, options.engine.unsupported),
322 entry_shape_names: &options.entry_shape_names,
323 }
324}
325
326fn validate_report_context(
327 shapes: &Loaded,
328 focus_data: &Graph,
329 frozen: FrozenIndexedDataset,
330 has_shapes_graph: bool,
331 options: &ValidationOptions,
332) -> ValidationReport {
333 let r = build_reporter(shapes, focus_data, frozen, has_shapes_graph, options);
334 let mut results = Vec::new();
335 for shape in r.target_shapes() {
336 let foci = r.focus_nodes(&shape);
337 r.prefetch_sparql(&shape, &foci);
338 for focus in &foci {
339 let mut visited = HashSet::new();
340 r.collect(&shape, focus, &mut results, &mut visited, &[]);
341 }
342 }
343 for result in &mut results {
347 if result.messages.is_empty() {
348 let message = r.default_message(result);
349 result.messages = vec![Term::Literal(Literal::new_simple_literal(message))];
350 }
351 }
352 if options.sort_results {
353 results.sort_by(|left, right| {
354 Severity::from_named_node(right.severity.clone())
355 .rank()
356 .cmp(&Severity::from_named_node(left.severity.clone()).rank())
357 .then_with(|| left.focus.to_string().cmp(&right.focus.to_string()))
358 .then_with(|| {
359 left.source_shape
360 .to_string()
361 .cmp(&right.source_shape.to_string())
362 })
363 .then_with(|| left.component.as_str().cmp(right.component.as_str()))
364 });
365 }
366 ValidationReport {
367 conforms: !results.iter().any(|result| {
368 Severity::from_named_node(result.severity.clone()).meets(&options.minimum_severity)
369 }),
370 results,
371 }
372}
373
374pub fn report_to_graph(report: &ValidationReport) -> Graph {
376 let mut g = Graph::new();
377 let root = BlankNode::default();
378 let t = |s: NamedOrBlankNode, p: NamedNodeRef, o: Term| Triple::new(s, p.into_owned(), o);
379
380 g.insert(&t(
381 root.clone().into(),
382 vocab::RDF_TYPE,
383 vocab::SH_VALIDATION_REPORT.into_owned().into(),
384 ));
385 g.insert(&t(
386 root.clone().into(),
387 vocab::SH_CONFORMS,
388 Literal::from(report.conforms).into(),
389 ));
390
391 for r in &report.results {
392 let rn = BlankNode::default();
393 g.insert(&t(root.clone().into(), vocab::SH_RESULT, rn.clone().into()));
394 g.insert(&t(
395 rn.clone().into(),
396 vocab::RDF_TYPE,
397 vocab::SH_VALIDATION_RESULT.into_owned().into(),
398 ));
399 g.insert(&t(rn.clone().into(), vocab::SH_FOCUS_NODE, r.focus.clone()));
400 if let Some(path) = &r.path {
401 g.insert(&t(rn.clone().into(), vocab::SH_RESULT_PATH, path.clone()));
402 }
403 if let Some(value) = &r.value {
404 g.insert(&t(rn.clone().into(), vocab::SH_VALUE, value.clone()));
405 }
406 g.insert(&t(
407 rn.clone().into(),
408 vocab::SH_RESULT_SEVERITY,
409 r.severity.clone().into(),
410 ));
411 g.insert(&t(
412 rn.clone().into(),
413 vocab::SH_SOURCE_CONSTRAINT_COMPONENT,
414 r.component.clone().into(),
415 ));
416 for msg in &r.messages {
417 g.insert(&t(rn.clone().into(), vocab::SH_RESULT_MESSAGE, msg.clone()));
418 }
419 g.insert(&t(
420 rn.into(),
421 vocab::SH_SOURCE_SHAPE,
422 r.source_shape.clone(),
423 ));
424 }
425 g
426}
427
428fn substitute_messages(
435 messages: &[Term],
436 focus: &Term,
437 bindings: &HashMap<String, Term>,
438) -> Vec<Term> {
439 messages
440 .iter()
441 .map(|msg| {
442 let Term::Literal(lit) = msg else {
443 return msg.clone();
444 };
445 let text = lit.value();
446 let substituted = apply_message_template(text, focus, bindings);
447 if substituted == text {
448 msg.clone()
449 } else {
450 Term::Literal(Literal::new_simple_literal(&substituted))
451 }
452 })
453 .collect()
454}
455
456struct CustomComponent {
460 iri: NamedNode,
462 params: Vec<ComponentParam>,
463 node_validator: Option<ComponentValidator>,
465 property_validator: Option<ComponentValidator>,
467 generic_validator: Option<ComponentValidator>,
469}
470
471struct ComponentParam {
472 path: NamedNode,
474 var: String,
476 optional: bool,
477}
478
479struct ComponentValidator {
480 kind: SparqlQueryKind,
481 query: String,
483 messages: Vec<Term>,
484}
485
486fn resolve_validator(
487 shapes: &Loaded,
488 node: Term,
489 component_iri: &NamedNode,
490 policy: UnsupportedPolicy,
491) -> Option<ComponentValidator> {
492 match parse_validator(shapes, &node) {
493 Ok(v) => Some(v),
494 Err(e) => {
495 assert!(
496 policy != UnsupportedPolicy::Error,
497 "invalid SPARQL in custom constraint component <{component_iri}>: {e}"
498 );
499 None
500 }
501 }
502}
503
504fn build_components(shapes: &Loaded, policy: UnsupportedPolicy) -> Vec<CustomComponent> {
515 let mut out = Vec::new();
516 let mut seen = HashSet::new();
517 for triple in shapes.graph.triples_for_predicate(vocab::SH_PARAMETER) {
518 let subject = triple.subject.into_owned();
519 if !seen.insert(subject.clone()) {
520 continue;
521 }
522 let NamedOrBlankNode::NamedNode(iri) = &subject else {
523 continue; };
525 if vocab::NATIVE_CONSTRAINT_COMPONENTS.contains(&iri.as_ref()) {
526 continue; }
528
529 let node_validator = shapes
530 .object(&subject, vocab::SH_NODE_VALIDATOR)
531 .and_then(|v| resolve_validator(shapes, v, iri, policy));
532 let property_validator = shapes
533 .object(&subject, vocab::SH_PROPERTY_VALIDATOR)
534 .and_then(|v| resolve_validator(shapes, v, iri, policy));
535 let generic_validator = shapes
536 .object(&subject, vocab::SH_VALIDATOR)
537 .and_then(|v| resolve_validator(shapes, v, iri, policy));
538 if node_validator.is_none() && property_validator.is_none() && generic_validator.is_none() {
539 continue; }
541 let mut params = Vec::new();
542 for p in shapes.objects(&subject, vocab::SH_PARAMETER) {
543 let Some(pn) = term_to_node(&p) else { continue };
544 let Some(Term::NamedNode(path)) = shapes.object(&pn, vocab::SH_PATH) else {
545 continue;
546 };
547 let var = local_name(path.as_str()).to_string();
548 let optional = matches!(
549 shapes.object(&pn, vocab::SH_OPTIONAL),
550 Some(Term::Literal(ref l)) if l.value() == "true"
551 );
552 params.push(ComponentParam {
553 path,
554 var,
555 optional,
556 });
557 }
558 if params.is_empty() {
559 continue;
560 }
561 out.push(CustomComponent {
562 iri: iri.clone(),
563 params,
564 node_validator,
565 property_validator,
566 generic_validator,
567 });
568 }
569 out.sort_by(|a, b| a.iri.as_str().cmp(b.iri.as_str()));
570 out
571}
572
573fn parse_validator(shapes: &Loaded, node: &Term) -> Result<ComponentValidator, String> {
577 let node = term_to_node(node)
578 .ok_or_else(|| "validator node is not an IRI or blank node".to_string())?;
579 let (kind, raw) = if let Some(Term::Literal(q)) = shapes.object(&node, vocab::SH_ASK) {
580 (SparqlQueryKind::Ask, q.value().to_string())
581 } else if let Some(Term::Literal(q)) = shapes.object(&node, vocab::SH_SELECT) {
582 (SparqlQueryKind::Select, q.value().to_string())
583 } else {
584 return Err("validator has neither sh:ask nor sh:select".to_string());
585 };
586 let (_, query) = canonical_sparql_query(shapes, &node, &raw)
587 .map_err(|e| format!("invalid SPARQL in {node}: {e}"))?;
588 let messages = shapes.objects(&node, vocab::SH_MESSAGE);
589 Ok(ComponentValidator {
590 kind,
591 query,
592 messages,
593 })
594}
595
596fn local_name(iri: &str) -> &str {
599 iri.rsplit(['#', '/']).next().unwrap_or(iri)
600}
601
602pub(crate) fn collect_functions(shapes: &Loaded) -> Vec<FunctionDef> {
607 let mut out = Vec::new();
608 for func in shapes
609 .graph
610 .subjects_for_predicate_object(vocab::RDF_TYPE, vocab::SH_SPARQL_FUNCTION)
611 .map(|s| s.into_owned())
612 .collect::<Vec<_>>()
613 {
614 let NamedOrBlankNode::NamedNode(iri) = &func else {
615 continue;
616 };
617 let raw = match shapes
618 .object(&func, vocab::SH_SELECT)
619 .or_else(|| shapes.object(&func, vocab::SH_ASK))
620 {
621 Some(Term::Literal(q)) => q.value().to_string(),
622 _ => continue,
623 };
624 let Ok((_, query)) = canonical_sparql_query(shapes, &func, &raw) else {
625 continue;
626 };
627 out.push(FunctionDef {
628 iri: iri.clone(),
629 params: function_param_names(shapes, &func),
630 reads_graph: crate::sparql::query_reads_graph(&query),
631 query,
632 });
633 }
634 out
635}
636
637fn function_param_names(shapes: &Loaded, func: &NamedOrBlankNode) -> Vec<String> {
640 let mut params: Vec<(i64, String)> = shapes
641 .objects(func, vocab::SH_PARAMETER)
642 .iter()
643 .filter_map(|p| {
644 let pn = term_to_node(p)?;
645 let order = match shapes.object(&pn, vocab::SH_ORDER) {
646 Some(Term::Literal(l)) => l.value().parse::<i64>().unwrap_or(0),
647 _ => 0,
648 };
649 let name = match shapes.object(&pn, vocab::SH_NAME) {
650 Some(Term::Literal(l)) => l.value().to_string(),
651 _ => match shapes.object(&pn, vocab::SH_PATH) {
652 Some(Term::NamedNode(n)) => local_name(n.as_str()).to_string(),
653 _ => return None,
654 },
655 };
656 Some((order, name))
657 })
658 .collect();
659 params.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
660 params.into_iter().map(|(_, name)| name).collect()
661}
662
663struct Reporter<'a> {
664 shapes: &'a Loaded,
665 focus_data: &'a Graph,
666 sparql: SparqlExecutor,
667 needs_sparql: bool,
668 class_index: HashMap<Term, Vec<Term>>,
671 path_cache: RefCell<HashMap<NamedOrBlankNode, PathCacheEntry>>,
674 components: Vec<CustomComponent>,
677 entry_shape_names: &'a [String],
680}
681
682type Visited = HashSet<(NamedOrBlankNode, Term)>;
683
684type PathCacheEntry = (Option<Term>, Option<Path>);
686
687impl Reporter<'_> {
688 fn frozen(&self) -> &FrozenIndexedDataset {
689 self.sparql
690 .frozen()
691 .expect("report validation always has a frozen dataset")
692 }
693
694 fn target_shapes(&self) -> Vec<NamedOrBlankNode> {
695 let mut found: HashSet<NamedOrBlankNode> = HashSet::new();
696 for t in self.shapes.graph.iter() {
697 let p = t.predicate;
698 if p == vocab::SH_TARGET_NODE
699 || p == vocab::SH_TARGET_CLASS
700 || p == vocab::SH_TARGET_SUBJECTS_OF
701 || p == vocab::SH_TARGET_OBJECTS_OF
702 {
703 found.insert(t.subject.into_owned());
704 }
705 if p == vocab::SH_TARGET
707 && let Some(target) = term_to_node(&t.object.into_owned())
708 && self.shapes.object(&target, vocab::SH_SELECT).is_some()
709 {
710 found.insert(t.subject.into_owned());
711 }
712 if p == vocab::RDF_TYPE {
714 let s = t.subject.into_owned();
715 if self.is_class(&s) && self.is_shape(&s) {
716 found.insert(s);
717 }
718 }
719 }
720 let mut v: Vec<_> = found.into_iter().collect();
721 v.retain(|shape| self.entry_shape_selected(shape));
722 v.sort_by_key(|n| n.to_string());
723 v
724 }
725
726 fn entry_shape_selected(&self, shape: &NamedOrBlankNode) -> bool {
727 let actual = match shape {
728 NamedOrBlankNode::NamedNode(named) => Some(named.as_str()),
729 NamedOrBlankNode::BlankNode(_) => None,
730 };
731 entry_shape_name_selected(self.entry_shape_names, actual)
732 }
733
734 fn is_shape(&self, n: &NamedOrBlankNode) -> bool {
736 is_shape_node(self.shapes, n)
737 }
738
739 fn is_class(&self, n: &NamedOrBlankNode) -> bool {
740 self.shapes.is_instance_of(n, vocab::RDFS_CLASS)
741 || self.shapes.is_instance_of(n, vocab::OWL_CLASS)
742 }
743
744 fn deactivated(&self, n: &NamedOrBlankNode) -> bool {
745 matches!(self.shapes.object(n, vocab::SH_DEACTIVATED),
746 Some(Term::Literal(ref l)) if l.value() == "true")
747 }
748
749 fn focus_nodes(&self, shape: &NamedOrBlankNode) -> Vec<Term> {
750 let mut nodes = Vec::new();
751 nodes.extend(self.shapes.objects(shape, vocab::SH_TARGET_NODE));
752 for c in self.shapes.objects(shape, vocab::SH_TARGET_CLASS) {
753 if let Some(instances) = self.class_index.get(&c) {
754 nodes.extend(instances.iter().cloned());
755 }
756 }
757 for p in self.shapes.objects(shape, vocab::SH_TARGET_SUBJECTS_OF) {
758 if let Term::NamedNode(n) = p {
759 nodes.extend(
760 self.focus_data
761 .triples_for_predicate(n.as_ref())
762 .map(|t| node_term(t.subject)),
763 );
764 }
765 }
766 for p in self.shapes.objects(shape, vocab::SH_TARGET_OBJECTS_OF) {
767 if let Term::NamedNode(n) = p {
768 nodes.extend(
769 self.focus_data
770 .triples_for_predicate(n.as_ref())
771 .map(|t| t.object.into_owned()),
772 );
773 }
774 }
775 if self.needs_sparql {
778 let exec = &self.sparql;
779 for target in self.shapes.objects(shape, vocab::SH_TARGET) {
780 let Some(target_node) = term_to_node(&target) else {
781 continue;
782 };
783 let Some(Term::Literal(query)) = self.shapes.object(&target_node, vocab::SH_SELECT)
784 else {
785 continue;
786 };
787 let Ok((_, canonical)) =
789 canonical_sparql_query(self.shapes, &target_node, query.value())
790 else {
791 continue;
792 };
793 if let Ok(found) = exec.target_nodes(&canonical) {
794 nodes.extend(found);
795 }
796 }
797 }
798 if let NamedOrBlankNode::NamedNode(n) = shape
800 && self.is_class(shape)
801 {
802 let class = Term::NamedNode(n.clone());
803 if let Some(instances) = self.class_index.get(&class) {
804 nodes.extend(instances.iter().cloned());
805 }
806 }
807 let mut seen = HashSet::new();
808 nodes.retain(|t| seen.insert(t.clone()));
809 nodes
810 }
811
812 fn shape_path(&self, shape: &NamedOrBlankNode) -> (Option<Term>, Option<Path>) {
815 if let Some(cached) = self.path_cache.borrow().get(shape) {
816 return cached.clone();
817 }
818 let path_term = self.shapes.object(shape, vocab::SH_PATH);
819 let parsed = path_term
820 .as_ref()
821 .and_then(|t| parse_path(self.shapes, t).ok());
822 let entry = (path_term, parsed);
823 self.path_cache
824 .borrow_mut()
825 .insert(shape.clone(), entry.clone());
826 entry
827 }
828
829 fn messages_or_inherited(&self, shape: &NamedOrBlankNode, inherited: &[Term]) -> Vec<Term> {
835 let own = self.messages(shape);
836 if own.is_empty() {
837 inherited.to_vec()
838 } else {
839 own
840 }
841 }
842
843 fn collect(
848 &self,
849 shape: &NamedOrBlankNode,
850 focus: &Term,
851 out: &mut Vec<ValidationResult>,
852 visited: &mut Visited,
853 inherited: &[Term],
854 ) {
855 if self.deactivated(shape) {
856 return; }
858 let key = (shape.clone(), focus.clone());
859 if !visited.insert(key.clone()) {
860 return; }
862
863 let (path_term, parsed) = self.shape_path(shape);
864 let value_nodes: Vec<Term> = match &parsed {
865 Some(p) => succ(self.frozen(), focus, p).into_iter().collect(),
866 None => vec![focus.clone()],
867 };
868 let severity = self.severity(shape);
869 let messages = self.messages_or_inherited(shape, inherited);
870 let push = |out: &mut Vec<ValidationResult>, value, component| {
871 out.push(ValidationResult {
872 focus: focus.clone(),
873 path: path_term.clone(),
874 value,
875 component,
876 source_shape: node_term_ref(shape),
877 severity: severity.clone(),
878 messages: messages.clone(),
879 sparql_diagnostic: None,
880 });
881 };
882
883 if parsed.is_some() {
885 if let Some(min) = self.int(shape, vocab::SH_MIN_COUNT)
886 && (value_nodes.len() as u64) < min
887 {
888 push(out, None, vocab::SH_CC_MIN_COUNT.into_owned());
889 }
890 if let Some(max) = self.int(shape, vocab::SH_MAX_COUNT)
891 && (value_nodes.len() as u64) > max
892 {
893 push(out, None, vocab::SH_CC_MAX_COUNT.into_owned());
894 }
895 }
896
897 for hv in self.shapes.objects(shape, vocab::SH_HAS_VALUE) {
899 if !value_nodes.contains(&hv) {
900 push(out, None, vocab::SH_CC_HAS_VALUE.into_owned());
901 }
902 }
903
904 self.collect_closed(shape, focus, &value_nodes, out, &messages);
905 self.collect_property_pairs(shape, focus, &path_term, &value_nodes, out, &messages);
906 self.collect_unique_lang(shape, focus, &path_term, &value_nodes, out, &messages);
907 self.collect_qualified_counts(
908 shape,
909 focus,
910 &path_term,
911 &value_nodes,
912 out,
913 visited,
914 &messages,
915 );
916
917 for u in &value_nodes {
919 for (component, ok) in self.value_checks(shape, u, visited) {
920 if !ok {
921 push(out, Some(u.clone()), component);
922 }
923 }
924 }
925
926 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
929 if let Some(pn) = term_to_node(&prop) {
930 for u in &value_nodes {
931 self.collect(&pn, u, out, visited, &messages);
932 }
933 }
934 }
935
936 self.collect_sparql(shape, focus, &path_term, &parsed, out, &messages);
937 self.collect_expression(shape, focus, out, visited, &messages);
938 self.collect_components(
939 shape,
940 focus,
941 &path_term,
942 &parsed,
943 &value_nodes,
944 out,
945 &messages,
946 );
947
948 visited.remove(&key);
949 }
950
951 fn collect_property_witnesses(
958 &self,
959 shape: &NamedOrBlankNode,
960 focus: &Term,
961 profile: &NamedOrBlankNode,
962 key_path: Option<&Path>,
963 visited: &mut Visited,
964 out: &mut Vec<PropertyWitness>,
965 ) {
966 if self.deactivated(shape) {
967 return;
968 }
969 let key = (shape.clone(), focus.clone());
970 if !visited.insert(key.clone()) {
971 return;
972 }
973
974 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
975 if let Some(pn) = term_to_node(&prop) {
976 self.collect_property_binding(&pn, focus, profile, key_path, visited, out);
977 }
978 }
979 for list in self.shapes.objects(shape, vocab::SH_AND) {
980 for member in self.shapes.read_list(&list) {
981 if let Some(mn) = term_to_node(&member) {
982 self.collect_property_witnesses(&mn, focus, profile, key_path, visited, out);
983 }
984 }
985 }
986 for n in self.shapes.objects(shape, vocab::SH_NODE) {
987 if let Some(nn) = term_to_node(&n) {
988 self.collect_property_witnesses(&nn, focus, profile, key_path, visited, out);
989 }
990 }
991
992 visited.remove(&key);
993 }
994
995 fn collect_property_binding(
1002 &self,
1003 pn: &NamedOrBlankNode,
1004 focus: &Term,
1005 profile: &NamedOrBlankNode,
1006 key_path: Option<&Path>,
1007 visited: &mut Visited,
1008 out: &mut Vec<PropertyWitness>,
1009 ) {
1010 if self.deactivated(pn) {
1011 return;
1012 }
1013 let (_, parsed_path) = self.shape_path(pn);
1014 let Some(path) = parsed_path else { return };
1015
1016 let key = key_path
1022 .and_then(|kp| {
1023 let mut matches: Vec<Term> = succ(&self.shapes.graph, &node_term_ref(pn), kp)
1024 .into_iter()
1025 .collect();
1026 matches.sort_by_key(ToString::to_string);
1027 matches.into_iter().next()
1028 })
1029 .unwrap_or_else(|| node_term_ref(pn));
1030
1031 let value_nodes: Vec<Term> = succ(self.frozen(), focus, &path).into_iter().collect();
1032 let values = match self.shapes.object(pn, vocab::SH_QUALIFIED_VALUE_SHAPE) {
1033 Some(qualifier_term) => {
1034 let Some(qualifier) = term_to_node(&qualifier_term) else {
1035 return;
1036 };
1037 let siblings = if self.bool(pn, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
1038 self.sibling_qualified_shapes(pn, &qualifier)
1039 } else {
1040 Vec::new()
1041 };
1042 value_nodes
1043 .into_iter()
1044 .filter(|v| {
1045 self.conforms(&qualifier, v, visited)
1046 && siblings
1047 .iter()
1048 .all(|sibling| !self.conforms(sibling, v, visited))
1049 })
1050 .collect()
1051 }
1052 None => value_nodes,
1053 };
1054
1055 out.push(PropertyWitness {
1056 focus: focus.clone(),
1057 shape: node_term_ref(profile),
1058 key,
1059 values,
1060 });
1061 }
1062
1063 #[allow(clippy::too_many_arguments)]
1070 fn collect_components(
1071 &self,
1072 shape: &NamedOrBlankNode,
1073 focus: &Term,
1074 path_term: &Option<Term>,
1075 parsed_path: &Option<Path>,
1076 value_nodes: &[Term],
1077 out: &mut Vec<ValidationResult>,
1078 inherited: &[Term],
1079 ) {
1080 if self.components.is_empty() {
1081 return;
1082 }
1083 let is_property_shape = parsed_path.is_some();
1084 for component in &self.components {
1085 let mut params: Vec<(String, Term)> = Vec::new();
1087 let mut activated = true;
1088 for p in &component.params {
1089 if let Some(value) = self.shapes.object(shape, p.path.as_ref()) {
1090 params.push((p.var.clone(), value));
1091 } else if !p.optional {
1092 activated = false;
1093 break;
1094 }
1095 }
1096 if !activated {
1097 continue;
1098 }
1099
1100 let validator = if is_property_shape {
1101 component
1102 .property_validator
1103 .as_ref()
1104 .or(component.generic_validator.as_ref())
1105 } else {
1106 component
1107 .node_validator
1108 .as_ref()
1109 .or(component.generic_validator.as_ref())
1110 };
1111 let Some(validator) = validator else { continue };
1112
1113 let mut base = params;
1117 base.push(("currentShape".to_string(), node_term_ref(shape)));
1118 let path = parsed_path.as_ref();
1119
1120 match validator.kind {
1121 SparqlQueryKind::Ask => {
1122 for value in value_nodes {
1123 let mut bindings = base.clone();
1124 bindings.push(("this".to_string(), focus.clone()));
1125 bindings.push(("value".to_string(), value.clone()));
1126 let violates = match self.sparql.eval_ask(&validator.query, path, &bindings)
1128 {
1129 Ok(conforms) => !conforms,
1130 Err(_) => true,
1131 };
1132 if violates {
1133 self.push_component_result(
1134 out,
1135 component,
1136 shape,
1137 focus,
1138 path_term.clone(),
1139 Some(value.clone()),
1140 &bindings,
1141 &[],
1142 &validator.messages,
1143 inherited,
1144 &validator.query,
1145 );
1146 }
1147 }
1148 }
1149 SparqlQueryKind::Select => {
1150 let mut bindings = base.clone();
1151 bindings.push(("this".to_string(), focus.clone()));
1152 match self.sparql.eval_select(&validator.query, path, &bindings) {
1153 Ok(rows) => {
1154 for row in rows {
1155 let value = row
1158 .get("value")
1159 .cloned()
1160 .or_else(|| (!is_property_shape).then(|| focus.clone()));
1161 let path = row.get("path").cloned().or_else(|| path_term.clone());
1162 let row_vec: Vec<(String, Term)> =
1163 row.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1164 self.push_component_result(
1165 out,
1166 component,
1167 shape,
1168 focus,
1169 path,
1170 value,
1171 &bindings,
1172 &row_vec,
1173 &validator.messages,
1174 inherited,
1175 &validator.query,
1176 );
1177 }
1178 }
1179 Err(_) => self.push_component_result(
1180 out,
1181 component,
1182 shape,
1183 focus,
1184 path_term.clone(),
1185 None,
1186 &bindings,
1187 &[],
1188 &validator.messages,
1189 inherited,
1190 &validator.query,
1191 ),
1192 }
1193 }
1194 }
1195 }
1196 }
1197
1198 #[allow(clippy::too_many_arguments)]
1199 fn push_component_result(
1200 &self,
1201 out: &mut Vec<ValidationResult>,
1202 component: &CustomComponent,
1203 shape: &NamedOrBlankNode,
1204 focus: &Term,
1205 path: Option<Term>,
1206 value: Option<Term>,
1207 bindings: &[(String, Term)],
1208 result_row: &[(String, Term)],
1209 validator_messages: &[Term],
1210 inherited: &[Term],
1211 validator_query: &str,
1212 ) {
1213 let raw = if validator_messages.is_empty() {
1214 self.messages_or_inherited(shape, inherited)
1215 } else {
1216 validator_messages.to_vec()
1217 };
1218 let bind_map: HashMap<String, Term> =
1223 bindings.iter().chain(result_row.iter()).cloned().collect();
1224 let messages = substitute_messages(&raw, focus, &bind_map);
1225 out.push(ValidationResult {
1226 focus: focus.clone(),
1227 path,
1228 value,
1229 component: component.iri.clone(),
1230 source_shape: node_term_ref(shape),
1231 severity: self.severity(shape),
1232 messages,
1233 sparql_diagnostic: Some(SparqlDiagnostic {
1237 query: validator_query.to_string(),
1238 bindings: bindings.to_vec(),
1239 results: if result_row.is_empty() {
1240 Vec::new()
1241 } else {
1242 vec![result_row.to_vec()]
1243 },
1244 fallback_reason: None,
1245 }),
1246 });
1247 }
1248
1249 fn collect_expression(
1256 &self,
1257 shape: &NamedOrBlankNode,
1258 focus: &Term,
1259 out: &mut Vec<ValidationResult>,
1260 visited: &mut Visited,
1261 inherited: &[Term],
1262 ) {
1263 for expr_term in self.shapes.objects(shape, vocab::SH_EXPRESSION) {
1264 let Some(results) = self.eval_node_expr(&expr_term, focus, visited) else {
1265 continue;
1266 };
1267 for value in results {
1268 if is_boolean_true(&value) {
1269 continue;
1270 }
1271 out.push(ValidationResult {
1272 focus: focus.clone(),
1273 path: None,
1274 value: Some(value),
1275 component: vocab::SH_CC_EXPRESSION.into_owned(),
1276 source_shape: node_term_ref(shape),
1277 severity: self.severity(shape),
1278 messages: self.messages_or_inherited(shape, inherited),
1279 sparql_diagnostic: None,
1280 });
1281 }
1282 }
1283 }
1284
1285 fn eval_node_expr(
1290 &self,
1291 term: &Term,
1292 focus: &Term,
1293 visited: &mut Visited,
1294 ) -> Option<Vec<Term>> {
1295 match term {
1296 Term::NamedNode(n) if n.as_ref() == vocab::SH_THIS => Some(vec![focus.clone()]),
1297 Term::NamedNode(_) | Term::Literal(_) => Some(vec![term.clone()]),
1298 Term::BlankNode(_) => {
1299 let node = term_to_node(term)?;
1300 if let Some(path_term) = self.shapes.object(&node, vocab::SH_PATH) {
1301 let path = parse_path(self.shapes, &path_term).ok()?;
1302 Some(succ(self.frozen(), focus, &path).into_iter().collect())
1303 } else if let Some(filter_shape) = self.shapes.object(&node, vocab::SH_FILTER_SHAPE)
1304 {
1305 let filter_shape = term_to_node(&filter_shape)?;
1306 let nodes_term = self.shapes.object(&node, vocab::SH_NODES)?;
1307 let inputs = self.eval_node_expr(&nodes_term, focus, visited)?;
1308 Some(
1309 inputs
1310 .into_iter()
1311 .filter(|x| self.conforms(&filter_shape, x, visited))
1312 .collect(),
1313 )
1314 } else if let Some(list) = self.shapes.object(&node, vocab::SH_INTERSECTION) {
1315 self.eval_node_expr_set(&list, focus, visited, true)
1316 } else if let Some(list) = self.shapes.object(&node, vocab::SH_UNION) {
1317 self.eval_node_expr_set(&list, focus, visited, false)
1318 } else {
1319 None
1321 }
1322 }
1323 }
1324 }
1325
1326 fn eval_node_expr_set(
1330 &self,
1331 list_head: &Term,
1332 focus: &Term,
1333 visited: &mut Visited,
1334 intersect: bool,
1335 ) -> Option<Vec<Term>> {
1336 let members = self.shapes.read_list(list_head);
1337 if members.is_empty() {
1338 return None;
1339 }
1340 let mut iter = members.iter();
1341 let mut acc = self.eval_node_expr(iter.next().unwrap(), focus, visited)?;
1342 for member in iter {
1343 let next = self.eval_node_expr(member, focus, visited)?;
1344 if intersect {
1345 acc.retain(|x| next.contains(x));
1346 } else {
1347 for t in next {
1348 if !acc.contains(&t) {
1349 acc.push(t);
1350 }
1351 }
1352 }
1353 }
1354 Some(acc)
1355 }
1356
1357 fn build_sparql_constraint(
1366 &self,
1367 shape: &NamedOrBlankNode,
1368 constraint_node: &NamedOrBlankNode,
1369 parsed_path: &Option<Path>,
1370 ) -> Option<SparqlConstraint> {
1371 let (kind, raw) = if let Some(Term::Literal(query)) =
1372 self.shapes.object(constraint_node, vocab::SH_SELECT)
1373 {
1374 (SparqlQueryKind::Select, query.value().to_string())
1375 } else if let Some(Term::Literal(query)) =
1376 self.shapes.object(constraint_node, vocab::SH_ASK)
1377 {
1378 (SparqlQueryKind::Ask, query.value().to_string())
1379 } else {
1380 return None;
1381 };
1382 let (_, query) = canonical_sparql_query(self.shapes, constraint_node, &raw).ok()?;
1383 Some(SparqlConstraint {
1384 kind,
1385 query,
1386 path: parsed_path.clone(),
1387 shape: Some(node_term_ref(shape)),
1388 messages: Vec::new(),
1391 extra_bindings: Vec::new(),
1392 bind_value_to_this: false,
1393 })
1394 }
1395
1396 fn prefetch_sparql(&self, shape: &NamedOrBlankNode, foci: &[Term]) {
1400 if !self.needs_sparql || foci.len() < 2 {
1401 return;
1402 }
1403 let (_, parsed_path) = self.shape_path(shape);
1404 for constraint_term in self.shapes.objects(shape, vocab::SH_SPARQL) {
1405 let Some(constraint_node) = term_to_node(&constraint_term) else {
1406 continue;
1407 };
1408 if let Some(constraint) =
1409 self.build_sparql_constraint(shape, &constraint_node, &parsed_path)
1410 {
1411 let _ = self.sparql.prefetch_constraint(&constraint, foci);
1412 }
1413 }
1414 }
1415
1416 fn collect_sparql(
1417 &self,
1418 shape: &NamedOrBlankNode,
1419 focus: &Term,
1420 path_term: &Option<Term>,
1421 parsed_path: &Option<Path>,
1422 out: &mut Vec<ValidationResult>,
1423 inherited: &[Term],
1424 ) {
1425 if !self.needs_sparql {
1426 return;
1427 }
1428 let sparql = &self.sparql;
1429 let severity = self.severity(shape);
1430 for constraint_term in self.shapes.objects(shape, vocab::SH_SPARQL) {
1431 let Some(constraint_node) = term_to_node(&constraint_term) else {
1432 continue;
1433 };
1434 let Some(constraint) =
1435 self.build_sparql_constraint(shape, &constraint_node, parsed_path)
1436 else {
1437 continue;
1438 };
1439 let raw_messages = {
1443 let on_constraint = self.shapes.objects(&constraint_node, vocab::SH_MESSAGE);
1444 if on_constraint.is_empty() {
1445 self.messages_or_inherited(shape, inherited)
1446 } else {
1447 on_constraint
1448 }
1449 };
1450 match sparql.constraint_violations(&constraint, focus) {
1451 Ok(violations) => {
1452 if violations.is_empty() {
1453 continue;
1454 }
1455 let diagnostic = sparql
1460 .constraint_diagnostic(&constraint, focus, &violations)
1461 .ok();
1462 for violation in violations {
1463 let messages =
1464 substitute_messages(&raw_messages, focus, &violation.bindings);
1465 let value = violation.value.or_else(|| match constraint.kind {
1468 SparqlQueryKind::Select => Some(focus.clone()),
1469 SparqlQueryKind::Ask => None,
1470 });
1471 out.push(ValidationResult {
1472 focus: focus.clone(),
1473 path: violation.path.or_else(|| path_term.clone()),
1474 value,
1475 component: vocab::SH_CC_SPARQL.into_owned(),
1476 source_shape: node_term_ref(shape),
1477 severity: severity.clone(),
1478 messages,
1479 sparql_diagnostic: diagnostic.clone(),
1480 });
1481 }
1482 }
1483 Err(error) => {
1487 let mut messages = raw_messages;
1488 messages.push(Term::Literal(Literal::new_simple_literal(format!(
1489 "SPARQL constraint evaluation failed: {error}"
1490 ))));
1491 out.push(ValidationResult {
1492 focus: focus.clone(),
1493 path: path_term.clone(),
1494 value: None,
1495 component: vocab::SH_CC_SPARQL.into_owned(),
1496 source_shape: node_term_ref(shape),
1497 severity: severity.clone(),
1498 messages,
1499 sparql_diagnostic: sparql
1500 .constraint_diagnostic(&constraint, focus, &[])
1501 .ok(),
1502 });
1503 }
1504 }
1505 }
1506 }
1507
1508 fn collect_closed(
1509 &self,
1510 shape: &NamedOrBlankNode,
1511 focus: &Term,
1512 value_nodes: &[Term],
1513 out: &mut Vec<ValidationResult>,
1514 inherited: &[Term],
1515 ) {
1516 if !self.bool(shape, vocab::SH_CLOSED) {
1517 return;
1518 }
1519 let mut allowed = HashSet::new();
1520 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
1521 let Some(prop) = term_to_node(&prop) else {
1522 continue;
1523 };
1524 if let Some(Term::NamedNode(path)) = self.shapes.object(&prop, vocab::SH_PATH) {
1525 allowed.insert(path);
1526 }
1527 }
1528 for list in self.shapes.objects(shape, vocab::SH_IGNORED_PROPERTIES) {
1529 for term in self.shapes.read_list(&list) {
1530 if let Term::NamedNode(predicate) = term {
1531 allowed.insert(predicate);
1532 }
1533 }
1534 }
1535 for value_node in value_nodes {
1536 for (predicate, object) in self.frozen().outgoing(value_node) {
1537 if allowed.contains(&predicate) {
1538 continue;
1539 }
1540 out.push(ValidationResult {
1541 focus: focus.clone(),
1542 path: Some(Term::NamedNode(predicate)),
1543 value: Some(object),
1544 component: vocab::SH_CC_CLOSED.into_owned(),
1545 source_shape: node_term_ref(shape),
1546 severity: self.severity(shape),
1547 messages: self.messages_or_inherited(shape, inherited),
1548 sparql_diagnostic: None,
1549 });
1550 }
1551 }
1552 }
1553
1554 #[allow(clippy::too_many_arguments)]
1555 fn collect_property_pairs(
1556 &self,
1557 shape: &NamedOrBlankNode,
1558 focus: &Term,
1559 path: &Option<Term>,
1560 value_nodes: &[Term],
1561 out: &mut Vec<ValidationResult>,
1562 inherited: &[Term],
1563 ) {
1564 for predicate in self.shapes.objects(shape, vocab::SH_EQUALS) {
1565 let Term::NamedNode(predicate) = predicate else {
1566 continue;
1567 };
1568 let other = succ(self.frozen(), focus, &Path::Pred(predicate));
1569 for value in value_nodes.iter().filter(|value| !other.contains(*value)) {
1570 self.push(
1571 out,
1572 shape,
1573 focus,
1574 path.clone(),
1575 Some((*value).clone()),
1576 vocab::SH_CC_EQUALS,
1577 inherited,
1578 );
1579 }
1580 for value in other.iter().filter(|value| !value_nodes.contains(*value)) {
1581 self.push(
1582 out,
1583 shape,
1584 focus,
1585 path.clone(),
1586 Some(value.clone()),
1587 vocab::SH_CC_EQUALS,
1588 inherited,
1589 );
1590 }
1591 }
1592 for predicate in self.shapes.objects(shape, vocab::SH_DISJOINT) {
1593 let Term::NamedNode(predicate) = predicate else {
1594 continue;
1595 };
1596 let other = succ(self.frozen(), focus, &Path::Pred(predicate));
1597 for value in value_nodes.iter().filter(|value| other.contains(*value)) {
1598 self.push(
1599 out,
1600 shape,
1601 focus,
1602 path.clone(),
1603 Some((*value).clone()),
1604 vocab::SH_CC_DISJOINT,
1605 inherited,
1606 );
1607 }
1608 }
1609 for (constraint, component, inclusive) in [
1610 (vocab::SH_LESS_THAN, vocab::SH_CC_LESS_THAN, false),
1611 (
1612 vocab::SH_LESS_THAN_OR_EQUALS,
1613 vocab::SH_CC_LESS_THAN_OR_EQUALS,
1614 true,
1615 ),
1616 ] {
1617 for predicate in self.shapes.objects(shape, constraint) {
1618 let Term::NamedNode(predicate) = predicate else {
1619 continue;
1620 };
1621 for left in value_nodes {
1622 for right in succ(self.frozen(), focus, &Path::Pred(predicate.clone())) {
1623 let ordering = compare_terms(left, &right);
1624 let passes = ordering == Some(Ordering::Less)
1625 || inclusive && ordering == Some(Ordering::Equal);
1626 if !passes {
1627 self.push(
1628 out,
1629 shape,
1630 focus,
1631 path.clone(),
1632 Some(left.clone()),
1633 component,
1634 inherited,
1635 );
1636 }
1637 }
1638 }
1639 }
1640 }
1641 }
1642
1643 #[allow(clippy::too_many_arguments)]
1644 fn collect_unique_lang(
1645 &self,
1646 shape: &NamedOrBlankNode,
1647 focus: &Term,
1648 path: &Option<Term>,
1649 value_nodes: &[Term],
1650 out: &mut Vec<ValidationResult>,
1651 inherited: &[Term],
1652 ) {
1653 if !self.bool(shape, vocab::SH_UNIQUE_LANG) {
1654 return;
1655 }
1656 let mut counts = HashMap::new();
1657 for value in value_nodes {
1658 if let Term::Literal(literal) = value
1659 && let Some(language) = literal.language()
1660 {
1661 *counts
1662 .entry(language.to_ascii_lowercase())
1663 .or_insert(0usize) += 1;
1664 }
1665 }
1666 for _ in counts.values().filter(|count| **count > 1) {
1667 self.push(
1668 out,
1669 shape,
1670 focus,
1671 path.clone(),
1672 None,
1673 vocab::SH_CC_UNIQUE_LANG,
1674 inherited,
1675 );
1676 }
1677 }
1678
1679 #[allow(clippy::too_many_arguments)]
1680 fn collect_qualified_counts(
1681 &self,
1682 shape: &NamedOrBlankNode,
1683 focus: &Term,
1684 path: &Option<Term>,
1685 value_nodes: &[Term],
1686 out: &mut Vec<ValidationResult>,
1687 visited: &mut Visited,
1688 inherited: &[Term],
1689 ) {
1690 for qualifier in self.shapes.objects(shape, vocab::SH_QUALIFIED_VALUE_SHAPE) {
1691 let Some(qualifier) = term_to_node(&qualifier) else {
1692 continue;
1693 };
1694 let siblings = if self.bool(shape, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
1695 self.sibling_qualified_shapes(shape, &qualifier)
1696 } else {
1697 Vec::new()
1698 };
1699 let count = value_nodes
1700 .iter()
1701 .filter(|value| {
1702 self.conforms(&qualifier, value, visited)
1703 && siblings
1704 .iter()
1705 .all(|sibling| !self.conforms(sibling, value, visited))
1706 })
1707 .count() as u64;
1708 if let Some(min) = self.int(shape, vocab::SH_QUALIFIED_MIN_COUNT)
1709 && count < min
1710 {
1711 self.push(
1712 out,
1713 shape,
1714 focus,
1715 path.clone(),
1716 None,
1717 vocab::SH_CC_QUALIFIED_MIN_COUNT,
1718 inherited,
1719 );
1720 }
1721 if let Some(max) = self.int(shape, vocab::SH_QUALIFIED_MAX_COUNT)
1722 && count > max
1723 {
1724 self.push(
1725 out,
1726 shape,
1727 focus,
1728 path.clone(),
1729 None,
1730 vocab::SH_CC_QUALIFIED_MAX_COUNT,
1731 inherited,
1732 );
1733 }
1734 }
1735 }
1736
1737 fn sibling_qualified_shapes(
1738 &self,
1739 shape: &NamedOrBlankNode,
1740 qualifier: &NamedOrBlankNode,
1741 ) -> Vec<NamedOrBlankNode> {
1742 let shape_term = node_term_ref(shape);
1743 let mut siblings = HashSet::new();
1744 for triple in self.shapes.graph.triples_for_predicate(vocab::SH_PROPERTY) {
1745 if triple.object != shape_term.as_ref() {
1746 continue;
1747 }
1748 let parent = triple.subject.into_owned();
1749 for property in self.shapes.objects(&parent, vocab::SH_PROPERTY) {
1750 let Some(property) = term_to_node(&property) else {
1751 continue;
1752 };
1753 for qualifier in self
1754 .shapes
1755 .objects(&property, vocab::SH_QUALIFIED_VALUE_SHAPE)
1756 {
1757 if let Some(qualifier) = term_to_node(&qualifier) {
1758 siblings.insert(qualifier);
1759 }
1760 }
1761 }
1762 }
1763 siblings.remove(qualifier);
1764 siblings.into_iter().collect()
1765 }
1766
1767 #[allow(clippy::too_many_arguments)]
1768 fn push(
1769 &self,
1770 out: &mut Vec<ValidationResult>,
1771 shape: &NamedOrBlankNode,
1772 focus: &Term,
1773 path: Option<Term>,
1774 value: Option<Term>,
1775 component: NamedNodeRef<'static>,
1776 inherited: &[Term],
1777 ) {
1778 let mut bindings = HashMap::new();
1779 if let Some(v) = &value {
1780 bindings.insert("value".to_string(), v.clone());
1781 }
1782 if let Some(p) = &path {
1783 bindings.insert("path".to_string(), p.clone());
1784 }
1785 let raw = self.messages_or_inherited(shape, inherited);
1786 let messages = substitute_messages(&raw, focus, &bindings);
1787 out.push(ValidationResult {
1788 focus: focus.clone(),
1789 path,
1790 value,
1791 component: component.into_owned(),
1792 source_shape: node_term_ref(shape),
1793 severity: self.severity(shape),
1794 messages,
1795 sparql_diagnostic: None,
1796 });
1797 }
1798
1799 fn messages(&self, shape: &NamedOrBlankNode) -> Vec<Term> {
1801 self.shapes.objects(shape, vocab::SH_MESSAGE)
1802 }
1803
1804 fn default_message(&self, result: &ValidationResult) -> String {
1810 let comp = result.component.as_ref();
1811 let value = result.value.as_ref().map(render_term);
1812 let path = result.path.as_ref().map(render_term);
1813 let Some(shape) = term_to_node(&result.source_shape) else {
1814 return format!(
1815 "Value does not conform to constraint component <{}>",
1816 comp.as_str()
1817 );
1818 };
1819 let param = |p: NamedNodeRef| self.shapes.object(&shape, p).as_ref().map(render_term);
1821 let int_param = |p: NamedNodeRef| self.int(&shape, p).map(|n| n.to_string());
1822 let some_or = |o: Option<String>, default: &str| o.unwrap_or_else(|| default.to_string());
1823 let val = || value.clone().unwrap_or_else(|| "the value".to_string());
1824 let on_path = || {
1825 path.as_ref()
1826 .map(|p| format!(" on path {p}"))
1827 .unwrap_or_default()
1828 };
1829
1830 if comp == vocab::SH_CC_MIN_COUNT {
1831 format!(
1832 "Fewer than {} values{}",
1833 some_or(int_param(vocab::SH_MIN_COUNT), "the required number of"),
1834 on_path()
1835 )
1836 } else if comp == vocab::SH_CC_MAX_COUNT {
1837 format!(
1838 "More than {} values{}",
1839 some_or(int_param(vocab::SH_MAX_COUNT), "the allowed number of"),
1840 on_path()
1841 )
1842 } else if comp == vocab::SH_CC_CLASS {
1843 format!(
1844 "Value {} is not an instance of class {}",
1845 val(),
1846 some_or(param(vocab::SH_CLASS), "the required class")
1847 )
1848 } else if comp == vocab::SH_CC_DATATYPE {
1849 format!(
1850 "Value {} does not have datatype {}",
1851 val(),
1852 some_or(param(vocab::SH_DATATYPE), "the required datatype")
1853 )
1854 } else if comp == vocab::SH_CC_NODE_KIND {
1855 format!(
1856 "Value {} does not have node kind {}",
1857 val(),
1858 some_or(param(vocab::SH_NODE_KIND), "the required node kind")
1859 )
1860 } else if comp == vocab::SH_CC_MIN_INCLUSIVE {
1861 format!(
1862 "Value {} is less than minimum {}",
1863 val(),
1864 some_or(param(vocab::SH_MIN_INCLUSIVE), "the minimum")
1865 )
1866 } else if comp == vocab::SH_CC_MIN_EXCLUSIVE {
1867 format!(
1868 "Value {} is not greater than exclusive minimum {}",
1869 val(),
1870 some_or(param(vocab::SH_MIN_EXCLUSIVE), "the minimum")
1871 )
1872 } else if comp == vocab::SH_CC_MAX_INCLUSIVE {
1873 format!(
1874 "Value {} is greater than maximum {}",
1875 val(),
1876 some_or(param(vocab::SH_MAX_INCLUSIVE), "the maximum")
1877 )
1878 } else if comp == vocab::SH_CC_MAX_EXCLUSIVE {
1879 format!(
1880 "Value {} is not less than exclusive maximum {}",
1881 val(),
1882 some_or(param(vocab::SH_MAX_EXCLUSIVE), "the maximum")
1883 )
1884 } else if comp == vocab::SH_CC_MIN_LENGTH {
1885 format!(
1886 "Value {} is shorter than {} characters",
1887 val(),
1888 some_or(int_param(vocab::SH_MIN_LENGTH), "the minimum number of")
1889 )
1890 } else if comp == vocab::SH_CC_MAX_LENGTH {
1891 format!(
1892 "Value {} is longer than {} characters",
1893 val(),
1894 some_or(int_param(vocab::SH_MAX_LENGTH), "the maximum number of")
1895 )
1896 } else if comp == vocab::SH_CC_PATTERN {
1897 format!(
1898 "Value {} does not match pattern \"{}\"",
1899 val(),
1900 some_or(param(vocab::SH_PATTERN), "the required pattern")
1901 )
1902 } else if comp == vocab::SH_CC_IN {
1903 format!("Value {} is not in the list of allowed values", val())
1904 } else if comp == vocab::SH_CC_LANGUAGE_IN {
1905 format!("Value {} has a language tag that is not allowed", val())
1906 } else if comp == vocab::SH_CC_HAS_VALUE {
1907 format!(
1908 "Missing required value {}{}",
1909 some_or(param(vocab::SH_HAS_VALUE), "the expected value"),
1910 on_path()
1911 )
1912 } else if comp == vocab::SH_CC_UNIQUE_LANG {
1913 format!("Values{} do not have unique language tags", on_path())
1914 } else if comp == vocab::SH_CC_EQUALS {
1915 format!(
1916 "Value {} must equal the values of {}",
1917 val(),
1918 some_or(param(vocab::SH_EQUALS), "the compared property")
1919 )
1920 } else if comp == vocab::SH_CC_DISJOINT {
1921 format!(
1922 "Value {} must be disjoint from the values of {}",
1923 val(),
1924 some_or(param(vocab::SH_DISJOINT), "the compared property")
1925 )
1926 } else if comp == vocab::SH_CC_LESS_THAN {
1927 format!(
1928 "Value {} is not less than the values of {}",
1929 val(),
1930 some_or(param(vocab::SH_LESS_THAN), "the compared property")
1931 )
1932 } else if comp == vocab::SH_CC_LESS_THAN_OR_EQUALS {
1933 format!(
1934 "Value {} is not less than or equal to the values of {}",
1935 val(),
1936 some_or(
1937 param(vocab::SH_LESS_THAN_OR_EQUALS),
1938 "the compared property"
1939 )
1940 )
1941 } else if comp == vocab::SH_CC_AND {
1942 format!(
1943 "Value {} does not conform to all of the given shapes",
1944 val()
1945 )
1946 } else if comp == vocab::SH_CC_OR {
1947 format!(
1948 "Value {} does not conform to any of the given shapes",
1949 val()
1950 )
1951 } else if comp == vocab::SH_CC_XONE {
1952 format!(
1953 "Value {} does not conform to exactly one of the given shapes",
1954 val()
1955 )
1956 } else if comp == vocab::SH_CC_NOT {
1957 format!("Value {} conforms to a shape it must not", val())
1958 } else if comp == vocab::SH_CC_NODE {
1959 format!(
1960 "Value {} does not conform to shape {}",
1961 val(),
1962 some_or(param(vocab::SH_NODE), "the required shape")
1963 )
1964 } else if comp == vocab::SH_CC_CLOSED {
1965 format!(
1966 "Predicate{} is not allowed on {} (closed shape)",
1967 path.as_ref().map(|p| format!(" {p}")).unwrap_or_default(),
1968 val()
1969 )
1970 } else if comp == vocab::SH_CC_QUALIFIED_MIN_COUNT {
1971 format!(
1972 "Fewer than {} values{} conform to the qualified shape",
1973 some_or(
1974 int_param(vocab::SH_QUALIFIED_MIN_COUNT),
1975 "the required number of"
1976 ),
1977 on_path()
1978 )
1979 } else if comp == vocab::SH_CC_QUALIFIED_MAX_COUNT {
1980 format!(
1981 "More than {} values{} conform to the qualified shape",
1982 some_or(
1983 int_param(vocab::SH_QUALIFIED_MAX_COUNT),
1984 "the allowed number of"
1985 ),
1986 on_path()
1987 )
1988 } else if comp == vocab::SH_CC_EXPRESSION {
1989 format!("Expression constraint not satisfied for value {}", val())
1990 } else if comp == vocab::SH_CC_SPARQL {
1991 "SPARQL constraint not satisfied".to_string()
1992 } else {
1993 format!(
1994 "Value does not conform to constraint component <{}>",
1995 comp.as_str()
1996 )
1997 }
1998 }
1999
2000 fn conforms(&self, shape: &NamedOrBlankNode, focus: &Term, visited: &mut Visited) -> bool {
2001 let mut scratch = Vec::new();
2002 self.collect(shape, focus, &mut scratch, visited, &[]);
2003 scratch.is_empty()
2004 }
2005
2006 fn value_checks(
2009 &self,
2010 shape: &NamedOrBlankNode,
2011 u: &Term,
2012 visited: &mut Visited,
2013 ) -> Vec<(NamedNode, bool)> {
2014 let mut checks = Vec::new();
2015
2016 for c in self.shapes.objects(shape, vocab::SH_CLASS) {
2017 checks.push((vocab::SH_CC_CLASS.into_owned(), self.is_instance(u, &c)));
2018 }
2019 for d in self.shapes.objects(shape, vocab::SH_DATATYPE) {
2020 if let Term::NamedNode(dt) = d {
2021 let ok = value_type_holds(&ValueType::Datatype(dt), u);
2022 checks.push((vocab::SH_CC_DATATYPE.into_owned(), ok));
2023 }
2024 }
2025 for k in self.shapes.objects(shape, vocab::SH_NODE_KIND) {
2026 if let Some(set) = map_node_kind(&k) {
2027 checks.push((vocab::SH_CC_NODE_KIND.into_owned(), set.matches(u)));
2028 }
2029 }
2030 for (pred_iri, comp, inclusive) in [
2032 (vocab::SH_MIN_INCLUSIVE, vocab::SH_CC_MIN_INCLUSIVE, true),
2033 (vocab::SH_MIN_EXCLUSIVE, vocab::SH_CC_MIN_EXCLUSIVE, false),
2034 ] {
2035 if let Some(Term::Literal(b)) = self.shapes.object(shape, pred_iri) {
2036 let vt = ValueType::NumericRange {
2037 lo: Some(Bound {
2038 value: b,
2039 inclusive,
2040 }),
2041 hi: None,
2042 };
2043 checks.push((comp.into_owned(), value_type_holds(&vt, u)));
2044 }
2045 }
2046 for (pred_iri, comp, inclusive) in [
2047 (vocab::SH_MAX_INCLUSIVE, vocab::SH_CC_MAX_INCLUSIVE, true),
2048 (vocab::SH_MAX_EXCLUSIVE, vocab::SH_CC_MAX_EXCLUSIVE, false),
2049 ] {
2050 if let Some(Term::Literal(b)) = self.shapes.object(shape, pred_iri) {
2051 let vt = ValueType::NumericRange {
2052 lo: None,
2053 hi: Some(Bound {
2054 value: b,
2055 inclusive,
2056 }),
2057 };
2058 checks.push((comp.into_owned(), value_type_holds(&vt, u)));
2059 }
2060 }
2061 let min_len = self.int(shape, vocab::SH_MIN_LENGTH);
2063 let max_len = self.int(shape, vocab::SH_MAX_LENGTH);
2064 if let Some(m) = min_len {
2065 let vt = ValueType::Length {
2066 min: Some(m),
2067 max: None,
2068 };
2069 checks.push((
2070 vocab::SH_CC_MIN_LENGTH.into_owned(),
2071 value_type_holds(&vt, u),
2072 ));
2073 }
2074 if let Some(m) = max_len {
2075 let vt = ValueType::Length {
2076 min: None,
2077 max: Some(m),
2078 };
2079 checks.push((
2080 vocab::SH_CC_MAX_LENGTH.into_owned(),
2081 value_type_holds(&vt, u),
2082 ));
2083 }
2084 if let Some(Term::Literal(re)) = self.shapes.object(shape, vocab::SH_PATTERN) {
2085 let flags = match self.shapes.object(shape, vocab::SH_FLAGS) {
2086 Some(Term::Literal(f)) => f.value().to_string(),
2087 _ => String::new(),
2088 };
2089 let vt = ValueType::Pattern {
2090 regex: re.value().to_string(),
2091 flags,
2092 };
2093 checks.push((vocab::SH_CC_PATTERN.into_owned(), value_type_holds(&vt, u)));
2094 }
2095 for list in self.shapes.objects(shape, vocab::SH_IN) {
2097 let members = self.shapes.read_list(&list);
2098 checks.push((vocab::SH_CC_IN.into_owned(), members.contains(u)));
2099 }
2100 for list in self.shapes.objects(shape, vocab::SH_LANGUAGE_IN) {
2101 let languages = self
2102 .shapes
2103 .read_list(&list)
2104 .into_iter()
2105 .filter_map(|term| match term {
2106 Term::Literal(literal) => Some(literal.value().to_string()),
2107 _ => None,
2108 })
2109 .collect();
2110 checks.push((
2111 vocab::SH_CC_LANGUAGE_IN.into_owned(),
2112 value_type_holds(&ValueType::LangIn(languages), u),
2113 ));
2114 }
2115
2116 for list in self.shapes.objects(shape, vocab::SH_AND) {
2118 let ok = self
2119 .shapes
2120 .read_list(&list)
2121 .iter()
2122 .filter_map(term_to_node)
2123 .all(|m| self.conforms(&m, u, visited));
2124 checks.push((vocab::SH_CC_AND.into_owned(), ok));
2125 }
2126 for list in self.shapes.objects(shape, vocab::SH_OR) {
2127 let ok = self
2128 .shapes
2129 .read_list(&list)
2130 .iter()
2131 .filter_map(term_to_node)
2132 .any(|m| self.conforms(&m, u, visited));
2133 checks.push((vocab::SH_CC_OR.into_owned(), ok));
2134 }
2135 for list in self.shapes.objects(shape, vocab::SH_XONE) {
2136 let count = self
2137 .shapes
2138 .read_list(&list)
2139 .iter()
2140 .filter_map(term_to_node)
2141 .filter(|m| self.conforms(m, u, visited))
2142 .count();
2143 checks.push((vocab::SH_CC_XONE.into_owned(), count == 1));
2144 }
2145 for n in self.shapes.objects(shape, vocab::SH_NOT) {
2146 if let Some(nn) = term_to_node(&n) {
2147 checks.push((
2148 vocab::SH_CC_NOT.into_owned(),
2149 !self.conforms(&nn, u, visited),
2150 ));
2151 }
2152 }
2153 for n in self.shapes.objects(shape, vocab::SH_NODE) {
2154 if let Some(nn) = term_to_node(&n) {
2155 checks.push((
2156 vocab::SH_CC_NODE.into_owned(),
2157 self.conforms(&nn, u, visited),
2158 ));
2159 }
2160 }
2161
2162 checks
2163 }
2164
2165 fn is_instance(&self, u: &Term, class: &Term) -> bool {
2166 succ(self.frozen(), u, &class_path()).contains(class)
2167 }
2168
2169 fn int(&self, s: &NamedOrBlankNode, p: NamedNodeRef) -> Option<u64> {
2170 match self.shapes.object(s, p) {
2171 Some(Term::Literal(l)) => l.value().parse().ok(),
2172 _ => None,
2173 }
2174 }
2175
2176 fn bool(&self, s: &NamedOrBlankNode, p: NamedNodeRef) -> bool {
2177 matches!(
2178 self.shapes.object(s, p),
2179 Some(Term::Literal(ref literal)) if matches!(literal.value(), "true" | "1")
2180 )
2181 }
2182
2183 fn severity(&self, shape: &NamedOrBlankNode) -> NamedNode {
2186 match self.shapes.object(shape, vocab::SH_SEVERITY) {
2187 Some(Term::NamedNode(n)) => n,
2188 _ => vocab::SH_VIOLATION.into_owned(),
2189 }
2190 }
2191}
2192
2193fn is_shape_node(shapes: &Loaded, node: &NamedOrBlankNode) -> bool {
2194 shapes.has_type(node, vocab::SH_NODE_SHAPE)
2195 || shapes.has_type(node, vocab::SH_PROPERTY_SHAPE)
2196 || [
2197 vocab::SH_PROPERTY,
2198 vocab::SH_NODE,
2199 vocab::SH_AND,
2200 vocab::SH_OR,
2201 vocab::SH_NOT,
2202 vocab::SH_XONE,
2203 vocab::SH_DATATYPE,
2204 vocab::SH_CLASS,
2205 vocab::SH_NODE_KIND,
2206 vocab::SH_IN,
2207 vocab::SH_HAS_VALUE,
2208 vocab::SH_PROPERTY,
2209 ]
2210 .iter()
2211 .any(|predicate| shapes.object(node, *predicate).is_some())
2212}
2213
2214fn shapes_reference_shapes_graph(shapes: &Loaded) -> bool {
2217 [vocab::SH_SELECT, vocab::SH_ASK].iter().any(|predicate| {
2218 shapes.graph.triples_for_predicate(*predicate).any(
2219 |t| matches!(t.object, oxrdf::TermRef::Literal(l) if l.value().contains("shapesGraph")),
2220 )
2221 })
2222}
2223
2224fn class_path() -> Path {
2225 Path::seq(vec![
2226 Path::Pred(vocab::rdf_type()),
2227 Path::star(Path::Pred(vocab::rdfs_subclassof())),
2228 ])
2229}
2230
2231fn build_class_index(
2240 focus_data: &Graph,
2241 frozen: &FrozenIndexedDataset,
2242) -> HashMap<Term, Vec<Term>> {
2243 let focus_nodes = graph_nodes(focus_data);
2244 let subclass_star = Path::star(Path::Pred(vocab::rdfs_subclassof()));
2245 let mut supers: HashMap<Term, Vec<Term>> = HashMap::new();
2246 let mut index: HashMap<Term, Vec<Term>> = HashMap::new();
2247 let mut seen: HashSet<(Term, Term)> = HashSet::new();
2248 for (node, ty) in frozen.triples_for_predicate(&vocab::rdf_type()) {
2249 if !focus_nodes.contains(&node) {
2250 continue;
2251 }
2252 let classes = supers
2253 .entry(ty.clone())
2254 .or_insert_with(|| succ(frozen, &ty, &subclass_star).into_iter().collect());
2255 for class in classes.iter() {
2256 if seen.insert((class.clone(), node.clone())) {
2257 index.entry(class.clone()).or_default().push(node.clone());
2258 }
2259 }
2260 }
2261 index
2262}
2263
2264fn graph_nodes(graph: &Graph) -> HashSet<Term> {
2265 let mut nodes = HashSet::new();
2266 for triple in graph.iter() {
2267 nodes.insert(node_term(triple.subject));
2268 nodes.insert(triple.object.into_owned());
2269 }
2270 nodes
2271}
2272
2273fn node_term(s: oxrdf::NamedOrBlankNodeRef) -> Term {
2274 crate::path::term_of(s.into_owned())
2275}
2276
2277fn render_term(t: &Term) -> String {
2281 match t {
2282 Term::NamedNode(n) => format!("<{}>", n.as_str()),
2283 Term::BlankNode(b) => format!("_:{}", b.as_str()),
2284 Term::Literal(l) => l.value().to_string(),
2285 }
2286}
2287
2288fn node_term_ref(s: &NamedOrBlankNode) -> Term {
2289 match s {
2290 NamedOrBlankNode::NamedNode(n) => Term::NamedNode(n.clone()),
2291 NamedOrBlankNode::BlankNode(b) => Term::BlankNode(b.clone()),
2292 }
2293}
2294
2295fn map_node_kind(term: &Term) -> Option<NodeKindSet> {
2296 let Term::NamedNode(n) = term else {
2297 return None;
2298 };
2299 let r = n.as_ref();
2300 Some(if r == vocab::SH_IRI {
2301 NodeKindSet::IRI
2302 } else if r == vocab::SH_BLANK_NODE {
2303 NodeKindSet::BLANK_NODE
2304 } else if r == vocab::SH_LITERAL {
2305 NodeKindSet::LITERAL
2306 } else if r == vocab::SH_BLANK_NODE_OR_IRI {
2307 NodeKindSet::BLANK_NODE_OR_IRI
2308 } else if r == vocab::SH_BLANK_NODE_OR_LITERAL {
2309 NodeKindSet::BLANK_NODE_OR_LITERAL
2310 } else if r == vocab::SH_IRI_OR_LITERAL {
2311 NodeKindSet::IRI_OR_LITERAL
2312 } else {
2313 return None;
2314 })
2315}
2316
2317#[cfg(test)]
2318mod sparql_diagnostic_tests {
2319 use super::*;
2320
2321 fn report_for(turtle: &str) -> ValidationReport {
2322 let loaded = shifty_parse::load_turtle(turtle.as_bytes(), None).expect("valid turtle");
2323 validate_report(&loaded, &loaded.graph)
2324 }
2325
2326 #[test]
2330 fn native_sparql_constraint_carries_query_bindings_and_results() {
2331 let report = report_for(
2332 r#"
2333 @prefix sh: <http://www.w3.org/ns/shacl#> .
2334 @prefix ex: <urn:ex/> .
2335
2336 ex:shape a sh:NodeShape ;
2337 sh:targetNode ex:sentinel ;
2338 sh:sparql [
2339 a sh:SPARQLConstraint ;
2340 sh:message "must not be a Bad" ;
2341 sh:select """
2342 PREFIX ex: <urn:ex/>
2343 SELECT ?this WHERE { ?this a ex:Bad . }
2344 """ ;
2345 ] .
2346
2347 ex:sentinel a ex:Bad .
2348 "#,
2349 );
2350 assert!(!report.conforms);
2351 let result = report
2352 .results
2353 .iter()
2354 .find(|r| r.component.as_str() == vocab::SH_CC_SPARQL.as_str())
2355 .expect("one sh:sparql violation");
2356 let diagnostic = result
2357 .sparql_diagnostic
2358 .as_ref()
2359 .expect("sh:sparql violations carry a diagnostic");
2360 assert!(
2361 diagnostic.fallback_reason.is_none(),
2362 "a simple BGP SELECT should lower to a native plan, got: {diagnostic:?}"
2363 );
2364 assert!(
2365 diagnostic.bindings.iter().any(|(name, _)| name == "this"),
2366 "bindings should include $this, got: {:?}",
2367 diagnostic.bindings
2368 );
2369 assert_eq!(
2370 diagnostic.results.len(),
2371 1,
2372 "the query produced one solution row, got: {:?}",
2373 diagnostic.results
2374 );
2375 assert!(
2376 diagnostic.results[0].iter().any(|(name, _)| name == "this"),
2377 "the result row should carry the projected ?this binding, got: {:?}",
2378 diagnostic.results
2379 );
2380 }
2381
2382 #[test]
2386 fn opaque_sparql_constraint_carries_query_bindings_and_results() {
2387 let report = report_for(
2388 r#"
2389 @prefix sh: <http://www.w3.org/ns/shacl#> .
2390 @prefix ex: <urn:ex/> .
2391
2392 ex:shape a sh:NodeShape ;
2393 sh:targetNode ex:sentinel ;
2394 sh:sparql [
2395 a sh:SPARQLConstraint ;
2396 sh:message "must have zero Bad things" ;
2397 sh:select """
2398 PREFIX ex: <urn:ex/>
2399 SELECT ?this ?c WHERE {
2400 {
2401 SELECT ?this (COUNT(*) AS ?c) WHERE { ?this a ex:Bad . }
2402 GROUP BY ?this
2403 }
2404 FILTER (?c > 0)
2405 }
2406 """ ;
2407 ] .
2408
2409 ex:sentinel a ex:Bad .
2410 "#,
2411 );
2412 assert!(!report.conforms);
2413 let result = report
2414 .results
2415 .iter()
2416 .find(|r| r.component.as_str() == vocab::SH_CC_SPARQL.as_str())
2417 .expect("one sh:sparql violation");
2418 let diagnostic = result
2419 .sparql_diagnostic
2420 .as_ref()
2421 .expect("sh:sparql violations carry a diagnostic");
2422 assert!(
2423 diagnostic.fallback_reason.is_some(),
2424 "an aggregate subquery should not lower natively, got: {diagnostic:?}"
2425 );
2426 assert!(
2427 diagnostic.query.contains("COUNT"),
2428 "opaque diagnostic should surface the executed query text, got: {}",
2429 diagnostic.query
2430 );
2431 assert!(
2432 diagnostic.bindings.iter().any(|(name, _)| name == "this"),
2433 "bindings should include $this, got: {:?}",
2434 diagnostic.bindings
2435 );
2436 assert_eq!(
2437 diagnostic.results.len(),
2438 1,
2439 "the query produced one solution row, got: {:?}",
2440 diagnostic.results
2441 );
2442 assert!(
2443 diagnostic.results[0].iter().any(|(name, _)| name == "c"),
2444 "the result row should carry the projected ?c binding \
2445 ($this itself is already covered by `bindings`, since it was \
2446 substituted out of the query text rather than left free), \
2447 got: {:?}",
2448 diagnostic.results
2449 );
2450 }
2451
2452 #[test]
2456 fn custom_component_diagnostic_is_always_opaque() {
2457 let report = report_for(
2458 r#"
2459 @prefix sh: <http://www.w3.org/ns/shacl#> .
2460 @prefix ex: <urn:ex/> .
2461
2462 ex:mustBeBadComponent a sh:ConstraintComponent ;
2463 sh:parameter [ sh:path ex:mustBeBad ] ;
2464 sh:validator ex:mustBeBadValidator .
2465
2466 ex:mustBeBadValidator a sh:SPARQLAskValidator ;
2467 sh:message "must be a Bad" ;
2468 sh:ask "ASK { $this a <urn:ex/Bad> }" .
2469
2470 ex:shape a sh:NodeShape ;
2471 sh:targetNode ex:sentinel ;
2472 ex:mustBeBad true .
2473
2474 ex:sentinel a ex:NotBad .
2475 "#,
2476 );
2477 assert!(!report.conforms);
2478 let result = report
2479 .results
2480 .iter()
2481 .find(|r| r.component.as_str() == "urn:ex/mustBeBadComponent")
2482 .expect("one custom-component violation");
2483 let diagnostic = result
2484 .sparql_diagnostic
2485 .as_ref()
2486 .expect("custom SPARQL component violations carry a diagnostic");
2487 assert!(diagnostic.fallback_reason.is_none());
2488 assert!(diagnostic.query.contains("ASK"));
2489 assert!(
2490 diagnostic.results.is_empty(),
2491 "ASK validators have no projected result row, got: {:?}",
2492 diagnostic.results
2493 );
2494 assert!(
2495 diagnostic.bindings.iter().any(|(name, _)| name == "this"),
2496 "bindings should include $this, got: {:?}",
2497 diagnostic.bindings
2498 );
2499 }
2500}