1use crate::frozen::FrozenIndexedDataset;
16use crate::path::succ;
17use crate::sparql::{FunctionDef, 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}
49
50#[derive(Debug, Clone)]
51pub struct ValidationReport {
52 pub conforms: bool,
53 pub results: Vec<ValidationResult>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct PropertyWitness {
68 pub focus: Term,
69 pub shape: Term,
71 pub key: Term,
72 pub values: Vec<Term>,
78}
79
80pub fn validate_report(shapes: &Loaded, data: &Graph) -> ValidationReport {
82 validate_report_with_options(shapes, data, &ValidationOptions::default())
83}
84
85pub fn evaluate_function_expression(shapes: &Loaded, expr: &str) -> Result<Option<Term>, String> {
91 let frozen = FrozenIndexedDataset::from_graph(&shapes.graph);
92 let mut sparql = SparqlExecutor::from_frozen(frozen, false);
93 sparql.set_functions(collect_functions(shapes), UnsupportedPolicy::Ignore);
96 let mut prologue = String::new();
97 if let Some(base) = &shapes.base {
98 prologue.push_str(&format!("BASE <{base}>\n"));
99 }
100 for (prefix, namespace) in &shapes.prefixes {
101 prologue.push_str(&format!("PREFIX {prefix}: <{namespace}>\n"));
102 }
103 sparql.evaluate_expression(&prologue, expr)
104}
105
106pub fn validate_report_with_options(
108 shapes: &Loaded,
109 data: &Graph,
110 options: &ValidationOptions,
111) -> ValidationReport {
112 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
113 let frozen = if has_shapes_graph {
114 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
115 } else {
116 FrozenIndexedDataset::from_graph(data)
117 };
118 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
119}
120
121pub fn validate_report_graphs(shapes: &Loaded, data: &Graph) -> ValidationReport {
123 validate_report_graphs_with_mode_and_options(
124 shapes,
125 data,
126 ValidationGraphMode::default(),
127 &ValidationOptions::default(),
128 )
129}
130
131pub fn validate_report_graphs_with_mode(
133 shapes: &Loaded,
134 data: &Graph,
135 mode: ValidationGraphMode,
136) -> ValidationReport {
137 validate_report_graphs_with_mode_and_options(shapes, data, mode, &ValidationOptions::default())
138}
139
140pub fn validate_report_graphs_with_mode_and_options(
142 shapes: &Loaded,
143 data: &Graph,
144 mode: ValidationGraphMode,
145 options: &ValidationOptions,
146) -> ValidationReport {
147 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
148 match mode {
149 ValidationGraphMode::Data => {
150 let frozen = if has_shapes_graph {
151 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
152 } else {
153 FrozenIndexedDataset::from_graph(data)
154 };
155 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
156 }
157 ValidationGraphMode::Union => {
158 let frozen = if has_shapes_graph {
159 FrozenIndexedDataset::from_graph_union_with_shapes(data, &shapes.graph)
160 } else {
161 FrozenIndexedDataset::from_graph_union(data, &shapes.graph)
162 };
163 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
164 }
165 ValidationGraphMode::UnionAll => {
166 let union = graph_union(data, &shapes.graph);
167 let frozen = if has_shapes_graph {
168 FrozenIndexedDataset::from_graphs(&union, &shapes.graph)
169 } else {
170 FrozenIndexedDataset::from_graph(&union)
171 };
172 validate_report_context(shapes, &union, frozen, has_shapes_graph, options)
173 }
174 }
175}
176
177pub fn property_witnesses_graphs_with_mode(
185 shapes: &Loaded,
186 data: &Graph,
187 mode: ValidationGraphMode,
188 key_path: Option<&Path>,
189) -> Vec<PropertyWitness> {
190 property_witnesses_graphs_with_mode_and_options(
191 shapes,
192 data,
193 mode,
194 key_path,
195 &ValidationOptions::default(),
196 )
197}
198
199pub fn property_witnesses_graphs_with_mode_and_options(
203 shapes: &Loaded,
204 data: &Graph,
205 mode: ValidationGraphMode,
206 key_path: Option<&Path>,
207 options: &ValidationOptions,
208) -> Vec<PropertyWitness> {
209 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
210 let (focus_data, frozen, union_owner);
211 match mode {
212 ValidationGraphMode::Data => {
213 frozen = if has_shapes_graph {
214 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
215 } else {
216 FrozenIndexedDataset::from_graph(data)
217 };
218 focus_data = data;
219 }
220 ValidationGraphMode::Union => {
221 frozen = if has_shapes_graph {
222 FrozenIndexedDataset::from_graph_union_with_shapes(data, &shapes.graph)
223 } else {
224 FrozenIndexedDataset::from_graph_union(data, &shapes.graph)
225 };
226 focus_data = data;
227 }
228 ValidationGraphMode::UnionAll => {
229 union_owner = graph_union(data, &shapes.graph);
230 frozen = if has_shapes_graph {
231 FrozenIndexedDataset::from_graphs(&union_owner, &shapes.graph)
232 } else {
233 FrozenIndexedDataset::from_graph(&union_owner)
234 };
235 focus_data = &union_owner;
236 }
237 }
238 let r = build_reporter(shapes, focus_data, frozen, has_shapes_graph, options);
239 let mut out = Vec::new();
240 for shape in r.target_shapes() {
241 let foci = r.focus_nodes(&shape);
242 r.prefetch_sparql(&shape, &foci);
243 for focus in &foci {
244 let mut check = HashSet::new();
245 let mut results = Vec::new();
246 r.collect(&shape, focus, &mut results, &mut check, &[]);
247 let conforms = !results.iter().any(|result| {
248 Severity::from_named_node(result.severity.clone()).meets(&options.minimum_severity)
249 });
250 if !conforms {
251 continue;
252 }
253 let mut visited = HashSet::new();
254 r.collect_property_witnesses(&shape, focus, &shape, key_path, &mut visited, &mut out);
255 }
256 }
257 out
258}
259
260fn build_reporter<'a>(
264 shapes: &'a Loaded,
265 focus_data: &'a Graph,
266 frozen: FrozenIndexedDataset,
267 has_shapes_graph: bool,
268 options: &'a ValidationOptions,
269) -> Reporter<'a> {
270 let needs_sparql = shapes
273 .graph
274 .triples_for_predicate(vocab::SH_SPARQL)
275 .next()
276 .is_some()
277 || shapes
278 .graph
279 .triples_for_predicate(vocab::SH_TARGET)
280 .next()
281 .is_some();
282 let mut sparql = SparqlExecutor::from_frozen(frozen, needs_sparql && has_shapes_graph);
283 sparql.set_functions(collect_functions(shapes), options.engine.unsupported);
284 let has_explicit_class_target = shapes
288 .graph
289 .triples_for_predicate(vocab::SH_TARGET_CLASS)
290 .next()
291 .is_some();
292 let has_implicit_class_target = shapes.graph.iter().any(|triple| {
293 let subject = triple.subject.into_owned();
294 is_shape_node(shapes, &subject)
295 && (shapes.is_instance_of(&subject, vocab::RDFS_CLASS)
296 || shapes.is_instance_of(&subject, vocab::OWL_CLASS))
297 });
298 let needs_class_index = has_explicit_class_target || has_implicit_class_target;
299 let class_index = if needs_class_index {
300 build_class_index(
301 focus_data,
302 sparql
303 .frozen()
304 .expect("report validation always has a frozen dataset"),
305 )
306 } else {
307 HashMap::new()
308 };
309 Reporter {
310 shapes,
311 focus_data,
312 sparql,
313 needs_sparql,
314 class_index,
315 path_cache: RefCell::new(HashMap::new()),
316 components: build_components(shapes, options.engine.unsupported),
317 entry_shape_names: &options.entry_shape_names,
318 }
319}
320
321fn validate_report_context(
322 shapes: &Loaded,
323 focus_data: &Graph,
324 frozen: FrozenIndexedDataset,
325 has_shapes_graph: bool,
326 options: &ValidationOptions,
327) -> ValidationReport {
328 let r = build_reporter(shapes, focus_data, frozen, has_shapes_graph, options);
329 let mut results = Vec::new();
330 for shape in r.target_shapes() {
331 let foci = r.focus_nodes(&shape);
332 r.prefetch_sparql(&shape, &foci);
333 for focus in &foci {
334 let mut visited = HashSet::new();
335 r.collect(&shape, focus, &mut results, &mut visited, &[]);
336 }
337 }
338 for result in &mut results {
342 if result.messages.is_empty() {
343 let message = r.default_message(result);
344 result.messages = vec![Term::Literal(Literal::new_simple_literal(message))];
345 }
346 }
347 if options.sort_results {
348 results.sort_by(|left, right| {
349 Severity::from_named_node(right.severity.clone())
350 .rank()
351 .cmp(&Severity::from_named_node(left.severity.clone()).rank())
352 .then_with(|| left.focus.to_string().cmp(&right.focus.to_string()))
353 .then_with(|| {
354 left.source_shape
355 .to_string()
356 .cmp(&right.source_shape.to_string())
357 })
358 .then_with(|| left.component.as_str().cmp(right.component.as_str()))
359 });
360 }
361 ValidationReport {
362 conforms: !results.iter().any(|result| {
363 Severity::from_named_node(result.severity.clone()).meets(&options.minimum_severity)
364 }),
365 results,
366 }
367}
368
369pub fn report_to_graph(report: &ValidationReport) -> Graph {
371 let mut g = Graph::new();
372 let root = BlankNode::default();
373 let t = |s: NamedOrBlankNode, p: NamedNodeRef, o: Term| Triple::new(s, p.into_owned(), o);
374
375 g.insert(&t(
376 root.clone().into(),
377 vocab::RDF_TYPE,
378 vocab::SH_VALIDATION_REPORT.into_owned().into(),
379 ));
380 g.insert(&t(
381 root.clone().into(),
382 vocab::SH_CONFORMS,
383 Literal::from(report.conforms).into(),
384 ));
385
386 for r in &report.results {
387 let rn = BlankNode::default();
388 g.insert(&t(root.clone().into(), vocab::SH_RESULT, rn.clone().into()));
389 g.insert(&t(
390 rn.clone().into(),
391 vocab::RDF_TYPE,
392 vocab::SH_VALIDATION_RESULT.into_owned().into(),
393 ));
394 g.insert(&t(rn.clone().into(), vocab::SH_FOCUS_NODE, r.focus.clone()));
395 if let Some(path) = &r.path {
396 g.insert(&t(rn.clone().into(), vocab::SH_RESULT_PATH, path.clone()));
397 }
398 if let Some(value) = &r.value {
399 g.insert(&t(rn.clone().into(), vocab::SH_VALUE, value.clone()));
400 }
401 g.insert(&t(
402 rn.clone().into(),
403 vocab::SH_RESULT_SEVERITY,
404 r.severity.clone().into(),
405 ));
406 g.insert(&t(
407 rn.clone().into(),
408 vocab::SH_SOURCE_CONSTRAINT_COMPONENT,
409 r.component.clone().into(),
410 ));
411 for msg in &r.messages {
412 g.insert(&t(rn.clone().into(), vocab::SH_RESULT_MESSAGE, msg.clone()));
413 }
414 g.insert(&t(
415 rn.into(),
416 vocab::SH_SOURCE_SHAPE,
417 r.source_shape.clone(),
418 ));
419 }
420 g
421}
422
423fn substitute_messages(
430 messages: &[Term],
431 focus: &Term,
432 bindings: &HashMap<String, Term>,
433) -> Vec<Term> {
434 messages
435 .iter()
436 .map(|msg| {
437 let Term::Literal(lit) = msg else {
438 return msg.clone();
439 };
440 let text = lit.value();
441 let substituted = apply_message_template(text, focus, bindings);
442 if substituted == text {
443 msg.clone()
444 } else {
445 Term::Literal(Literal::new_simple_literal(&substituted))
446 }
447 })
448 .collect()
449}
450
451struct CustomComponent {
455 iri: NamedNode,
457 params: Vec<ComponentParam>,
458 node_validator: Option<ComponentValidator>,
460 property_validator: Option<ComponentValidator>,
462 generic_validator: Option<ComponentValidator>,
464}
465
466struct ComponentParam {
467 path: NamedNode,
469 var: String,
471 optional: bool,
472}
473
474struct ComponentValidator {
475 kind: SparqlQueryKind,
476 query: String,
478 messages: Vec<Term>,
479}
480
481fn resolve_validator(
482 shapes: &Loaded,
483 node: Term,
484 component_iri: &NamedNode,
485 policy: UnsupportedPolicy,
486) -> Option<ComponentValidator> {
487 match parse_validator(shapes, &node) {
488 Ok(v) => Some(v),
489 Err(e) => {
490 assert!(
491 policy != UnsupportedPolicy::Error,
492 "invalid SPARQL in custom constraint component <{component_iri}>: {e}"
493 );
494 None
495 }
496 }
497}
498
499fn build_components(shapes: &Loaded, policy: UnsupportedPolicy) -> Vec<CustomComponent> {
510 let mut out = Vec::new();
511 let mut seen = HashSet::new();
512 for triple in shapes.graph.triples_for_predicate(vocab::SH_PARAMETER) {
513 let subject = triple.subject.into_owned();
514 if !seen.insert(subject.clone()) {
515 continue;
516 }
517 let NamedOrBlankNode::NamedNode(iri) = &subject else {
518 continue; };
520 if vocab::NATIVE_CONSTRAINT_COMPONENTS.contains(&iri.as_ref()) {
521 continue; }
523
524 let node_validator = shapes
525 .object(&subject, vocab::SH_NODE_VALIDATOR)
526 .and_then(|v| resolve_validator(shapes, v, iri, policy));
527 let property_validator = shapes
528 .object(&subject, vocab::SH_PROPERTY_VALIDATOR)
529 .and_then(|v| resolve_validator(shapes, v, iri, policy));
530 let generic_validator = shapes
531 .object(&subject, vocab::SH_VALIDATOR)
532 .and_then(|v| resolve_validator(shapes, v, iri, policy));
533 if node_validator.is_none() && property_validator.is_none() && generic_validator.is_none() {
534 continue; }
536 let mut params = Vec::new();
537 for p in shapes.objects(&subject, vocab::SH_PARAMETER) {
538 let Some(pn) = term_to_node(&p) else { continue };
539 let Some(Term::NamedNode(path)) = shapes.object(&pn, vocab::SH_PATH) else {
540 continue;
541 };
542 let var = local_name(path.as_str()).to_string();
543 let optional = matches!(
544 shapes.object(&pn, vocab::SH_OPTIONAL),
545 Some(Term::Literal(ref l)) if l.value() == "true"
546 );
547 params.push(ComponentParam {
548 path,
549 var,
550 optional,
551 });
552 }
553 if params.is_empty() {
554 continue;
555 }
556 out.push(CustomComponent {
557 iri: iri.clone(),
558 params,
559 node_validator,
560 property_validator,
561 generic_validator,
562 });
563 }
564 out.sort_by(|a, b| a.iri.as_str().cmp(b.iri.as_str()));
565 out
566}
567
568fn parse_validator(shapes: &Loaded, node: &Term) -> Result<ComponentValidator, String> {
572 let node = term_to_node(node)
573 .ok_or_else(|| "validator node is not an IRI or blank node".to_string())?;
574 let (kind, raw) = if let Some(Term::Literal(q)) = shapes.object(&node, vocab::SH_ASK) {
575 (SparqlQueryKind::Ask, q.value().to_string())
576 } else if let Some(Term::Literal(q)) = shapes.object(&node, vocab::SH_SELECT) {
577 (SparqlQueryKind::Select, q.value().to_string())
578 } else {
579 return Err("validator has neither sh:ask nor sh:select".to_string());
580 };
581 let (_, query) = canonical_sparql_query(shapes, &node, &raw)
582 .map_err(|e| format!("invalid SPARQL in {node}: {e}"))?;
583 let messages = shapes.objects(&node, vocab::SH_MESSAGE);
584 Ok(ComponentValidator {
585 kind,
586 query,
587 messages,
588 })
589}
590
591fn local_name(iri: &str) -> &str {
594 iri.rsplit(['#', '/']).next().unwrap_or(iri)
595}
596
597pub(crate) fn collect_functions(shapes: &Loaded) -> Vec<FunctionDef> {
602 let mut out = Vec::new();
603 for func in shapes
604 .graph
605 .subjects_for_predicate_object(vocab::RDF_TYPE, vocab::SH_SPARQL_FUNCTION)
606 .map(|s| s.into_owned())
607 .collect::<Vec<_>>()
608 {
609 let NamedOrBlankNode::NamedNode(iri) = &func else {
610 continue;
611 };
612 let raw = match shapes
613 .object(&func, vocab::SH_SELECT)
614 .or_else(|| shapes.object(&func, vocab::SH_ASK))
615 {
616 Some(Term::Literal(q)) => q.value().to_string(),
617 _ => continue,
618 };
619 let Ok((_, query)) = canonical_sparql_query(shapes, &func, &raw) else {
620 continue;
621 };
622 out.push(FunctionDef {
623 iri: iri.clone(),
624 params: function_param_names(shapes, &func),
625 reads_graph: crate::sparql::query_reads_graph(&query),
626 query,
627 });
628 }
629 out
630}
631
632fn function_param_names(shapes: &Loaded, func: &NamedOrBlankNode) -> Vec<String> {
635 let mut params: Vec<(i64, String)> = shapes
636 .objects(func, vocab::SH_PARAMETER)
637 .iter()
638 .filter_map(|p| {
639 let pn = term_to_node(p)?;
640 let order = match shapes.object(&pn, vocab::SH_ORDER) {
641 Some(Term::Literal(l)) => l.value().parse::<i64>().unwrap_or(0),
642 _ => 0,
643 };
644 let name = match shapes.object(&pn, vocab::SH_NAME) {
645 Some(Term::Literal(l)) => l.value().to_string(),
646 _ => match shapes.object(&pn, vocab::SH_PATH) {
647 Some(Term::NamedNode(n)) => local_name(n.as_str()).to_string(),
648 _ => return None,
649 },
650 };
651 Some((order, name))
652 })
653 .collect();
654 params.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
655 params.into_iter().map(|(_, name)| name).collect()
656}
657
658struct Reporter<'a> {
659 shapes: &'a Loaded,
660 focus_data: &'a Graph,
661 sparql: SparqlExecutor,
662 needs_sparql: bool,
663 class_index: HashMap<Term, Vec<Term>>,
666 path_cache: RefCell<HashMap<NamedOrBlankNode, PathCacheEntry>>,
669 components: Vec<CustomComponent>,
672 entry_shape_names: &'a [String],
675}
676
677type Visited = HashSet<(NamedOrBlankNode, Term)>;
678
679type PathCacheEntry = (Option<Term>, Option<Path>);
681
682impl Reporter<'_> {
683 fn frozen(&self) -> &FrozenIndexedDataset {
684 self.sparql
685 .frozen()
686 .expect("report validation always has a frozen dataset")
687 }
688
689 fn target_shapes(&self) -> Vec<NamedOrBlankNode> {
690 let mut found: HashSet<NamedOrBlankNode> = HashSet::new();
691 for t in self.shapes.graph.iter() {
692 let p = t.predicate;
693 if p == vocab::SH_TARGET_NODE
694 || p == vocab::SH_TARGET_CLASS
695 || p == vocab::SH_TARGET_SUBJECTS_OF
696 || p == vocab::SH_TARGET_OBJECTS_OF
697 {
698 found.insert(t.subject.into_owned());
699 }
700 if p == vocab::SH_TARGET
702 && let Some(target) = term_to_node(&t.object.into_owned())
703 && self.shapes.object(&target, vocab::SH_SELECT).is_some()
704 {
705 found.insert(t.subject.into_owned());
706 }
707 if p == vocab::RDF_TYPE {
709 let s = t.subject.into_owned();
710 if self.is_class(&s) && self.is_shape(&s) {
711 found.insert(s);
712 }
713 }
714 }
715 let mut v: Vec<_> = found.into_iter().collect();
716 v.retain(|shape| self.entry_shape_selected(shape));
717 v.sort_by_key(|n| n.to_string());
718 v
719 }
720
721 fn entry_shape_selected(&self, shape: &NamedOrBlankNode) -> bool {
722 let actual = match shape {
723 NamedOrBlankNode::NamedNode(named) => Some(named.as_str()),
724 NamedOrBlankNode::BlankNode(_) => None,
725 };
726 entry_shape_name_selected(self.entry_shape_names, actual)
727 }
728
729 fn is_shape(&self, n: &NamedOrBlankNode) -> bool {
731 is_shape_node(self.shapes, n)
732 }
733
734 fn is_class(&self, n: &NamedOrBlankNode) -> bool {
735 self.shapes.is_instance_of(n, vocab::RDFS_CLASS)
736 || self.shapes.is_instance_of(n, vocab::OWL_CLASS)
737 }
738
739 fn deactivated(&self, n: &NamedOrBlankNode) -> bool {
740 matches!(self.shapes.object(n, vocab::SH_DEACTIVATED),
741 Some(Term::Literal(ref l)) if l.value() == "true")
742 }
743
744 fn focus_nodes(&self, shape: &NamedOrBlankNode) -> Vec<Term> {
745 let mut nodes = Vec::new();
746 nodes.extend(self.shapes.objects(shape, vocab::SH_TARGET_NODE));
747 for c in self.shapes.objects(shape, vocab::SH_TARGET_CLASS) {
748 if let Some(instances) = self.class_index.get(&c) {
749 nodes.extend(instances.iter().cloned());
750 }
751 }
752 for p in self.shapes.objects(shape, vocab::SH_TARGET_SUBJECTS_OF) {
753 if let Term::NamedNode(n) = p {
754 nodes.extend(
755 self.focus_data
756 .triples_for_predicate(n.as_ref())
757 .map(|t| node_term(t.subject)),
758 );
759 }
760 }
761 for p in self.shapes.objects(shape, vocab::SH_TARGET_OBJECTS_OF) {
762 if let Term::NamedNode(n) = p {
763 nodes.extend(
764 self.focus_data
765 .triples_for_predicate(n.as_ref())
766 .map(|t| t.object.into_owned()),
767 );
768 }
769 }
770 if self.needs_sparql {
773 let exec = &self.sparql;
774 for target in self.shapes.objects(shape, vocab::SH_TARGET) {
775 let Some(target_node) = term_to_node(&target) else {
776 continue;
777 };
778 let Some(Term::Literal(query)) = self.shapes.object(&target_node, vocab::SH_SELECT)
779 else {
780 continue;
781 };
782 let Ok((_, canonical)) =
784 canonical_sparql_query(self.shapes, &target_node, query.value())
785 else {
786 continue;
787 };
788 if let Ok(found) = exec.target_nodes(&canonical) {
789 nodes.extend(found);
790 }
791 }
792 }
793 if let NamedOrBlankNode::NamedNode(n) = shape
795 && self.is_class(shape)
796 {
797 let class = Term::NamedNode(n.clone());
798 if let Some(instances) = self.class_index.get(&class) {
799 nodes.extend(instances.iter().cloned());
800 }
801 }
802 let mut seen = HashSet::new();
803 nodes.retain(|t| seen.insert(t.clone()));
804 nodes
805 }
806
807 fn shape_path(&self, shape: &NamedOrBlankNode) -> (Option<Term>, Option<Path>) {
810 if let Some(cached) = self.path_cache.borrow().get(shape) {
811 return cached.clone();
812 }
813 let path_term = self.shapes.object(shape, vocab::SH_PATH);
814 let parsed = path_term
815 .as_ref()
816 .and_then(|t| parse_path(self.shapes, t).ok());
817 let entry = (path_term, parsed);
818 self.path_cache
819 .borrow_mut()
820 .insert(shape.clone(), entry.clone());
821 entry
822 }
823
824 fn messages_or_inherited(&self, shape: &NamedOrBlankNode, inherited: &[Term]) -> Vec<Term> {
830 let own = self.messages(shape);
831 if own.is_empty() {
832 inherited.to_vec()
833 } else {
834 own
835 }
836 }
837
838 fn collect(
843 &self,
844 shape: &NamedOrBlankNode,
845 focus: &Term,
846 out: &mut Vec<ValidationResult>,
847 visited: &mut Visited,
848 inherited: &[Term],
849 ) {
850 if self.deactivated(shape) {
851 return; }
853 let key = (shape.clone(), focus.clone());
854 if !visited.insert(key.clone()) {
855 return; }
857
858 let (path_term, parsed) = self.shape_path(shape);
859 let value_nodes: Vec<Term> = match &parsed {
860 Some(p) => succ(self.frozen(), focus, p).into_iter().collect(),
861 None => vec![focus.clone()],
862 };
863 let severity = self.severity(shape);
864 let messages = self.messages_or_inherited(shape, inherited);
865 let push = |out: &mut Vec<ValidationResult>, value, component| {
866 out.push(ValidationResult {
867 focus: focus.clone(),
868 path: path_term.clone(),
869 value,
870 component,
871 source_shape: node_term_ref(shape),
872 severity: severity.clone(),
873 messages: messages.clone(),
874 });
875 };
876
877 if parsed.is_some() {
879 if let Some(min) = self.int(shape, vocab::SH_MIN_COUNT)
880 && (value_nodes.len() as u64) < min
881 {
882 push(out, None, vocab::SH_CC_MIN_COUNT.into_owned());
883 }
884 if let Some(max) = self.int(shape, vocab::SH_MAX_COUNT)
885 && (value_nodes.len() as u64) > max
886 {
887 push(out, None, vocab::SH_CC_MAX_COUNT.into_owned());
888 }
889 }
890
891 for hv in self.shapes.objects(shape, vocab::SH_HAS_VALUE) {
893 if !value_nodes.contains(&hv) {
894 push(out, None, vocab::SH_CC_HAS_VALUE.into_owned());
895 }
896 }
897
898 self.collect_closed(shape, focus, &value_nodes, out, &messages);
899 self.collect_property_pairs(shape, focus, &path_term, &value_nodes, out, &messages);
900 self.collect_unique_lang(shape, focus, &path_term, &value_nodes, out, &messages);
901 self.collect_qualified_counts(
902 shape,
903 focus,
904 &path_term,
905 &value_nodes,
906 out,
907 visited,
908 &messages,
909 );
910
911 for u in &value_nodes {
913 for (component, ok) in self.value_checks(shape, u, visited) {
914 if !ok {
915 push(out, Some(u.clone()), component);
916 }
917 }
918 }
919
920 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
923 if let Some(pn) = term_to_node(&prop) {
924 for u in &value_nodes {
925 self.collect(&pn, u, out, visited, &messages);
926 }
927 }
928 }
929
930 self.collect_sparql(shape, focus, &path_term, &parsed, out, &messages);
931 self.collect_expression(shape, focus, out, visited, &messages);
932 self.collect_components(
933 shape,
934 focus,
935 &path_term,
936 &parsed,
937 &value_nodes,
938 out,
939 &messages,
940 );
941
942 visited.remove(&key);
943 }
944
945 fn collect_property_witnesses(
952 &self,
953 shape: &NamedOrBlankNode,
954 focus: &Term,
955 profile: &NamedOrBlankNode,
956 key_path: Option<&Path>,
957 visited: &mut Visited,
958 out: &mut Vec<PropertyWitness>,
959 ) {
960 if self.deactivated(shape) {
961 return;
962 }
963 let key = (shape.clone(), focus.clone());
964 if !visited.insert(key.clone()) {
965 return;
966 }
967
968 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
969 if let Some(pn) = term_to_node(&prop) {
970 self.collect_property_binding(&pn, focus, profile, key_path, visited, out);
971 }
972 }
973 for list in self.shapes.objects(shape, vocab::SH_AND) {
974 for member in self.shapes.read_list(&list) {
975 if let Some(mn) = term_to_node(&member) {
976 self.collect_property_witnesses(&mn, focus, profile, key_path, visited, out);
977 }
978 }
979 }
980 for n in self.shapes.objects(shape, vocab::SH_NODE) {
981 if let Some(nn) = term_to_node(&n) {
982 self.collect_property_witnesses(&nn, focus, profile, key_path, visited, out);
983 }
984 }
985
986 visited.remove(&key);
987 }
988
989 fn collect_property_binding(
996 &self,
997 pn: &NamedOrBlankNode,
998 focus: &Term,
999 profile: &NamedOrBlankNode,
1000 key_path: Option<&Path>,
1001 visited: &mut Visited,
1002 out: &mut Vec<PropertyWitness>,
1003 ) {
1004 if self.deactivated(pn) {
1005 return;
1006 }
1007 let (_, parsed_path) = self.shape_path(pn);
1008 let Some(path) = parsed_path else { return };
1009
1010 let key = key_path
1016 .and_then(|kp| {
1017 let mut matches: Vec<Term> = succ(&self.shapes.graph, &node_term_ref(pn), kp)
1018 .into_iter()
1019 .collect();
1020 matches.sort_by_key(ToString::to_string);
1021 matches.into_iter().next()
1022 })
1023 .unwrap_or_else(|| node_term_ref(pn));
1024
1025 let value_nodes: Vec<Term> = succ(self.frozen(), focus, &path).into_iter().collect();
1026 let values = match self.shapes.object(pn, vocab::SH_QUALIFIED_VALUE_SHAPE) {
1027 Some(qualifier_term) => {
1028 let Some(qualifier) = term_to_node(&qualifier_term) else {
1029 return;
1030 };
1031 let siblings = if self.bool(pn, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
1032 self.sibling_qualified_shapes(pn, &qualifier)
1033 } else {
1034 Vec::new()
1035 };
1036 value_nodes
1037 .into_iter()
1038 .filter(|v| {
1039 self.conforms(&qualifier, v, visited)
1040 && siblings
1041 .iter()
1042 .all(|sibling| !self.conforms(sibling, v, visited))
1043 })
1044 .collect()
1045 }
1046 None => value_nodes,
1047 };
1048
1049 out.push(PropertyWitness {
1050 focus: focus.clone(),
1051 shape: node_term_ref(profile),
1052 key,
1053 values,
1054 });
1055 }
1056
1057 #[allow(clippy::too_many_arguments)]
1064 fn collect_components(
1065 &self,
1066 shape: &NamedOrBlankNode,
1067 focus: &Term,
1068 path_term: &Option<Term>,
1069 parsed_path: &Option<Path>,
1070 value_nodes: &[Term],
1071 out: &mut Vec<ValidationResult>,
1072 inherited: &[Term],
1073 ) {
1074 if self.components.is_empty() {
1075 return;
1076 }
1077 let is_property_shape = parsed_path.is_some();
1078 for component in &self.components {
1079 let mut params: Vec<(String, Term)> = Vec::new();
1081 let mut activated = true;
1082 for p in &component.params {
1083 if let Some(value) = self.shapes.object(shape, p.path.as_ref()) {
1084 params.push((p.var.clone(), value));
1085 } else if !p.optional {
1086 activated = false;
1087 break;
1088 }
1089 }
1090 if !activated {
1091 continue;
1092 }
1093
1094 let validator = if is_property_shape {
1095 component
1096 .property_validator
1097 .as_ref()
1098 .or(component.generic_validator.as_ref())
1099 } else {
1100 component
1101 .node_validator
1102 .as_ref()
1103 .or(component.generic_validator.as_ref())
1104 };
1105 let Some(validator) = validator else { continue };
1106
1107 let mut base = params;
1111 base.push(("currentShape".to_string(), node_term_ref(shape)));
1112 let path = parsed_path.as_ref();
1113
1114 match validator.kind {
1115 SparqlQueryKind::Ask => {
1116 for value in value_nodes {
1117 let mut bindings = base.clone();
1118 bindings.push(("this".to_string(), focus.clone()));
1119 bindings.push(("value".to_string(), value.clone()));
1120 let violates = match self.sparql.eval_ask(&validator.query, path, &bindings)
1122 {
1123 Ok(conforms) => !conforms,
1124 Err(_) => true,
1125 };
1126 if violates {
1127 self.push_component_result(
1128 out,
1129 component,
1130 shape,
1131 focus,
1132 path_term.clone(),
1133 Some(value.clone()),
1134 &bindings,
1135 &validator.messages,
1136 inherited,
1137 );
1138 }
1139 }
1140 }
1141 SparqlQueryKind::Select => {
1142 let mut bindings = base.clone();
1143 bindings.push(("this".to_string(), focus.clone()));
1144 match self.sparql.eval_select(&validator.query, path, &bindings) {
1145 Ok(rows) => {
1146 for row in rows {
1147 let value = row
1150 .get("value")
1151 .cloned()
1152 .or_else(|| (!is_property_shape).then(|| focus.clone()));
1153 let path = row.get("path").cloned().or_else(|| path_term.clone());
1154 let mut binds = bindings.clone();
1155 binds.extend(row);
1156 self.push_component_result(
1157 out,
1158 component,
1159 shape,
1160 focus,
1161 path,
1162 value,
1163 &binds,
1164 &validator.messages,
1165 inherited,
1166 );
1167 }
1168 }
1169 Err(_) => self.push_component_result(
1170 out,
1171 component,
1172 shape,
1173 focus,
1174 path_term.clone(),
1175 None,
1176 &bindings,
1177 &validator.messages,
1178 inherited,
1179 ),
1180 }
1181 }
1182 }
1183 }
1184 }
1185
1186 #[allow(clippy::too_many_arguments)]
1187 fn push_component_result(
1188 &self,
1189 out: &mut Vec<ValidationResult>,
1190 component: &CustomComponent,
1191 shape: &NamedOrBlankNode,
1192 focus: &Term,
1193 path: Option<Term>,
1194 value: Option<Term>,
1195 bindings: &[(String, Term)],
1196 validator_messages: &[Term],
1197 inherited: &[Term],
1198 ) {
1199 let raw = if validator_messages.is_empty() {
1200 self.messages_or_inherited(shape, inherited)
1201 } else {
1202 validator_messages.to_vec()
1203 };
1204 let bind_map: HashMap<String, Term> = bindings.iter().cloned().collect();
1205 let messages = substitute_messages(&raw, focus, &bind_map);
1206 out.push(ValidationResult {
1207 focus: focus.clone(),
1208 path,
1209 value,
1210 component: component.iri.clone(),
1211 source_shape: node_term_ref(shape),
1212 severity: self.severity(shape),
1213 messages,
1214 });
1215 }
1216
1217 fn collect_expression(
1224 &self,
1225 shape: &NamedOrBlankNode,
1226 focus: &Term,
1227 out: &mut Vec<ValidationResult>,
1228 visited: &mut Visited,
1229 inherited: &[Term],
1230 ) {
1231 for expr_term in self.shapes.objects(shape, vocab::SH_EXPRESSION) {
1232 let Some(results) = self.eval_node_expr(&expr_term, focus, visited) else {
1233 continue;
1234 };
1235 for value in results {
1236 if is_boolean_true(&value) {
1237 continue;
1238 }
1239 out.push(ValidationResult {
1240 focus: focus.clone(),
1241 path: None,
1242 value: Some(value),
1243 component: vocab::SH_CC_EXPRESSION.into_owned(),
1244 source_shape: node_term_ref(shape),
1245 severity: self.severity(shape),
1246 messages: self.messages_or_inherited(shape, inherited),
1247 });
1248 }
1249 }
1250 }
1251
1252 fn eval_node_expr(
1257 &self,
1258 term: &Term,
1259 focus: &Term,
1260 visited: &mut Visited,
1261 ) -> Option<Vec<Term>> {
1262 match term {
1263 Term::NamedNode(n) if n.as_ref() == vocab::SH_THIS => Some(vec![focus.clone()]),
1264 Term::NamedNode(_) | Term::Literal(_) => Some(vec![term.clone()]),
1265 Term::BlankNode(_) => {
1266 let node = term_to_node(term)?;
1267 if let Some(path_term) = self.shapes.object(&node, vocab::SH_PATH) {
1268 let path = parse_path(self.shapes, &path_term).ok()?;
1269 Some(succ(self.frozen(), focus, &path).into_iter().collect())
1270 } else if let Some(filter_shape) = self.shapes.object(&node, vocab::SH_FILTER_SHAPE)
1271 {
1272 let filter_shape = term_to_node(&filter_shape)?;
1273 let nodes_term = self.shapes.object(&node, vocab::SH_NODES)?;
1274 let inputs = self.eval_node_expr(&nodes_term, focus, visited)?;
1275 Some(
1276 inputs
1277 .into_iter()
1278 .filter(|x| self.conforms(&filter_shape, x, visited))
1279 .collect(),
1280 )
1281 } else if let Some(list) = self.shapes.object(&node, vocab::SH_INTERSECTION) {
1282 self.eval_node_expr_set(&list, focus, visited, true)
1283 } else if let Some(list) = self.shapes.object(&node, vocab::SH_UNION) {
1284 self.eval_node_expr_set(&list, focus, visited, false)
1285 } else {
1286 None
1288 }
1289 }
1290 }
1291 }
1292
1293 fn eval_node_expr_set(
1297 &self,
1298 list_head: &Term,
1299 focus: &Term,
1300 visited: &mut Visited,
1301 intersect: bool,
1302 ) -> Option<Vec<Term>> {
1303 let members = self.shapes.read_list(list_head);
1304 if members.is_empty() {
1305 return None;
1306 }
1307 let mut iter = members.iter();
1308 let mut acc = self.eval_node_expr(iter.next().unwrap(), focus, visited)?;
1309 for member in iter {
1310 let next = self.eval_node_expr(member, focus, visited)?;
1311 if intersect {
1312 acc.retain(|x| next.contains(x));
1313 } else {
1314 for t in next {
1315 if !acc.contains(&t) {
1316 acc.push(t);
1317 }
1318 }
1319 }
1320 }
1321 Some(acc)
1322 }
1323
1324 fn build_sparql_constraint(
1333 &self,
1334 shape: &NamedOrBlankNode,
1335 constraint_node: &NamedOrBlankNode,
1336 parsed_path: &Option<Path>,
1337 ) -> Option<SparqlConstraint> {
1338 let (kind, raw) = if let Some(Term::Literal(query)) =
1339 self.shapes.object(constraint_node, vocab::SH_SELECT)
1340 {
1341 (SparqlQueryKind::Select, query.value().to_string())
1342 } else if let Some(Term::Literal(query)) =
1343 self.shapes.object(constraint_node, vocab::SH_ASK)
1344 {
1345 (SparqlQueryKind::Ask, query.value().to_string())
1346 } else {
1347 return None;
1348 };
1349 let (_, query) = canonical_sparql_query(self.shapes, constraint_node, &raw).ok()?;
1350 Some(SparqlConstraint {
1351 kind,
1352 query,
1353 path: parsed_path.clone(),
1354 shape: Some(node_term_ref(shape)),
1355 messages: Vec::new(),
1358 extra_bindings: Vec::new(),
1359 bind_value_to_this: false,
1360 })
1361 }
1362
1363 fn prefetch_sparql(&self, shape: &NamedOrBlankNode, foci: &[Term]) {
1367 if !self.needs_sparql || foci.len() < 2 {
1368 return;
1369 }
1370 let (_, parsed_path) = self.shape_path(shape);
1371 for constraint_term in self.shapes.objects(shape, vocab::SH_SPARQL) {
1372 let Some(constraint_node) = term_to_node(&constraint_term) else {
1373 continue;
1374 };
1375 if let Some(constraint) =
1376 self.build_sparql_constraint(shape, &constraint_node, &parsed_path)
1377 {
1378 let _ = self.sparql.prefetch_constraint(&constraint, foci);
1379 }
1380 }
1381 }
1382
1383 fn collect_sparql(
1384 &self,
1385 shape: &NamedOrBlankNode,
1386 focus: &Term,
1387 path_term: &Option<Term>,
1388 parsed_path: &Option<Path>,
1389 out: &mut Vec<ValidationResult>,
1390 inherited: &[Term],
1391 ) {
1392 if !self.needs_sparql {
1393 return;
1394 }
1395 let sparql = &self.sparql;
1396 let severity = self.severity(shape);
1397 for constraint_term in self.shapes.objects(shape, vocab::SH_SPARQL) {
1398 let Some(constraint_node) = term_to_node(&constraint_term) else {
1399 continue;
1400 };
1401 let Some(constraint) =
1402 self.build_sparql_constraint(shape, &constraint_node, parsed_path)
1403 else {
1404 continue;
1405 };
1406 let raw_messages = {
1410 let on_constraint = self.shapes.objects(&constraint_node, vocab::SH_MESSAGE);
1411 if on_constraint.is_empty() {
1412 self.messages_or_inherited(shape, inherited)
1413 } else {
1414 on_constraint
1415 }
1416 };
1417 match sparql.constraint_violations(&constraint, focus) {
1418 Ok(violations) => {
1419 for violation in violations {
1420 let messages =
1421 substitute_messages(&raw_messages, focus, &violation.bindings);
1422 let value = violation.value.or_else(|| match constraint.kind {
1425 SparqlQueryKind::Select => Some(focus.clone()),
1426 SparqlQueryKind::Ask => None,
1427 });
1428 out.push(ValidationResult {
1429 focus: focus.clone(),
1430 path: violation.path.or_else(|| path_term.clone()),
1431 value,
1432 component: vocab::SH_CC_SPARQL.into_owned(),
1433 source_shape: node_term_ref(shape),
1434 severity: severity.clone(),
1435 messages,
1436 });
1437 }
1438 }
1439 Err(error) => {
1443 let mut messages = raw_messages;
1444 messages.push(Term::Literal(Literal::new_simple_literal(format!(
1445 "SPARQL constraint evaluation failed: {error}"
1446 ))));
1447 out.push(ValidationResult {
1448 focus: focus.clone(),
1449 path: path_term.clone(),
1450 value: None,
1451 component: vocab::SH_CC_SPARQL.into_owned(),
1452 source_shape: node_term_ref(shape),
1453 severity: severity.clone(),
1454 messages,
1455 });
1456 }
1457 }
1458 }
1459 }
1460
1461 fn collect_closed(
1462 &self,
1463 shape: &NamedOrBlankNode,
1464 focus: &Term,
1465 value_nodes: &[Term],
1466 out: &mut Vec<ValidationResult>,
1467 inherited: &[Term],
1468 ) {
1469 if !self.bool(shape, vocab::SH_CLOSED) {
1470 return;
1471 }
1472 let mut allowed = HashSet::new();
1473 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
1474 let Some(prop) = term_to_node(&prop) else {
1475 continue;
1476 };
1477 if let Some(Term::NamedNode(path)) = self.shapes.object(&prop, vocab::SH_PATH) {
1478 allowed.insert(path);
1479 }
1480 }
1481 for list in self.shapes.objects(shape, vocab::SH_IGNORED_PROPERTIES) {
1482 for term in self.shapes.read_list(&list) {
1483 if let Term::NamedNode(predicate) = term {
1484 allowed.insert(predicate);
1485 }
1486 }
1487 }
1488 for value_node in value_nodes {
1489 for (predicate, object) in self.frozen().outgoing(value_node) {
1490 if allowed.contains(&predicate) {
1491 continue;
1492 }
1493 out.push(ValidationResult {
1494 focus: focus.clone(),
1495 path: Some(Term::NamedNode(predicate)),
1496 value: Some(object),
1497 component: vocab::SH_CC_CLOSED.into_owned(),
1498 source_shape: node_term_ref(shape),
1499 severity: self.severity(shape),
1500 messages: self.messages_or_inherited(shape, inherited),
1501 });
1502 }
1503 }
1504 }
1505
1506 #[allow(clippy::too_many_arguments)]
1507 fn collect_property_pairs(
1508 &self,
1509 shape: &NamedOrBlankNode,
1510 focus: &Term,
1511 path: &Option<Term>,
1512 value_nodes: &[Term],
1513 out: &mut Vec<ValidationResult>,
1514 inherited: &[Term],
1515 ) {
1516 for predicate in self.shapes.objects(shape, vocab::SH_EQUALS) {
1517 let Term::NamedNode(predicate) = predicate else {
1518 continue;
1519 };
1520 let other = succ(self.frozen(), focus, &Path::Pred(predicate));
1521 for value in value_nodes.iter().filter(|value| !other.contains(*value)) {
1522 self.push(
1523 out,
1524 shape,
1525 focus,
1526 path.clone(),
1527 Some((*value).clone()),
1528 vocab::SH_CC_EQUALS,
1529 inherited,
1530 );
1531 }
1532 for value in other.iter().filter(|value| !value_nodes.contains(*value)) {
1533 self.push(
1534 out,
1535 shape,
1536 focus,
1537 path.clone(),
1538 Some(value.clone()),
1539 vocab::SH_CC_EQUALS,
1540 inherited,
1541 );
1542 }
1543 }
1544 for predicate in self.shapes.objects(shape, vocab::SH_DISJOINT) {
1545 let Term::NamedNode(predicate) = predicate else {
1546 continue;
1547 };
1548 let other = succ(self.frozen(), focus, &Path::Pred(predicate));
1549 for value in value_nodes.iter().filter(|value| other.contains(*value)) {
1550 self.push(
1551 out,
1552 shape,
1553 focus,
1554 path.clone(),
1555 Some((*value).clone()),
1556 vocab::SH_CC_DISJOINT,
1557 inherited,
1558 );
1559 }
1560 }
1561 for (constraint, component, inclusive) in [
1562 (vocab::SH_LESS_THAN, vocab::SH_CC_LESS_THAN, false),
1563 (
1564 vocab::SH_LESS_THAN_OR_EQUALS,
1565 vocab::SH_CC_LESS_THAN_OR_EQUALS,
1566 true,
1567 ),
1568 ] {
1569 for predicate in self.shapes.objects(shape, constraint) {
1570 let Term::NamedNode(predicate) = predicate else {
1571 continue;
1572 };
1573 for left in value_nodes {
1574 for right in succ(self.frozen(), focus, &Path::Pred(predicate.clone())) {
1575 let ordering = compare_terms(left, &right);
1576 let passes = ordering == Some(Ordering::Less)
1577 || inclusive && ordering == Some(Ordering::Equal);
1578 if !passes {
1579 self.push(
1580 out,
1581 shape,
1582 focus,
1583 path.clone(),
1584 Some(left.clone()),
1585 component,
1586 inherited,
1587 );
1588 }
1589 }
1590 }
1591 }
1592 }
1593 }
1594
1595 #[allow(clippy::too_many_arguments)]
1596 fn collect_unique_lang(
1597 &self,
1598 shape: &NamedOrBlankNode,
1599 focus: &Term,
1600 path: &Option<Term>,
1601 value_nodes: &[Term],
1602 out: &mut Vec<ValidationResult>,
1603 inherited: &[Term],
1604 ) {
1605 if !self.bool(shape, vocab::SH_UNIQUE_LANG) {
1606 return;
1607 }
1608 let mut counts = HashMap::new();
1609 for value in value_nodes {
1610 if let Term::Literal(literal) = value
1611 && let Some(language) = literal.language()
1612 {
1613 *counts
1614 .entry(language.to_ascii_lowercase())
1615 .or_insert(0usize) += 1;
1616 }
1617 }
1618 for _ in counts.values().filter(|count| **count > 1) {
1619 self.push(
1620 out,
1621 shape,
1622 focus,
1623 path.clone(),
1624 None,
1625 vocab::SH_CC_UNIQUE_LANG,
1626 inherited,
1627 );
1628 }
1629 }
1630
1631 #[allow(clippy::too_many_arguments)]
1632 fn collect_qualified_counts(
1633 &self,
1634 shape: &NamedOrBlankNode,
1635 focus: &Term,
1636 path: &Option<Term>,
1637 value_nodes: &[Term],
1638 out: &mut Vec<ValidationResult>,
1639 visited: &mut Visited,
1640 inherited: &[Term],
1641 ) {
1642 for qualifier in self.shapes.objects(shape, vocab::SH_QUALIFIED_VALUE_SHAPE) {
1643 let Some(qualifier) = term_to_node(&qualifier) else {
1644 continue;
1645 };
1646 let siblings = if self.bool(shape, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
1647 self.sibling_qualified_shapes(shape, &qualifier)
1648 } else {
1649 Vec::new()
1650 };
1651 let count = value_nodes
1652 .iter()
1653 .filter(|value| {
1654 self.conforms(&qualifier, value, visited)
1655 && siblings
1656 .iter()
1657 .all(|sibling| !self.conforms(sibling, value, visited))
1658 })
1659 .count() as u64;
1660 if let Some(min) = self.int(shape, vocab::SH_QUALIFIED_MIN_COUNT)
1661 && count < min
1662 {
1663 self.push(
1664 out,
1665 shape,
1666 focus,
1667 path.clone(),
1668 None,
1669 vocab::SH_CC_QUALIFIED_MIN_COUNT,
1670 inherited,
1671 );
1672 }
1673 if let Some(max) = self.int(shape, vocab::SH_QUALIFIED_MAX_COUNT)
1674 && count > max
1675 {
1676 self.push(
1677 out,
1678 shape,
1679 focus,
1680 path.clone(),
1681 None,
1682 vocab::SH_CC_QUALIFIED_MAX_COUNT,
1683 inherited,
1684 );
1685 }
1686 }
1687 }
1688
1689 fn sibling_qualified_shapes(
1690 &self,
1691 shape: &NamedOrBlankNode,
1692 qualifier: &NamedOrBlankNode,
1693 ) -> Vec<NamedOrBlankNode> {
1694 let shape_term = node_term_ref(shape);
1695 let mut siblings = HashSet::new();
1696 for triple in self.shapes.graph.triples_for_predicate(vocab::SH_PROPERTY) {
1697 if triple.object != shape_term.as_ref() {
1698 continue;
1699 }
1700 let parent = triple.subject.into_owned();
1701 for property in self.shapes.objects(&parent, vocab::SH_PROPERTY) {
1702 let Some(property) = term_to_node(&property) else {
1703 continue;
1704 };
1705 for qualifier in self
1706 .shapes
1707 .objects(&property, vocab::SH_QUALIFIED_VALUE_SHAPE)
1708 {
1709 if let Some(qualifier) = term_to_node(&qualifier) {
1710 siblings.insert(qualifier);
1711 }
1712 }
1713 }
1714 }
1715 siblings.remove(qualifier);
1716 siblings.into_iter().collect()
1717 }
1718
1719 #[allow(clippy::too_many_arguments)]
1720 fn push(
1721 &self,
1722 out: &mut Vec<ValidationResult>,
1723 shape: &NamedOrBlankNode,
1724 focus: &Term,
1725 path: Option<Term>,
1726 value: Option<Term>,
1727 component: NamedNodeRef<'static>,
1728 inherited: &[Term],
1729 ) {
1730 let mut bindings = HashMap::new();
1731 if let Some(v) = &value {
1732 bindings.insert("value".to_string(), v.clone());
1733 }
1734 if let Some(p) = &path {
1735 bindings.insert("path".to_string(), p.clone());
1736 }
1737 let raw = self.messages_or_inherited(shape, inherited);
1738 let messages = substitute_messages(&raw, focus, &bindings);
1739 out.push(ValidationResult {
1740 focus: focus.clone(),
1741 path,
1742 value,
1743 component: component.into_owned(),
1744 source_shape: node_term_ref(shape),
1745 severity: self.severity(shape),
1746 messages,
1747 });
1748 }
1749
1750 fn messages(&self, shape: &NamedOrBlankNode) -> Vec<Term> {
1752 self.shapes.objects(shape, vocab::SH_MESSAGE)
1753 }
1754
1755 fn default_message(&self, result: &ValidationResult) -> String {
1761 let comp = result.component.as_ref();
1762 let value = result.value.as_ref().map(render_term);
1763 let path = result.path.as_ref().map(render_term);
1764 let Some(shape) = term_to_node(&result.source_shape) else {
1765 return format!(
1766 "Value does not conform to constraint component <{}>",
1767 comp.as_str()
1768 );
1769 };
1770 let param = |p: NamedNodeRef| self.shapes.object(&shape, p).as_ref().map(render_term);
1772 let int_param = |p: NamedNodeRef| self.int(&shape, p).map(|n| n.to_string());
1773 let some_or = |o: Option<String>, default: &str| o.unwrap_or_else(|| default.to_string());
1774 let val = || value.clone().unwrap_or_else(|| "the value".to_string());
1775 let on_path = || {
1776 path.as_ref()
1777 .map(|p| format!(" on path {p}"))
1778 .unwrap_or_default()
1779 };
1780
1781 if comp == vocab::SH_CC_MIN_COUNT {
1782 format!(
1783 "Fewer than {} values{}",
1784 some_or(int_param(vocab::SH_MIN_COUNT), "the required number of"),
1785 on_path()
1786 )
1787 } else if comp == vocab::SH_CC_MAX_COUNT {
1788 format!(
1789 "More than {} values{}",
1790 some_or(int_param(vocab::SH_MAX_COUNT), "the allowed number of"),
1791 on_path()
1792 )
1793 } else if comp == vocab::SH_CC_CLASS {
1794 format!(
1795 "Value {} is not an instance of class {}",
1796 val(),
1797 some_or(param(vocab::SH_CLASS), "the required class")
1798 )
1799 } else if comp == vocab::SH_CC_DATATYPE {
1800 format!(
1801 "Value {} does not have datatype {}",
1802 val(),
1803 some_or(param(vocab::SH_DATATYPE), "the required datatype")
1804 )
1805 } else if comp == vocab::SH_CC_NODE_KIND {
1806 format!(
1807 "Value {} does not have node kind {}",
1808 val(),
1809 some_or(param(vocab::SH_NODE_KIND), "the required node kind")
1810 )
1811 } else if comp == vocab::SH_CC_MIN_INCLUSIVE {
1812 format!(
1813 "Value {} is less than minimum {}",
1814 val(),
1815 some_or(param(vocab::SH_MIN_INCLUSIVE), "the minimum")
1816 )
1817 } else if comp == vocab::SH_CC_MIN_EXCLUSIVE {
1818 format!(
1819 "Value {} is not greater than exclusive minimum {}",
1820 val(),
1821 some_or(param(vocab::SH_MIN_EXCLUSIVE), "the minimum")
1822 )
1823 } else if comp == vocab::SH_CC_MAX_INCLUSIVE {
1824 format!(
1825 "Value {} is greater than maximum {}",
1826 val(),
1827 some_or(param(vocab::SH_MAX_INCLUSIVE), "the maximum")
1828 )
1829 } else if comp == vocab::SH_CC_MAX_EXCLUSIVE {
1830 format!(
1831 "Value {} is not less than exclusive maximum {}",
1832 val(),
1833 some_or(param(vocab::SH_MAX_EXCLUSIVE), "the maximum")
1834 )
1835 } else if comp == vocab::SH_CC_MIN_LENGTH {
1836 format!(
1837 "Value {} is shorter than {} characters",
1838 val(),
1839 some_or(int_param(vocab::SH_MIN_LENGTH), "the minimum number of")
1840 )
1841 } else if comp == vocab::SH_CC_MAX_LENGTH {
1842 format!(
1843 "Value {} is longer than {} characters",
1844 val(),
1845 some_or(int_param(vocab::SH_MAX_LENGTH), "the maximum number of")
1846 )
1847 } else if comp == vocab::SH_CC_PATTERN {
1848 format!(
1849 "Value {} does not match pattern \"{}\"",
1850 val(),
1851 some_or(param(vocab::SH_PATTERN), "the required pattern")
1852 )
1853 } else if comp == vocab::SH_CC_IN {
1854 format!("Value {} is not in the list of allowed values", val())
1855 } else if comp == vocab::SH_CC_LANGUAGE_IN {
1856 format!("Value {} has a language tag that is not allowed", val())
1857 } else if comp == vocab::SH_CC_HAS_VALUE {
1858 format!(
1859 "Missing required value {}{}",
1860 some_or(param(vocab::SH_HAS_VALUE), "the expected value"),
1861 on_path()
1862 )
1863 } else if comp == vocab::SH_CC_UNIQUE_LANG {
1864 format!("Values{} do not have unique language tags", on_path())
1865 } else if comp == vocab::SH_CC_EQUALS {
1866 format!(
1867 "Value {} must equal the values of {}",
1868 val(),
1869 some_or(param(vocab::SH_EQUALS), "the compared property")
1870 )
1871 } else if comp == vocab::SH_CC_DISJOINT {
1872 format!(
1873 "Value {} must be disjoint from the values of {}",
1874 val(),
1875 some_or(param(vocab::SH_DISJOINT), "the compared property")
1876 )
1877 } else if comp == vocab::SH_CC_LESS_THAN {
1878 format!(
1879 "Value {} is not less than the values of {}",
1880 val(),
1881 some_or(param(vocab::SH_LESS_THAN), "the compared property")
1882 )
1883 } else if comp == vocab::SH_CC_LESS_THAN_OR_EQUALS {
1884 format!(
1885 "Value {} is not less than or equal to the values of {}",
1886 val(),
1887 some_or(
1888 param(vocab::SH_LESS_THAN_OR_EQUALS),
1889 "the compared property"
1890 )
1891 )
1892 } else if comp == vocab::SH_CC_AND {
1893 format!(
1894 "Value {} does not conform to all of the given shapes",
1895 val()
1896 )
1897 } else if comp == vocab::SH_CC_OR {
1898 format!(
1899 "Value {} does not conform to any of the given shapes",
1900 val()
1901 )
1902 } else if comp == vocab::SH_CC_XONE {
1903 format!(
1904 "Value {} does not conform to exactly one of the given shapes",
1905 val()
1906 )
1907 } else if comp == vocab::SH_CC_NOT {
1908 format!("Value {} conforms to a shape it must not", val())
1909 } else if comp == vocab::SH_CC_NODE {
1910 format!(
1911 "Value {} does not conform to shape {}",
1912 val(),
1913 some_or(param(vocab::SH_NODE), "the required shape")
1914 )
1915 } else if comp == vocab::SH_CC_CLOSED {
1916 format!(
1917 "Predicate{} is not allowed on {} (closed shape)",
1918 path.as_ref().map(|p| format!(" {p}")).unwrap_or_default(),
1919 val()
1920 )
1921 } else if comp == vocab::SH_CC_QUALIFIED_MIN_COUNT {
1922 format!(
1923 "Fewer than {} values{} conform to the qualified shape",
1924 some_or(
1925 int_param(vocab::SH_QUALIFIED_MIN_COUNT),
1926 "the required number of"
1927 ),
1928 on_path()
1929 )
1930 } else if comp == vocab::SH_CC_QUALIFIED_MAX_COUNT {
1931 format!(
1932 "More than {} values{} conform to the qualified shape",
1933 some_or(
1934 int_param(vocab::SH_QUALIFIED_MAX_COUNT),
1935 "the allowed number of"
1936 ),
1937 on_path()
1938 )
1939 } else if comp == vocab::SH_CC_EXPRESSION {
1940 format!("Expression constraint not satisfied for value {}", val())
1941 } else if comp == vocab::SH_CC_SPARQL {
1942 "SPARQL constraint not satisfied".to_string()
1943 } else {
1944 format!(
1945 "Value does not conform to constraint component <{}>",
1946 comp.as_str()
1947 )
1948 }
1949 }
1950
1951 fn conforms(&self, shape: &NamedOrBlankNode, focus: &Term, visited: &mut Visited) -> bool {
1952 let mut scratch = Vec::new();
1953 self.collect(shape, focus, &mut scratch, visited, &[]);
1954 scratch.is_empty()
1955 }
1956
1957 fn value_checks(
1960 &self,
1961 shape: &NamedOrBlankNode,
1962 u: &Term,
1963 visited: &mut Visited,
1964 ) -> Vec<(NamedNode, bool)> {
1965 let mut checks = Vec::new();
1966
1967 for c in self.shapes.objects(shape, vocab::SH_CLASS) {
1968 checks.push((vocab::SH_CC_CLASS.into_owned(), self.is_instance(u, &c)));
1969 }
1970 for d in self.shapes.objects(shape, vocab::SH_DATATYPE) {
1971 if let Term::NamedNode(dt) = d {
1972 let ok = value_type_holds(&ValueType::Datatype(dt), u);
1973 checks.push((vocab::SH_CC_DATATYPE.into_owned(), ok));
1974 }
1975 }
1976 for k in self.shapes.objects(shape, vocab::SH_NODE_KIND) {
1977 if let Some(set) = map_node_kind(&k) {
1978 checks.push((vocab::SH_CC_NODE_KIND.into_owned(), set.matches(u)));
1979 }
1980 }
1981 for (pred_iri, comp, inclusive) in [
1983 (vocab::SH_MIN_INCLUSIVE, vocab::SH_CC_MIN_INCLUSIVE, true),
1984 (vocab::SH_MIN_EXCLUSIVE, vocab::SH_CC_MIN_EXCLUSIVE, false),
1985 ] {
1986 if let Some(Term::Literal(b)) = self.shapes.object(shape, pred_iri) {
1987 let vt = ValueType::NumericRange {
1988 lo: Some(Bound {
1989 value: b,
1990 inclusive,
1991 }),
1992 hi: None,
1993 };
1994 checks.push((comp.into_owned(), value_type_holds(&vt, u)));
1995 }
1996 }
1997 for (pred_iri, comp, inclusive) in [
1998 (vocab::SH_MAX_INCLUSIVE, vocab::SH_CC_MAX_INCLUSIVE, true),
1999 (vocab::SH_MAX_EXCLUSIVE, vocab::SH_CC_MAX_EXCLUSIVE, false),
2000 ] {
2001 if let Some(Term::Literal(b)) = self.shapes.object(shape, pred_iri) {
2002 let vt = ValueType::NumericRange {
2003 lo: None,
2004 hi: Some(Bound {
2005 value: b,
2006 inclusive,
2007 }),
2008 };
2009 checks.push((comp.into_owned(), value_type_holds(&vt, u)));
2010 }
2011 }
2012 let min_len = self.int(shape, vocab::SH_MIN_LENGTH);
2014 let max_len = self.int(shape, vocab::SH_MAX_LENGTH);
2015 if let Some(m) = min_len {
2016 let vt = ValueType::Length {
2017 min: Some(m),
2018 max: None,
2019 };
2020 checks.push((
2021 vocab::SH_CC_MIN_LENGTH.into_owned(),
2022 value_type_holds(&vt, u),
2023 ));
2024 }
2025 if let Some(m) = max_len {
2026 let vt = ValueType::Length {
2027 min: None,
2028 max: Some(m),
2029 };
2030 checks.push((
2031 vocab::SH_CC_MAX_LENGTH.into_owned(),
2032 value_type_holds(&vt, u),
2033 ));
2034 }
2035 if let Some(Term::Literal(re)) = self.shapes.object(shape, vocab::SH_PATTERN) {
2036 let flags = match self.shapes.object(shape, vocab::SH_FLAGS) {
2037 Some(Term::Literal(f)) => f.value().to_string(),
2038 _ => String::new(),
2039 };
2040 let vt = ValueType::Pattern {
2041 regex: re.value().to_string(),
2042 flags,
2043 };
2044 checks.push((vocab::SH_CC_PATTERN.into_owned(), value_type_holds(&vt, u)));
2045 }
2046 for list in self.shapes.objects(shape, vocab::SH_IN) {
2048 let members = self.shapes.read_list(&list);
2049 checks.push((vocab::SH_CC_IN.into_owned(), members.contains(u)));
2050 }
2051 for list in self.shapes.objects(shape, vocab::SH_LANGUAGE_IN) {
2052 let languages = self
2053 .shapes
2054 .read_list(&list)
2055 .into_iter()
2056 .filter_map(|term| match term {
2057 Term::Literal(literal) => Some(literal.value().to_string()),
2058 _ => None,
2059 })
2060 .collect();
2061 checks.push((
2062 vocab::SH_CC_LANGUAGE_IN.into_owned(),
2063 value_type_holds(&ValueType::LangIn(languages), u),
2064 ));
2065 }
2066
2067 for list in self.shapes.objects(shape, vocab::SH_AND) {
2069 let ok = self
2070 .shapes
2071 .read_list(&list)
2072 .iter()
2073 .filter_map(term_to_node)
2074 .all(|m| self.conforms(&m, u, visited));
2075 checks.push((vocab::SH_CC_AND.into_owned(), ok));
2076 }
2077 for list in self.shapes.objects(shape, vocab::SH_OR) {
2078 let ok = self
2079 .shapes
2080 .read_list(&list)
2081 .iter()
2082 .filter_map(term_to_node)
2083 .any(|m| self.conforms(&m, u, visited));
2084 checks.push((vocab::SH_CC_OR.into_owned(), ok));
2085 }
2086 for list in self.shapes.objects(shape, vocab::SH_XONE) {
2087 let count = self
2088 .shapes
2089 .read_list(&list)
2090 .iter()
2091 .filter_map(term_to_node)
2092 .filter(|m| self.conforms(m, u, visited))
2093 .count();
2094 checks.push((vocab::SH_CC_XONE.into_owned(), count == 1));
2095 }
2096 for n in self.shapes.objects(shape, vocab::SH_NOT) {
2097 if let Some(nn) = term_to_node(&n) {
2098 checks.push((
2099 vocab::SH_CC_NOT.into_owned(),
2100 !self.conforms(&nn, u, visited),
2101 ));
2102 }
2103 }
2104 for n in self.shapes.objects(shape, vocab::SH_NODE) {
2105 if let Some(nn) = term_to_node(&n) {
2106 checks.push((
2107 vocab::SH_CC_NODE.into_owned(),
2108 self.conforms(&nn, u, visited),
2109 ));
2110 }
2111 }
2112
2113 checks
2114 }
2115
2116 fn is_instance(&self, u: &Term, class: &Term) -> bool {
2117 succ(self.frozen(), u, &class_path()).contains(class)
2118 }
2119
2120 fn int(&self, s: &NamedOrBlankNode, p: NamedNodeRef) -> Option<u64> {
2121 match self.shapes.object(s, p) {
2122 Some(Term::Literal(l)) => l.value().parse().ok(),
2123 _ => None,
2124 }
2125 }
2126
2127 fn bool(&self, s: &NamedOrBlankNode, p: NamedNodeRef) -> bool {
2128 matches!(
2129 self.shapes.object(s, p),
2130 Some(Term::Literal(ref literal)) if matches!(literal.value(), "true" | "1")
2131 )
2132 }
2133
2134 fn severity(&self, shape: &NamedOrBlankNode) -> NamedNode {
2137 match self.shapes.object(shape, vocab::SH_SEVERITY) {
2138 Some(Term::NamedNode(n)) => n,
2139 _ => vocab::SH_VIOLATION.into_owned(),
2140 }
2141 }
2142}
2143
2144fn is_shape_node(shapes: &Loaded, node: &NamedOrBlankNode) -> bool {
2145 shapes.has_type(node, vocab::SH_NODE_SHAPE)
2146 || shapes.has_type(node, vocab::SH_PROPERTY_SHAPE)
2147 || [
2148 vocab::SH_PROPERTY,
2149 vocab::SH_NODE,
2150 vocab::SH_AND,
2151 vocab::SH_OR,
2152 vocab::SH_NOT,
2153 vocab::SH_XONE,
2154 vocab::SH_DATATYPE,
2155 vocab::SH_CLASS,
2156 vocab::SH_NODE_KIND,
2157 vocab::SH_IN,
2158 vocab::SH_HAS_VALUE,
2159 vocab::SH_PROPERTY,
2160 ]
2161 .iter()
2162 .any(|predicate| shapes.object(node, *predicate).is_some())
2163}
2164
2165fn shapes_reference_shapes_graph(shapes: &Loaded) -> bool {
2168 [vocab::SH_SELECT, vocab::SH_ASK].iter().any(|predicate| {
2169 shapes.graph.triples_for_predicate(*predicate).any(
2170 |t| matches!(t.object, oxrdf::TermRef::Literal(l) if l.value().contains("shapesGraph")),
2171 )
2172 })
2173}
2174
2175fn class_path() -> Path {
2176 Path::seq(vec![
2177 Path::Pred(vocab::rdf_type()),
2178 Path::star(Path::Pred(vocab::rdfs_subclassof())),
2179 ])
2180}
2181
2182fn build_class_index(
2191 focus_data: &Graph,
2192 frozen: &FrozenIndexedDataset,
2193) -> HashMap<Term, Vec<Term>> {
2194 let focus_nodes = graph_nodes(focus_data);
2195 let subclass_star = Path::star(Path::Pred(vocab::rdfs_subclassof()));
2196 let mut supers: HashMap<Term, Vec<Term>> = HashMap::new();
2197 let mut index: HashMap<Term, Vec<Term>> = HashMap::new();
2198 let mut seen: HashSet<(Term, Term)> = HashSet::new();
2199 for (node, ty) in frozen.triples_for_predicate(&vocab::rdf_type()) {
2200 if !focus_nodes.contains(&node) {
2201 continue;
2202 }
2203 let classes = supers
2204 .entry(ty.clone())
2205 .or_insert_with(|| succ(frozen, &ty, &subclass_star).into_iter().collect());
2206 for class in classes.iter() {
2207 if seen.insert((class.clone(), node.clone())) {
2208 index.entry(class.clone()).or_default().push(node.clone());
2209 }
2210 }
2211 }
2212 index
2213}
2214
2215fn graph_nodes(graph: &Graph) -> HashSet<Term> {
2216 let mut nodes = HashSet::new();
2217 for triple in graph.iter() {
2218 nodes.insert(node_term(triple.subject));
2219 nodes.insert(triple.object.into_owned());
2220 }
2221 nodes
2222}
2223
2224fn node_term(s: oxrdf::NamedOrBlankNodeRef) -> Term {
2225 crate::path::term_of(s.into_owned())
2226}
2227
2228fn render_term(t: &Term) -> String {
2232 match t {
2233 Term::NamedNode(n) => format!("<{}>", n.as_str()),
2234 Term::BlankNode(b) => format!("_:{}", b.as_str()),
2235 Term::Literal(l) => l.value().to_string(),
2236 }
2237}
2238
2239fn node_term_ref(s: &NamedOrBlankNode) -> Term {
2240 match s {
2241 NamedOrBlankNode::NamedNode(n) => Term::NamedNode(n.clone()),
2242 NamedOrBlankNode::BlankNode(b) => Term::BlankNode(b.clone()),
2243 }
2244}
2245
2246fn map_node_kind(term: &Term) -> Option<NodeKindSet> {
2247 let Term::NamedNode(n) = term else {
2248 return None;
2249 };
2250 let r = n.as_ref();
2251 Some(if r == vocab::SH_IRI {
2252 NodeKindSet::IRI
2253 } else if r == vocab::SH_BLANK_NODE {
2254 NodeKindSet::BLANK_NODE
2255 } else if r == vocab::SH_LITERAL {
2256 NodeKindSet::LITERAL
2257 } else if r == vocab::SH_BLANK_NODE_OR_IRI {
2258 NodeKindSet::BLANK_NODE_OR_IRI
2259 } else if r == vocab::SH_BLANK_NODE_OR_LITERAL {
2260 NodeKindSet::BLANK_NODE_OR_LITERAL
2261 } else if r == vocab::SH_IRI_OR_LITERAL {
2262 NodeKindSet::IRI_OR_LITERAL
2263 } else {
2264 return None;
2265 })
2266}