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