1use crate::frozen::FrozenIndexedDataset;
16use crate::path::succ;
17use crate::sparql::{FunctionDef, SparqlExecutor};
18use crate::validate::{
19 UnsupportedPolicy, ValidationGraphMode, ValidationOptions, apply_message_template, graph_union,
20 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
56pub fn validate_report(shapes: &Loaded, data: &Graph) -> ValidationReport {
58 validate_report_with_options(shapes, data, &ValidationOptions::default())
59}
60
61pub fn evaluate_function_expression(shapes: &Loaded, expr: &str) -> Result<Option<Term>, String> {
67 let frozen = FrozenIndexedDataset::from_graph(&shapes.graph);
68 let mut sparql = SparqlExecutor::from_frozen(frozen, false);
69 sparql.set_functions(collect_functions(shapes), UnsupportedPolicy::Ignore);
72 let mut prologue = String::new();
73 if let Some(base) = &shapes.base {
74 prologue.push_str(&format!("BASE <{base}>\n"));
75 }
76 for (prefix, namespace) in &shapes.prefixes {
77 prologue.push_str(&format!("PREFIX {prefix}: <{namespace}>\n"));
78 }
79 sparql.evaluate_expression(&prologue, expr)
80}
81
82pub fn validate_report_with_options(
84 shapes: &Loaded,
85 data: &Graph,
86 options: &ValidationOptions,
87) -> ValidationReport {
88 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
89 let frozen = if has_shapes_graph {
90 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
91 } else {
92 FrozenIndexedDataset::from_graph(data)
93 };
94 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
95}
96
97pub fn validate_report_graphs(shapes: &Loaded, data: &Graph) -> ValidationReport {
99 validate_report_graphs_with_mode_and_options(
100 shapes,
101 data,
102 ValidationGraphMode::default(),
103 &ValidationOptions::default(),
104 )
105}
106
107pub fn validate_report_graphs_with_mode(
109 shapes: &Loaded,
110 data: &Graph,
111 mode: ValidationGraphMode,
112) -> ValidationReport {
113 validate_report_graphs_with_mode_and_options(shapes, data, mode, &ValidationOptions::default())
114}
115
116pub fn validate_report_graphs_with_mode_and_options(
118 shapes: &Loaded,
119 data: &Graph,
120 mode: ValidationGraphMode,
121 options: &ValidationOptions,
122) -> ValidationReport {
123 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
124 match mode {
125 ValidationGraphMode::Data => {
126 let frozen = if has_shapes_graph {
127 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
128 } else {
129 FrozenIndexedDataset::from_graph(data)
130 };
131 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
132 }
133 ValidationGraphMode::Union => {
134 let frozen = if has_shapes_graph {
135 FrozenIndexedDataset::from_graph_union_with_shapes(data, &shapes.graph)
136 } else {
137 FrozenIndexedDataset::from_graph_union(data, &shapes.graph)
138 };
139 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
140 }
141 ValidationGraphMode::UnionAll => {
142 let union = graph_union(data, &shapes.graph);
143 let frozen = if has_shapes_graph {
144 FrozenIndexedDataset::from_graphs(&union, &shapes.graph)
145 } else {
146 FrozenIndexedDataset::from_graph(&union)
147 };
148 validate_report_context(shapes, &union, frozen, has_shapes_graph, options)
149 }
150 }
151}
152
153fn validate_report_context(
154 shapes: &Loaded,
155 focus_data: &Graph,
156 frozen: FrozenIndexedDataset,
157 has_shapes_graph: bool,
158 options: &ValidationOptions,
159) -> ValidationReport {
160 let needs_sparql = shapes
163 .graph
164 .triples_for_predicate(vocab::SH_SPARQL)
165 .next()
166 .is_some()
167 || shapes
168 .graph
169 .triples_for_predicate(vocab::SH_TARGET)
170 .next()
171 .is_some();
172 let mut sparql = SparqlExecutor::from_frozen(frozen, needs_sparql && has_shapes_graph);
173 sparql.set_functions(collect_functions(shapes), options.engine.unsupported);
174 let has_explicit_class_target = shapes
178 .graph
179 .triples_for_predicate(vocab::SH_TARGET_CLASS)
180 .next()
181 .is_some();
182 let has_implicit_class_target = shapes.graph.iter().any(|triple| {
183 let subject = triple.subject.into_owned();
184 is_shape_node(shapes, &subject)
185 && (shapes.is_instance_of(&subject, vocab::RDFS_CLASS)
186 || shapes.is_instance_of(&subject, vocab::OWL_CLASS))
187 });
188 let needs_class_index = has_explicit_class_target || has_implicit_class_target;
189 let class_index = if needs_class_index {
190 build_class_index(
191 focus_data,
192 sparql
193 .frozen()
194 .expect("report validation always has a frozen dataset"),
195 )
196 } else {
197 HashMap::new()
198 };
199 let r = Reporter {
200 shapes,
201 focus_data,
202 sparql,
203 needs_sparql,
204 class_index,
205 path_cache: RefCell::new(HashMap::new()),
206 components: build_components(shapes, options.engine.unsupported),
207 };
208 let mut results = Vec::new();
209 for shape in r.target_shapes() {
210 let foci = r.focus_nodes(&shape);
211 r.prefetch_sparql(&shape, &foci);
212 for focus in &foci {
213 let mut visited = HashSet::new();
214 r.collect(&shape, focus, &mut results, &mut visited);
215 }
216 }
217 if options.sort_results {
218 results.sort_by(|left, right| {
219 Severity::from_named_node(right.severity.clone())
220 .rank()
221 .cmp(&Severity::from_named_node(left.severity.clone()).rank())
222 .then_with(|| left.focus.to_string().cmp(&right.focus.to_string()))
223 .then_with(|| {
224 left.source_shape
225 .to_string()
226 .cmp(&right.source_shape.to_string())
227 })
228 .then_with(|| left.component.as_str().cmp(right.component.as_str()))
229 });
230 }
231 ValidationReport {
232 conforms: !results.iter().any(|result| {
233 Severity::from_named_node(result.severity.clone()).meets(&options.minimum_severity)
234 }),
235 results,
236 }
237}
238
239pub fn report_to_graph(report: &ValidationReport) -> Graph {
241 let mut g = Graph::new();
242 let root = BlankNode::default();
243 let t = |s: NamedOrBlankNode, p: NamedNodeRef, o: Term| Triple::new(s, p.into_owned(), o);
244
245 g.insert(&t(
246 root.clone().into(),
247 vocab::RDF_TYPE,
248 vocab::SH_VALIDATION_REPORT.into_owned().into(),
249 ));
250 g.insert(&t(
251 root.clone().into(),
252 vocab::SH_CONFORMS,
253 Literal::from(report.conforms).into(),
254 ));
255
256 for r in &report.results {
257 let rn = BlankNode::default();
258 g.insert(&t(root.clone().into(), vocab::SH_RESULT, rn.clone().into()));
259 g.insert(&t(
260 rn.clone().into(),
261 vocab::RDF_TYPE,
262 vocab::SH_VALIDATION_RESULT.into_owned().into(),
263 ));
264 g.insert(&t(rn.clone().into(), vocab::SH_FOCUS_NODE, r.focus.clone()));
265 if let Some(path) = &r.path {
266 g.insert(&t(rn.clone().into(), vocab::SH_RESULT_PATH, path.clone()));
267 }
268 if let Some(value) = &r.value {
269 g.insert(&t(rn.clone().into(), vocab::SH_VALUE, value.clone()));
270 }
271 g.insert(&t(
272 rn.clone().into(),
273 vocab::SH_RESULT_SEVERITY,
274 r.severity.clone().into(),
275 ));
276 g.insert(&t(
277 rn.clone().into(),
278 vocab::SH_SOURCE_CONSTRAINT_COMPONENT,
279 r.component.clone().into(),
280 ));
281 for msg in &r.messages {
282 g.insert(&t(rn.clone().into(), vocab::SH_RESULT_MESSAGE, msg.clone()));
283 }
284 g.insert(&t(
285 rn.into(),
286 vocab::SH_SOURCE_SHAPE,
287 r.source_shape.clone(),
288 ));
289 }
290 g
291}
292
293fn substitute_messages(
300 messages: &[Term],
301 focus: &Term,
302 bindings: &HashMap<String, Term>,
303) -> Vec<Term> {
304 messages
305 .iter()
306 .map(|msg| {
307 let Term::Literal(lit) = msg else {
308 return msg.clone();
309 };
310 let text = lit.value();
311 let substituted = apply_message_template(text, focus, bindings);
312 if substituted == text {
313 msg.clone()
314 } else {
315 Term::Literal(Literal::new_simple_literal(&substituted))
316 }
317 })
318 .collect()
319}
320
321struct CustomComponent {
325 iri: NamedNode,
327 params: Vec<ComponentParam>,
328 node_validator: Option<ComponentValidator>,
330 property_validator: Option<ComponentValidator>,
332 generic_validator: Option<ComponentValidator>,
334}
335
336struct ComponentParam {
337 path: NamedNode,
339 var: String,
341 optional: bool,
342}
343
344struct ComponentValidator {
345 kind: SparqlQueryKind,
346 query: String,
348 messages: Vec<Term>,
349}
350
351fn resolve_validator(
352 shapes: &Loaded,
353 node: Term,
354 component_iri: &NamedNode,
355 policy: UnsupportedPolicy,
356) -> Option<ComponentValidator> {
357 match parse_validator(shapes, &node) {
358 Ok(v) => Some(v),
359 Err(e) => {
360 assert!(
361 policy != UnsupportedPolicy::Error,
362 "invalid SPARQL in custom constraint component <{component_iri}>: {e}"
363 );
364 None
365 }
366 }
367}
368
369fn build_components(shapes: &Loaded, policy: UnsupportedPolicy) -> Vec<CustomComponent> {
380 let mut out = Vec::new();
381 let mut seen = HashSet::new();
382 for triple in shapes.graph.triples_for_predicate(vocab::SH_PARAMETER) {
383 let subject = triple.subject.into_owned();
384 if !seen.insert(subject.clone()) {
385 continue;
386 }
387 let NamedOrBlankNode::NamedNode(iri) = &subject else {
388 continue; };
390
391 let node_validator = shapes
392 .object(&subject, vocab::SH_NODE_VALIDATOR)
393 .and_then(|v| resolve_validator(shapes, v, iri, policy));
394 let property_validator = shapes
395 .object(&subject, vocab::SH_PROPERTY_VALIDATOR)
396 .and_then(|v| resolve_validator(shapes, v, iri, policy));
397 let generic_validator = shapes
398 .object(&subject, vocab::SH_VALIDATOR)
399 .and_then(|v| resolve_validator(shapes, v, iri, policy));
400 if node_validator.is_none() && property_validator.is_none() && generic_validator.is_none() {
401 continue; }
403 let mut params = Vec::new();
404 for p in shapes.objects(&subject, vocab::SH_PARAMETER) {
405 let Some(pn) = term_to_node(&p) else { continue };
406 let Some(Term::NamedNode(path)) = shapes.object(&pn, vocab::SH_PATH) else {
407 continue;
408 };
409 let var = local_name(path.as_str()).to_string();
410 let optional = matches!(
411 shapes.object(&pn, vocab::SH_OPTIONAL),
412 Some(Term::Literal(ref l)) if l.value() == "true"
413 );
414 params.push(ComponentParam {
415 path,
416 var,
417 optional,
418 });
419 }
420 if params.is_empty() {
421 continue;
422 }
423 out.push(CustomComponent {
424 iri: iri.clone(),
425 params,
426 node_validator,
427 property_validator,
428 generic_validator,
429 });
430 }
431 out.sort_by(|a, b| a.iri.as_str().cmp(b.iri.as_str()));
432 out
433}
434
435fn parse_validator(shapes: &Loaded, node: &Term) -> Result<ComponentValidator, String> {
439 let node = term_to_node(node)
440 .ok_or_else(|| "validator node is not an IRI or blank node".to_string())?;
441 let (kind, raw) = if let Some(Term::Literal(q)) = shapes.object(&node, vocab::SH_ASK) {
442 (SparqlQueryKind::Ask, q.value().to_string())
443 } else if let Some(Term::Literal(q)) = shapes.object(&node, vocab::SH_SELECT) {
444 (SparqlQueryKind::Select, q.value().to_string())
445 } else {
446 return Err("validator has neither sh:ask nor sh:select".to_string());
447 };
448 let (_, query) = canonical_sparql_query(shapes, &node, &raw)
449 .map_err(|e| format!("invalid SPARQL in {node}: {e}"))?;
450 let messages = shapes.objects(&node, vocab::SH_MESSAGE);
451 Ok(ComponentValidator {
452 kind,
453 query,
454 messages,
455 })
456}
457
458fn local_name(iri: &str) -> &str {
461 iri.rsplit(['#', '/']).next().unwrap_or(iri)
462}
463
464pub(crate) fn collect_functions(shapes: &Loaded) -> Vec<FunctionDef> {
469 let mut out = Vec::new();
470 for func in shapes
471 .graph
472 .subjects_for_predicate_object(vocab::RDF_TYPE, vocab::SH_SPARQL_FUNCTION)
473 .map(|s| s.into_owned())
474 .collect::<Vec<_>>()
475 {
476 let NamedOrBlankNode::NamedNode(iri) = &func else {
477 continue;
478 };
479 let raw = match shapes
480 .object(&func, vocab::SH_SELECT)
481 .or_else(|| shapes.object(&func, vocab::SH_ASK))
482 {
483 Some(Term::Literal(q)) => q.value().to_string(),
484 _ => continue,
485 };
486 let Ok((_, query)) = canonical_sparql_query(shapes, &func, &raw) else {
487 continue;
488 };
489 out.push(FunctionDef {
490 iri: iri.clone(),
491 params: function_param_names(shapes, &func),
492 reads_graph: crate::sparql::query_reads_graph(&query),
493 query,
494 });
495 }
496 out
497}
498
499fn function_param_names(shapes: &Loaded, func: &NamedOrBlankNode) -> Vec<String> {
502 let mut params: Vec<(i64, String)> = shapes
503 .objects(func, vocab::SH_PARAMETER)
504 .iter()
505 .filter_map(|p| {
506 let pn = term_to_node(p)?;
507 let order = match shapes.object(&pn, vocab::SH_ORDER) {
508 Some(Term::Literal(l)) => l.value().parse::<i64>().unwrap_or(0),
509 _ => 0,
510 };
511 let name = match shapes.object(&pn, vocab::SH_NAME) {
512 Some(Term::Literal(l)) => l.value().to_string(),
513 _ => match shapes.object(&pn, vocab::SH_PATH) {
514 Some(Term::NamedNode(n)) => local_name(n.as_str()).to_string(),
515 _ => return None,
516 },
517 };
518 Some((order, name))
519 })
520 .collect();
521 params.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
522 params.into_iter().map(|(_, name)| name).collect()
523}
524
525struct Reporter<'a> {
526 shapes: &'a Loaded,
527 focus_data: &'a Graph,
528 sparql: SparqlExecutor,
529 needs_sparql: bool,
530 class_index: HashMap<Term, Vec<Term>>,
533 path_cache: RefCell<HashMap<NamedOrBlankNode, PathCacheEntry>>,
536 components: Vec<CustomComponent>,
539}
540
541type Visited = HashSet<(NamedOrBlankNode, Term)>;
542
543type PathCacheEntry = (Option<Term>, Option<Path>);
545
546impl Reporter<'_> {
547 fn frozen(&self) -> &FrozenIndexedDataset {
548 self.sparql
549 .frozen()
550 .expect("report validation always has a frozen dataset")
551 }
552
553 fn target_shapes(&self) -> Vec<NamedOrBlankNode> {
554 let mut found: HashSet<NamedOrBlankNode> = HashSet::new();
555 for t in self.shapes.graph.iter() {
556 let p = t.predicate;
557 if p == vocab::SH_TARGET_NODE
558 || p == vocab::SH_TARGET_CLASS
559 || p == vocab::SH_TARGET_SUBJECTS_OF
560 || p == vocab::SH_TARGET_OBJECTS_OF
561 {
562 found.insert(t.subject.into_owned());
563 }
564 if p == vocab::SH_TARGET
566 && let Some(target) = term_to_node(&t.object.into_owned())
567 && self.shapes.object(&target, vocab::SH_SELECT).is_some()
568 {
569 found.insert(t.subject.into_owned());
570 }
571 if p == vocab::RDF_TYPE {
573 let s = t.subject.into_owned();
574 if self.is_class(&s) && self.is_shape(&s) {
575 found.insert(s);
576 }
577 }
578 }
579 let mut v: Vec<_> = found.into_iter().collect();
580 v.sort_by_key(|n| n.to_string());
581 v
582 }
583
584 fn is_shape(&self, n: &NamedOrBlankNode) -> bool {
586 is_shape_node(self.shapes, n)
587 }
588
589 fn is_class(&self, n: &NamedOrBlankNode) -> bool {
590 self.shapes.is_instance_of(n, vocab::RDFS_CLASS)
591 || self.shapes.is_instance_of(n, vocab::OWL_CLASS)
592 }
593
594 fn deactivated(&self, n: &NamedOrBlankNode) -> bool {
595 matches!(self.shapes.object(n, vocab::SH_DEACTIVATED),
596 Some(Term::Literal(ref l)) if l.value() == "true")
597 }
598
599 fn focus_nodes(&self, shape: &NamedOrBlankNode) -> Vec<Term> {
600 let mut nodes = Vec::new();
601 nodes.extend(self.shapes.objects(shape, vocab::SH_TARGET_NODE));
602 for c in self.shapes.objects(shape, vocab::SH_TARGET_CLASS) {
603 if let Some(instances) = self.class_index.get(&c) {
604 nodes.extend(instances.iter().cloned());
605 }
606 }
607 for p in self.shapes.objects(shape, vocab::SH_TARGET_SUBJECTS_OF) {
608 if let Term::NamedNode(n) = p {
609 nodes.extend(
610 self.focus_data
611 .triples_for_predicate(n.as_ref())
612 .map(|t| node_term(t.subject)),
613 );
614 }
615 }
616 for p in self.shapes.objects(shape, vocab::SH_TARGET_OBJECTS_OF) {
617 if let Term::NamedNode(n) = p {
618 nodes.extend(
619 self.focus_data
620 .triples_for_predicate(n.as_ref())
621 .map(|t| t.object.into_owned()),
622 );
623 }
624 }
625 if self.needs_sparql {
628 let exec = &self.sparql;
629 for target in self.shapes.objects(shape, vocab::SH_TARGET) {
630 let Some(target_node) = term_to_node(&target) else {
631 continue;
632 };
633 let Some(Term::Literal(query)) = self.shapes.object(&target_node, vocab::SH_SELECT)
634 else {
635 continue;
636 };
637 let Ok((_, canonical)) =
639 canonical_sparql_query(self.shapes, &target_node, query.value())
640 else {
641 continue;
642 };
643 if let Ok(found) = exec.target_nodes(&canonical) {
644 nodes.extend(found);
645 }
646 }
647 }
648 if let NamedOrBlankNode::NamedNode(n) = shape
650 && self.is_class(shape)
651 {
652 let class = Term::NamedNode(n.clone());
653 if let Some(instances) = self.class_index.get(&class) {
654 nodes.extend(instances.iter().cloned());
655 }
656 }
657 let mut seen = HashSet::new();
658 nodes.retain(|t| seen.insert(t.clone()));
659 nodes
660 }
661
662 fn shape_path(&self, shape: &NamedOrBlankNode) -> (Option<Term>, Option<Path>) {
665 if let Some(cached) = self.path_cache.borrow().get(shape) {
666 return cached.clone();
667 }
668 let path_term = self.shapes.object(shape, vocab::SH_PATH);
669 let parsed = path_term
670 .as_ref()
671 .and_then(|t| parse_path(self.shapes, t).ok());
672 let entry = (path_term, parsed);
673 self.path_cache
674 .borrow_mut()
675 .insert(shape.clone(), entry.clone());
676 entry
677 }
678
679 fn collect(
681 &self,
682 shape: &NamedOrBlankNode,
683 focus: &Term,
684 out: &mut Vec<ValidationResult>,
685 visited: &mut Visited,
686 ) {
687 if self.deactivated(shape) {
688 return; }
690 let key = (shape.clone(), focus.clone());
691 if !visited.insert(key.clone()) {
692 return; }
694
695 let (path_term, parsed) = self.shape_path(shape);
696 let value_nodes: Vec<Term> = match &parsed {
697 Some(p) => succ(self.frozen(), focus, p).into_iter().collect(),
698 None => vec![focus.clone()],
699 };
700 let severity = self.severity(shape);
701 let messages = self.messages(shape);
702 let push = |out: &mut Vec<ValidationResult>, value, component| {
703 out.push(ValidationResult {
704 focus: focus.clone(),
705 path: path_term.clone(),
706 value,
707 component,
708 source_shape: node_term_ref(shape),
709 severity: severity.clone(),
710 messages: messages.clone(),
711 });
712 };
713
714 if parsed.is_some() {
716 if let Some(min) = self.int(shape, vocab::SH_MIN_COUNT)
717 && (value_nodes.len() as u64) < min
718 {
719 push(out, None, vocab::SH_CC_MIN_COUNT.into_owned());
720 }
721 if let Some(max) = self.int(shape, vocab::SH_MAX_COUNT)
722 && (value_nodes.len() as u64) > max
723 {
724 push(out, None, vocab::SH_CC_MAX_COUNT.into_owned());
725 }
726 }
727
728 for hv in self.shapes.objects(shape, vocab::SH_HAS_VALUE) {
730 if !value_nodes.contains(&hv) {
731 push(out, None, vocab::SH_CC_HAS_VALUE.into_owned());
732 }
733 }
734
735 self.collect_closed(shape, focus, &value_nodes, out);
736 self.collect_property_pairs(shape, focus, &path_term, &value_nodes, out);
737 self.collect_unique_lang(shape, focus, &path_term, &value_nodes, out);
738 self.collect_qualified_counts(shape, focus, &path_term, &value_nodes, out, visited);
739
740 for u in &value_nodes {
742 for (component, ok) in self.value_checks(shape, u, visited) {
743 if !ok {
744 push(out, Some(u.clone()), component);
745 }
746 }
747 }
748
749 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
751 if let Some(pn) = term_to_node(&prop) {
752 for u in &value_nodes {
753 self.collect(&pn, u, out, visited);
754 }
755 }
756 }
757
758 self.collect_sparql(shape, focus, &path_term, &parsed, out);
759 self.collect_expression(shape, focus, out, visited);
760 self.collect_components(shape, focus, &path_term, &parsed, &value_nodes, out);
761
762 visited.remove(&key);
763 }
764
765 fn collect_components(
772 &self,
773 shape: &NamedOrBlankNode,
774 focus: &Term,
775 path_term: &Option<Term>,
776 parsed_path: &Option<Path>,
777 value_nodes: &[Term],
778 out: &mut Vec<ValidationResult>,
779 ) {
780 if self.components.is_empty() {
781 return;
782 }
783 let is_property_shape = parsed_path.is_some();
784 for component in &self.components {
785 let mut params: Vec<(String, Term)> = Vec::new();
787 let mut activated = true;
788 for p in &component.params {
789 if let Some(value) = self.shapes.object(shape, p.path.as_ref()) {
790 params.push((p.var.clone(), value));
791 } else if !p.optional {
792 activated = false;
793 break;
794 }
795 }
796 if !activated {
797 continue;
798 }
799
800 let validator = if is_property_shape {
801 component
802 .property_validator
803 .as_ref()
804 .or(component.generic_validator.as_ref())
805 } else {
806 component
807 .node_validator
808 .as_ref()
809 .or(component.generic_validator.as_ref())
810 };
811 let Some(validator) = validator else { continue };
812
813 let mut base = params;
817 base.push(("currentShape".to_string(), node_term_ref(shape)));
818 let path = parsed_path.as_ref();
819
820 match validator.kind {
821 SparqlQueryKind::Ask => {
822 for value in value_nodes {
823 let mut bindings = base.clone();
824 bindings.push(("this".to_string(), focus.clone()));
825 bindings.push(("value".to_string(), value.clone()));
826 let violates = match self.sparql.eval_ask(&validator.query, path, &bindings)
828 {
829 Ok(conforms) => !conforms,
830 Err(_) => true,
831 };
832 if violates {
833 self.push_component_result(
834 out,
835 component,
836 shape,
837 focus,
838 path_term.clone(),
839 Some(value.clone()),
840 &bindings,
841 &validator.messages,
842 );
843 }
844 }
845 }
846 SparqlQueryKind::Select => {
847 let mut bindings = base.clone();
848 bindings.push(("this".to_string(), focus.clone()));
849 match self.sparql.eval_select(&validator.query, path, &bindings) {
850 Ok(rows) => {
851 for row in rows {
852 let value = row
855 .get("value")
856 .cloned()
857 .or_else(|| (!is_property_shape).then(|| focus.clone()));
858 let path = row.get("path").cloned().or_else(|| path_term.clone());
859 let mut binds = bindings.clone();
860 binds.extend(row);
861 self.push_component_result(
862 out,
863 component,
864 shape,
865 focus,
866 path,
867 value,
868 &binds,
869 &validator.messages,
870 );
871 }
872 }
873 Err(_) => self.push_component_result(
874 out,
875 component,
876 shape,
877 focus,
878 path_term.clone(),
879 None,
880 &bindings,
881 &validator.messages,
882 ),
883 }
884 }
885 }
886 }
887 }
888
889 #[allow(clippy::too_many_arguments)]
890 fn push_component_result(
891 &self,
892 out: &mut Vec<ValidationResult>,
893 component: &CustomComponent,
894 shape: &NamedOrBlankNode,
895 focus: &Term,
896 path: Option<Term>,
897 value: Option<Term>,
898 bindings: &[(String, Term)],
899 validator_messages: &[Term],
900 ) {
901 let raw = if validator_messages.is_empty() {
902 self.messages(shape)
903 } else {
904 validator_messages.to_vec()
905 };
906 let bind_map: HashMap<String, Term> = bindings.iter().cloned().collect();
907 let messages = substitute_messages(&raw, focus, &bind_map);
908 out.push(ValidationResult {
909 focus: focus.clone(),
910 path,
911 value,
912 component: component.iri.clone(),
913 source_shape: node_term_ref(shape),
914 severity: self.severity(shape),
915 messages,
916 });
917 }
918
919 fn collect_expression(
926 &self,
927 shape: &NamedOrBlankNode,
928 focus: &Term,
929 out: &mut Vec<ValidationResult>,
930 visited: &mut Visited,
931 ) {
932 for expr_term in self.shapes.objects(shape, vocab::SH_EXPRESSION) {
933 let Some(results) = self.eval_node_expr(&expr_term, focus, visited) else {
934 continue;
935 };
936 for value in results {
937 if is_boolean_true(&value) {
938 continue;
939 }
940 out.push(ValidationResult {
941 focus: focus.clone(),
942 path: None,
943 value: Some(value),
944 component: vocab::SH_CC_EXPRESSION.into_owned(),
945 source_shape: node_term_ref(shape),
946 severity: self.severity(shape),
947 messages: self.messages(shape),
948 });
949 }
950 }
951 }
952
953 fn eval_node_expr(
958 &self,
959 term: &Term,
960 focus: &Term,
961 visited: &mut Visited,
962 ) -> Option<Vec<Term>> {
963 match term {
964 Term::NamedNode(n) if n.as_ref() == vocab::SH_THIS => Some(vec![focus.clone()]),
965 Term::NamedNode(_) | Term::Literal(_) => Some(vec![term.clone()]),
966 Term::BlankNode(_) => {
967 let node = term_to_node(term)?;
968 if let Some(path_term) = self.shapes.object(&node, vocab::SH_PATH) {
969 let path = parse_path(self.shapes, &path_term).ok()?;
970 Some(succ(self.frozen(), focus, &path).into_iter().collect())
971 } else if let Some(filter_shape) = self.shapes.object(&node, vocab::SH_FILTER_SHAPE)
972 {
973 let filter_shape = term_to_node(&filter_shape)?;
974 let nodes_term = self.shapes.object(&node, vocab::SH_NODES)?;
975 let inputs = self.eval_node_expr(&nodes_term, focus, visited)?;
976 Some(
977 inputs
978 .into_iter()
979 .filter(|x| self.conforms(&filter_shape, x, visited))
980 .collect(),
981 )
982 } else if let Some(list) = self.shapes.object(&node, vocab::SH_INTERSECTION) {
983 self.eval_node_expr_set(&list, focus, visited, true)
984 } else if let Some(list) = self.shapes.object(&node, vocab::SH_UNION) {
985 self.eval_node_expr_set(&list, focus, visited, false)
986 } else {
987 None
989 }
990 }
991 }
992 }
993
994 fn eval_node_expr_set(
998 &self,
999 list_head: &Term,
1000 focus: &Term,
1001 visited: &mut Visited,
1002 intersect: bool,
1003 ) -> Option<Vec<Term>> {
1004 let members = self.shapes.read_list(list_head);
1005 if members.is_empty() {
1006 return None;
1007 }
1008 let mut iter = members.iter();
1009 let mut acc = self.eval_node_expr(iter.next().unwrap(), focus, visited)?;
1010 for member in iter {
1011 let next = self.eval_node_expr(member, focus, visited)?;
1012 if intersect {
1013 acc.retain(|x| next.contains(x));
1014 } else {
1015 for t in next {
1016 if !acc.contains(&t) {
1017 acc.push(t);
1018 }
1019 }
1020 }
1021 }
1022 Some(acc)
1023 }
1024
1025 fn build_sparql_constraint(
1034 &self,
1035 shape: &NamedOrBlankNode,
1036 constraint_node: &NamedOrBlankNode,
1037 parsed_path: &Option<Path>,
1038 ) -> Option<SparqlConstraint> {
1039 let (kind, raw) = if let Some(Term::Literal(query)) =
1040 self.shapes.object(constraint_node, vocab::SH_SELECT)
1041 {
1042 (SparqlQueryKind::Select, query.value().to_string())
1043 } else if let Some(Term::Literal(query)) =
1044 self.shapes.object(constraint_node, vocab::SH_ASK)
1045 {
1046 (SparqlQueryKind::Ask, query.value().to_string())
1047 } else {
1048 return None;
1049 };
1050 let (_, query) = canonical_sparql_query(self.shapes, constraint_node, &raw).ok()?;
1051 Some(SparqlConstraint {
1052 kind,
1053 query,
1054 path: parsed_path.clone(),
1055 shape: Some(node_term_ref(shape)),
1056 messages: Vec::new(),
1059 extra_bindings: Vec::new(),
1060 bind_value_to_this: false,
1061 })
1062 }
1063
1064 fn prefetch_sparql(&self, shape: &NamedOrBlankNode, foci: &[Term]) {
1068 if !self.needs_sparql || foci.len() < 2 {
1069 return;
1070 }
1071 let (_, parsed_path) = self.shape_path(shape);
1072 for constraint_term in self.shapes.objects(shape, vocab::SH_SPARQL) {
1073 let Some(constraint_node) = term_to_node(&constraint_term) else {
1074 continue;
1075 };
1076 if let Some(constraint) =
1077 self.build_sparql_constraint(shape, &constraint_node, &parsed_path)
1078 {
1079 let _ = self.sparql.prefetch_constraint(&constraint, foci);
1080 }
1081 }
1082 }
1083
1084 fn collect_sparql(
1085 &self,
1086 shape: &NamedOrBlankNode,
1087 focus: &Term,
1088 path_term: &Option<Term>,
1089 parsed_path: &Option<Path>,
1090 out: &mut Vec<ValidationResult>,
1091 ) {
1092 if !self.needs_sparql {
1093 return;
1094 }
1095 let sparql = &self.sparql;
1096 let severity = self.severity(shape);
1097 for constraint_term in self.shapes.objects(shape, vocab::SH_SPARQL) {
1098 let Some(constraint_node) = term_to_node(&constraint_term) else {
1099 continue;
1100 };
1101 let Some(constraint) =
1102 self.build_sparql_constraint(shape, &constraint_node, parsed_path)
1103 else {
1104 continue;
1105 };
1106 let raw_messages = {
1109 let on_constraint = self.shapes.objects(&constraint_node, vocab::SH_MESSAGE);
1110 if on_constraint.is_empty() {
1111 self.messages(shape)
1112 } else {
1113 on_constraint
1114 }
1115 };
1116 match sparql.constraint_violations(&constraint, focus) {
1117 Ok(violations) => {
1118 for violation in violations {
1119 let messages =
1120 substitute_messages(&raw_messages, focus, &violation.bindings);
1121 let value = violation.value.or_else(|| match constraint.kind {
1124 SparqlQueryKind::Select => Some(focus.clone()),
1125 SparqlQueryKind::Ask => None,
1126 });
1127 out.push(ValidationResult {
1128 focus: focus.clone(),
1129 path: violation.path.or_else(|| path_term.clone()),
1130 value,
1131 component: vocab::SH_CC_SPARQL.into_owned(),
1132 source_shape: node_term_ref(shape),
1133 severity: severity.clone(),
1134 messages,
1135 });
1136 }
1137 }
1138 Err(error) => {
1142 let mut messages = raw_messages;
1143 messages.push(Term::Literal(Literal::new_simple_literal(format!(
1144 "SPARQL constraint evaluation failed: {error}"
1145 ))));
1146 out.push(ValidationResult {
1147 focus: focus.clone(),
1148 path: path_term.clone(),
1149 value: None,
1150 component: vocab::SH_CC_SPARQL.into_owned(),
1151 source_shape: node_term_ref(shape),
1152 severity: severity.clone(),
1153 messages,
1154 });
1155 }
1156 }
1157 }
1158 }
1159
1160 fn collect_closed(
1161 &self,
1162 shape: &NamedOrBlankNode,
1163 focus: &Term,
1164 value_nodes: &[Term],
1165 out: &mut Vec<ValidationResult>,
1166 ) {
1167 if !self.bool(shape, vocab::SH_CLOSED) {
1168 return;
1169 }
1170 let mut allowed = HashSet::new();
1171 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
1172 let Some(prop) = term_to_node(&prop) else {
1173 continue;
1174 };
1175 if let Some(Term::NamedNode(path)) = self.shapes.object(&prop, vocab::SH_PATH) {
1176 allowed.insert(path);
1177 }
1178 }
1179 for list in self.shapes.objects(shape, vocab::SH_IGNORED_PROPERTIES) {
1180 for term in self.shapes.read_list(&list) {
1181 if let Term::NamedNode(predicate) = term {
1182 allowed.insert(predicate);
1183 }
1184 }
1185 }
1186 for value_node in value_nodes {
1187 for (predicate, object) in self.frozen().outgoing(value_node) {
1188 if allowed.contains(&predicate) {
1189 continue;
1190 }
1191 out.push(ValidationResult {
1192 focus: focus.clone(),
1193 path: Some(Term::NamedNode(predicate)),
1194 value: Some(object),
1195 component: vocab::SH_CC_CLOSED.into_owned(),
1196 source_shape: node_term_ref(shape),
1197 severity: self.severity(shape),
1198 messages: self.messages(shape),
1199 });
1200 }
1201 }
1202 }
1203
1204 fn collect_property_pairs(
1205 &self,
1206 shape: &NamedOrBlankNode,
1207 focus: &Term,
1208 path: &Option<Term>,
1209 value_nodes: &[Term],
1210 out: &mut Vec<ValidationResult>,
1211 ) {
1212 for predicate in self.shapes.objects(shape, vocab::SH_EQUALS) {
1213 let Term::NamedNode(predicate) = predicate else {
1214 continue;
1215 };
1216 let other = succ(self.frozen(), focus, &Path::Pred(predicate));
1217 for value in value_nodes.iter().filter(|value| !other.contains(*value)) {
1218 self.push(
1219 out,
1220 shape,
1221 focus,
1222 path.clone(),
1223 Some((*value).clone()),
1224 vocab::SH_CC_EQUALS,
1225 );
1226 }
1227 for value in other.iter().filter(|value| !value_nodes.contains(*value)) {
1228 self.push(
1229 out,
1230 shape,
1231 focus,
1232 path.clone(),
1233 Some(value.clone()),
1234 vocab::SH_CC_EQUALS,
1235 );
1236 }
1237 }
1238 for predicate in self.shapes.objects(shape, vocab::SH_DISJOINT) {
1239 let Term::NamedNode(predicate) = predicate else {
1240 continue;
1241 };
1242 let other = succ(self.frozen(), focus, &Path::Pred(predicate));
1243 for value in value_nodes.iter().filter(|value| other.contains(*value)) {
1244 self.push(
1245 out,
1246 shape,
1247 focus,
1248 path.clone(),
1249 Some((*value).clone()),
1250 vocab::SH_CC_DISJOINT,
1251 );
1252 }
1253 }
1254 for (constraint, component, inclusive) in [
1255 (vocab::SH_LESS_THAN, vocab::SH_CC_LESS_THAN, false),
1256 (
1257 vocab::SH_LESS_THAN_OR_EQUALS,
1258 vocab::SH_CC_LESS_THAN_OR_EQUALS,
1259 true,
1260 ),
1261 ] {
1262 for predicate in self.shapes.objects(shape, constraint) {
1263 let Term::NamedNode(predicate) = predicate else {
1264 continue;
1265 };
1266 for left in value_nodes {
1267 for right in succ(self.frozen(), focus, &Path::Pred(predicate.clone())) {
1268 let ordering = compare_terms(left, &right);
1269 let passes = ordering == Some(Ordering::Less)
1270 || inclusive && ordering == Some(Ordering::Equal);
1271 if !passes {
1272 self.push(
1273 out,
1274 shape,
1275 focus,
1276 path.clone(),
1277 Some(left.clone()),
1278 component,
1279 );
1280 }
1281 }
1282 }
1283 }
1284 }
1285 }
1286
1287 fn collect_unique_lang(
1288 &self,
1289 shape: &NamedOrBlankNode,
1290 focus: &Term,
1291 path: &Option<Term>,
1292 value_nodes: &[Term],
1293 out: &mut Vec<ValidationResult>,
1294 ) {
1295 if !self.bool(shape, vocab::SH_UNIQUE_LANG) {
1296 return;
1297 }
1298 let mut counts = HashMap::new();
1299 for value in value_nodes {
1300 if let Term::Literal(literal) = value
1301 && let Some(language) = literal.language()
1302 {
1303 *counts
1304 .entry(language.to_ascii_lowercase())
1305 .or_insert(0usize) += 1;
1306 }
1307 }
1308 for _ in counts.values().filter(|count| **count > 1) {
1309 self.push(
1310 out,
1311 shape,
1312 focus,
1313 path.clone(),
1314 None,
1315 vocab::SH_CC_UNIQUE_LANG,
1316 );
1317 }
1318 }
1319
1320 fn collect_qualified_counts(
1321 &self,
1322 shape: &NamedOrBlankNode,
1323 focus: &Term,
1324 path: &Option<Term>,
1325 value_nodes: &[Term],
1326 out: &mut Vec<ValidationResult>,
1327 visited: &mut Visited,
1328 ) {
1329 for qualifier in self.shapes.objects(shape, vocab::SH_QUALIFIED_VALUE_SHAPE) {
1330 let Some(qualifier) = term_to_node(&qualifier) else {
1331 continue;
1332 };
1333 let siblings = if self.bool(shape, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
1334 self.sibling_qualified_shapes(shape, &qualifier)
1335 } else {
1336 Vec::new()
1337 };
1338 let count = value_nodes
1339 .iter()
1340 .filter(|value| {
1341 self.conforms(&qualifier, value, visited)
1342 && siblings
1343 .iter()
1344 .all(|sibling| !self.conforms(sibling, value, visited))
1345 })
1346 .count() as u64;
1347 if let Some(min) = self.int(shape, vocab::SH_QUALIFIED_MIN_COUNT)
1348 && count < min
1349 {
1350 self.push(
1351 out,
1352 shape,
1353 focus,
1354 path.clone(),
1355 None,
1356 vocab::SH_CC_QUALIFIED_MIN_COUNT,
1357 );
1358 }
1359 if let Some(max) = self.int(shape, vocab::SH_QUALIFIED_MAX_COUNT)
1360 && count > max
1361 {
1362 self.push(
1363 out,
1364 shape,
1365 focus,
1366 path.clone(),
1367 None,
1368 vocab::SH_CC_QUALIFIED_MAX_COUNT,
1369 );
1370 }
1371 }
1372 }
1373
1374 fn sibling_qualified_shapes(
1375 &self,
1376 shape: &NamedOrBlankNode,
1377 qualifier: &NamedOrBlankNode,
1378 ) -> Vec<NamedOrBlankNode> {
1379 let shape_term = node_term_ref(shape);
1380 let mut siblings = HashSet::new();
1381 for triple in self.shapes.graph.triples_for_predicate(vocab::SH_PROPERTY) {
1382 if triple.object != shape_term.as_ref() {
1383 continue;
1384 }
1385 let parent = triple.subject.into_owned();
1386 for property in self.shapes.objects(&parent, vocab::SH_PROPERTY) {
1387 let Some(property) = term_to_node(&property) else {
1388 continue;
1389 };
1390 for qualifier in self
1391 .shapes
1392 .objects(&property, vocab::SH_QUALIFIED_VALUE_SHAPE)
1393 {
1394 if let Some(qualifier) = term_to_node(&qualifier) {
1395 siblings.insert(qualifier);
1396 }
1397 }
1398 }
1399 }
1400 siblings.remove(qualifier);
1401 siblings.into_iter().collect()
1402 }
1403
1404 fn push(
1405 &self,
1406 out: &mut Vec<ValidationResult>,
1407 shape: &NamedOrBlankNode,
1408 focus: &Term,
1409 path: Option<Term>,
1410 value: Option<Term>,
1411 component: NamedNodeRef<'static>,
1412 ) {
1413 let mut bindings = HashMap::new();
1414 if let Some(v) = &value {
1415 bindings.insert("value".to_string(), v.clone());
1416 }
1417 if let Some(p) = &path {
1418 bindings.insert("path".to_string(), p.clone());
1419 }
1420 let raw = self.messages(shape);
1421 let messages = substitute_messages(&raw, focus, &bindings);
1422 out.push(ValidationResult {
1423 focus: focus.clone(),
1424 path,
1425 value,
1426 component: component.into_owned(),
1427 source_shape: node_term_ref(shape),
1428 severity: self.severity(shape),
1429 messages,
1430 });
1431 }
1432
1433 fn messages(&self, shape: &NamedOrBlankNode) -> Vec<Term> {
1435 self.shapes.objects(shape, vocab::SH_MESSAGE)
1436 }
1437
1438 fn conforms(&self, shape: &NamedOrBlankNode, focus: &Term, visited: &mut Visited) -> bool {
1439 let mut scratch = Vec::new();
1440 self.collect(shape, focus, &mut scratch, visited);
1441 scratch.is_empty()
1442 }
1443
1444 fn value_checks(
1447 &self,
1448 shape: &NamedOrBlankNode,
1449 u: &Term,
1450 visited: &mut Visited,
1451 ) -> Vec<(NamedNode, bool)> {
1452 let mut checks = Vec::new();
1453
1454 for c in self.shapes.objects(shape, vocab::SH_CLASS) {
1455 checks.push((vocab::SH_CC_CLASS.into_owned(), self.is_instance(u, &c)));
1456 }
1457 for d in self.shapes.objects(shape, vocab::SH_DATATYPE) {
1458 if let Term::NamedNode(dt) = d {
1459 let ok = value_type_holds(&ValueType::Datatype(dt), u);
1460 checks.push((vocab::SH_CC_DATATYPE.into_owned(), ok));
1461 }
1462 }
1463 for k in self.shapes.objects(shape, vocab::SH_NODE_KIND) {
1464 if let Some(set) = map_node_kind(&k) {
1465 checks.push((vocab::SH_CC_NODE_KIND.into_owned(), set.matches(u)));
1466 }
1467 }
1468 for (pred_iri, comp, inclusive) in [
1470 (vocab::SH_MIN_INCLUSIVE, vocab::SH_CC_MIN_INCLUSIVE, true),
1471 (vocab::SH_MIN_EXCLUSIVE, vocab::SH_CC_MIN_EXCLUSIVE, false),
1472 ] {
1473 if let Some(Term::Literal(b)) = self.shapes.object(shape, pred_iri) {
1474 let vt = ValueType::NumericRange {
1475 lo: Some(Bound {
1476 value: b,
1477 inclusive,
1478 }),
1479 hi: None,
1480 };
1481 checks.push((comp.into_owned(), value_type_holds(&vt, u)));
1482 }
1483 }
1484 for (pred_iri, comp, inclusive) in [
1485 (vocab::SH_MAX_INCLUSIVE, vocab::SH_CC_MAX_INCLUSIVE, true),
1486 (vocab::SH_MAX_EXCLUSIVE, vocab::SH_CC_MAX_EXCLUSIVE, false),
1487 ] {
1488 if let Some(Term::Literal(b)) = self.shapes.object(shape, pred_iri) {
1489 let vt = ValueType::NumericRange {
1490 lo: None,
1491 hi: Some(Bound {
1492 value: b,
1493 inclusive,
1494 }),
1495 };
1496 checks.push((comp.into_owned(), value_type_holds(&vt, u)));
1497 }
1498 }
1499 let min_len = self.int(shape, vocab::SH_MIN_LENGTH);
1501 let max_len = self.int(shape, vocab::SH_MAX_LENGTH);
1502 if let Some(m) = min_len {
1503 let vt = ValueType::Length {
1504 min: Some(m),
1505 max: None,
1506 };
1507 checks.push((
1508 vocab::SH_CC_MIN_LENGTH.into_owned(),
1509 value_type_holds(&vt, u),
1510 ));
1511 }
1512 if let Some(m) = max_len {
1513 let vt = ValueType::Length {
1514 min: None,
1515 max: Some(m),
1516 };
1517 checks.push((
1518 vocab::SH_CC_MAX_LENGTH.into_owned(),
1519 value_type_holds(&vt, u),
1520 ));
1521 }
1522 if let Some(Term::Literal(re)) = self.shapes.object(shape, vocab::SH_PATTERN) {
1523 let flags = match self.shapes.object(shape, vocab::SH_FLAGS) {
1524 Some(Term::Literal(f)) => f.value().to_string(),
1525 _ => String::new(),
1526 };
1527 let vt = ValueType::Pattern {
1528 regex: re.value().to_string(),
1529 flags,
1530 };
1531 checks.push((vocab::SH_CC_PATTERN.into_owned(), value_type_holds(&vt, u)));
1532 }
1533 for list in self.shapes.objects(shape, vocab::SH_IN) {
1535 let members = self.shapes.read_list(&list);
1536 checks.push((vocab::SH_CC_IN.into_owned(), members.contains(u)));
1537 }
1538 for list in self.shapes.objects(shape, vocab::SH_LANGUAGE_IN) {
1539 let languages = self
1540 .shapes
1541 .read_list(&list)
1542 .into_iter()
1543 .filter_map(|term| match term {
1544 Term::Literal(literal) => Some(literal.value().to_string()),
1545 _ => None,
1546 })
1547 .collect();
1548 checks.push((
1549 vocab::SH_CC_LANGUAGE_IN.into_owned(),
1550 value_type_holds(&ValueType::LangIn(languages), u),
1551 ));
1552 }
1553
1554 for list in self.shapes.objects(shape, vocab::SH_AND) {
1556 let ok = self
1557 .shapes
1558 .read_list(&list)
1559 .iter()
1560 .filter_map(term_to_node)
1561 .all(|m| self.conforms(&m, u, visited));
1562 checks.push((vocab::SH_CC_AND.into_owned(), ok));
1563 }
1564 for list in self.shapes.objects(shape, vocab::SH_OR) {
1565 let ok = self
1566 .shapes
1567 .read_list(&list)
1568 .iter()
1569 .filter_map(term_to_node)
1570 .any(|m| self.conforms(&m, u, visited));
1571 checks.push((vocab::SH_CC_OR.into_owned(), ok));
1572 }
1573 for list in self.shapes.objects(shape, vocab::SH_XONE) {
1574 let count = self
1575 .shapes
1576 .read_list(&list)
1577 .iter()
1578 .filter_map(term_to_node)
1579 .filter(|m| self.conforms(m, u, visited))
1580 .count();
1581 checks.push((vocab::SH_CC_XONE.into_owned(), count == 1));
1582 }
1583 for n in self.shapes.objects(shape, vocab::SH_NOT) {
1584 if let Some(nn) = term_to_node(&n) {
1585 checks.push((
1586 vocab::SH_CC_NOT.into_owned(),
1587 !self.conforms(&nn, u, visited),
1588 ));
1589 }
1590 }
1591 for n in self.shapes.objects(shape, vocab::SH_NODE) {
1592 if let Some(nn) = term_to_node(&n) {
1593 checks.push((
1594 vocab::SH_CC_NODE.into_owned(),
1595 self.conforms(&nn, u, visited),
1596 ));
1597 }
1598 }
1599
1600 checks
1601 }
1602
1603 fn is_instance(&self, u: &Term, class: &Term) -> bool {
1604 succ(self.frozen(), u, &class_path()).contains(class)
1605 }
1606
1607 fn int(&self, s: &NamedOrBlankNode, p: NamedNodeRef) -> Option<u64> {
1608 match self.shapes.object(s, p) {
1609 Some(Term::Literal(l)) => l.value().parse().ok(),
1610 _ => None,
1611 }
1612 }
1613
1614 fn bool(&self, s: &NamedOrBlankNode, p: NamedNodeRef) -> bool {
1615 matches!(
1616 self.shapes.object(s, p),
1617 Some(Term::Literal(ref literal)) if matches!(literal.value(), "true" | "1")
1618 )
1619 }
1620
1621 fn severity(&self, shape: &NamedOrBlankNode) -> NamedNode {
1624 match self.shapes.object(shape, vocab::SH_SEVERITY) {
1625 Some(Term::NamedNode(n)) => n,
1626 _ => vocab::SH_VIOLATION.into_owned(),
1627 }
1628 }
1629}
1630
1631fn is_shape_node(shapes: &Loaded, node: &NamedOrBlankNode) -> bool {
1632 shapes.has_type(node, vocab::SH_NODE_SHAPE)
1633 || shapes.has_type(node, vocab::SH_PROPERTY_SHAPE)
1634 || [
1635 vocab::SH_PROPERTY,
1636 vocab::SH_NODE,
1637 vocab::SH_AND,
1638 vocab::SH_OR,
1639 vocab::SH_NOT,
1640 vocab::SH_XONE,
1641 vocab::SH_DATATYPE,
1642 vocab::SH_CLASS,
1643 vocab::SH_NODE_KIND,
1644 vocab::SH_IN,
1645 vocab::SH_HAS_VALUE,
1646 vocab::SH_PROPERTY,
1647 ]
1648 .iter()
1649 .any(|predicate| shapes.object(node, *predicate).is_some())
1650}
1651
1652fn shapes_reference_shapes_graph(shapes: &Loaded) -> bool {
1655 [vocab::SH_SELECT, vocab::SH_ASK].iter().any(|predicate| {
1656 shapes.graph.triples_for_predicate(*predicate).any(
1657 |t| matches!(t.object, oxrdf::TermRef::Literal(l) if l.value().contains("shapesGraph")),
1658 )
1659 })
1660}
1661
1662fn class_path() -> Path {
1663 Path::seq(vec![
1664 Path::Pred(vocab::rdf_type()),
1665 Path::star(Path::Pred(vocab::rdfs_subclassof())),
1666 ])
1667}
1668
1669fn build_class_index(
1678 focus_data: &Graph,
1679 frozen: &FrozenIndexedDataset,
1680) -> HashMap<Term, Vec<Term>> {
1681 let focus_nodes = graph_nodes(focus_data);
1682 let subclass_star = Path::star(Path::Pred(vocab::rdfs_subclassof()));
1683 let mut supers: HashMap<Term, Vec<Term>> = HashMap::new();
1684 let mut index: HashMap<Term, Vec<Term>> = HashMap::new();
1685 let mut seen: HashSet<(Term, Term)> = HashSet::new();
1686 for (node, ty) in frozen.triples_for_predicate(&vocab::rdf_type()) {
1687 if !focus_nodes.contains(&node) {
1688 continue;
1689 }
1690 let classes = supers
1691 .entry(ty.clone())
1692 .or_insert_with(|| succ(frozen, &ty, &subclass_star).into_iter().collect());
1693 for class in classes.iter() {
1694 if seen.insert((class.clone(), node.clone())) {
1695 index.entry(class.clone()).or_default().push(node.clone());
1696 }
1697 }
1698 }
1699 index
1700}
1701
1702fn graph_nodes(graph: &Graph) -> HashSet<Term> {
1703 let mut nodes = HashSet::new();
1704 for triple in graph.iter() {
1705 nodes.insert(node_term(triple.subject));
1706 nodes.insert(triple.object.into_owned());
1707 }
1708 nodes
1709}
1710
1711fn node_term(s: oxrdf::NamedOrBlankNodeRef) -> Term {
1712 crate::path::term_of(s.into_owned())
1713}
1714
1715fn node_term_ref(s: &NamedOrBlankNode) -> Term {
1716 match s {
1717 NamedOrBlankNode::NamedNode(n) => Term::NamedNode(n.clone()),
1718 NamedOrBlankNode::BlankNode(b) => Term::BlankNode(b.clone()),
1719 }
1720}
1721
1722fn map_node_kind(term: &Term) -> Option<NodeKindSet> {
1723 let Term::NamedNode(n) = term else {
1724 return None;
1725 };
1726 let r = n.as_ref();
1727 Some(if r == vocab::SH_IRI {
1728 NodeKindSet::IRI
1729 } else if r == vocab::SH_BLANK_NODE {
1730 NodeKindSet::BLANK_NODE
1731 } else if r == vocab::SH_LITERAL {
1732 NodeKindSet::LITERAL
1733 } else if r == vocab::SH_BLANK_NODE_OR_IRI {
1734 NodeKindSet::BLANK_NODE_OR_IRI
1735 } else if r == vocab::SH_BLANK_NODE_OR_LITERAL {
1736 NodeKindSet::BLANK_NODE_OR_LITERAL
1737 } else if r == vocab::SH_IRI_OR_LITERAL {
1738 NodeKindSet::IRI_OR_LITERAL
1739 } else {
1740 return None;
1741 })
1742}