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),
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 build_components(shapes: &Loaded) -> Vec<CustomComponent> {
356 let mut out = Vec::new();
357 let mut seen = HashSet::new();
358 for triple in shapes.graph.triples_for_predicate(vocab::SH_PARAMETER) {
359 let subject = triple.subject.into_owned();
360 if !seen.insert(subject.clone()) {
361 continue;
362 }
363 let NamedOrBlankNode::NamedNode(iri) = &subject else {
364 continue; };
366 let node_validator = shapes
367 .object(&subject, vocab::SH_NODE_VALIDATOR)
368 .and_then(|v| parse_validator(shapes, &v));
369 let property_validator = shapes
370 .object(&subject, vocab::SH_PROPERTY_VALIDATOR)
371 .and_then(|v| parse_validator(shapes, &v));
372 let generic_validator = shapes
373 .object(&subject, vocab::SH_VALIDATOR)
374 .and_then(|v| parse_validator(shapes, &v));
375 if node_validator.is_none() && property_validator.is_none() && generic_validator.is_none() {
376 continue; }
378 let mut params = Vec::new();
379 for p in shapes.objects(&subject, vocab::SH_PARAMETER) {
380 let Some(pn) = term_to_node(&p) else { continue };
381 let Some(Term::NamedNode(path)) = shapes.object(&pn, vocab::SH_PATH) else {
382 continue;
383 };
384 let var = local_name(path.as_str()).to_string();
385 let optional = matches!(
386 shapes.object(&pn, vocab::SH_OPTIONAL),
387 Some(Term::Literal(ref l)) if l.value() == "true"
388 );
389 params.push(ComponentParam {
390 path,
391 var,
392 optional,
393 });
394 }
395 if params.is_empty() {
396 continue;
397 }
398 out.push(CustomComponent {
399 iri: iri.clone(),
400 params,
401 node_validator,
402 property_validator,
403 generic_validator,
404 });
405 }
406 out.sort_by(|a, b| a.iri.as_str().cmp(b.iri.as_str()));
407 out
408}
409
410fn parse_validator(shapes: &Loaded, node: &Term) -> Option<ComponentValidator> {
413 let node = term_to_node(node)?;
414 let (kind, raw) = if let Some(Term::Literal(q)) = shapes.object(&node, vocab::SH_ASK) {
415 (SparqlQueryKind::Ask, q.value().to_string())
416 } else if let Some(Term::Literal(q)) = shapes.object(&node, vocab::SH_SELECT) {
417 (SparqlQueryKind::Select, q.value().to_string())
418 } else {
419 return None;
420 };
421 let (_, query) = canonical_sparql_query(shapes, &node, &raw).ok()?;
422 let messages = shapes.objects(&node, vocab::SH_MESSAGE);
423 Some(ComponentValidator {
424 kind,
425 query,
426 messages,
427 })
428}
429
430fn local_name(iri: &str) -> &str {
433 iri.rsplit(['#', '/']).next().unwrap_or(iri)
434}
435
436pub(crate) fn collect_functions(shapes: &Loaded) -> Vec<FunctionDef> {
441 let mut out = Vec::new();
442 for func in shapes
443 .graph
444 .subjects_for_predicate_object(vocab::RDF_TYPE, vocab::SH_SPARQL_FUNCTION)
445 .map(|s| s.into_owned())
446 .collect::<Vec<_>>()
447 {
448 let NamedOrBlankNode::NamedNode(iri) = &func else {
449 continue;
450 };
451 let raw = match shapes
452 .object(&func, vocab::SH_SELECT)
453 .or_else(|| shapes.object(&func, vocab::SH_ASK))
454 {
455 Some(Term::Literal(q)) => q.value().to_string(),
456 _ => continue,
457 };
458 let Ok((_, query)) = canonical_sparql_query(shapes, &func, &raw) else {
459 continue;
460 };
461 out.push(FunctionDef {
462 iri: iri.clone(),
463 params: function_param_names(shapes, &func),
464 reads_graph: crate::sparql::query_reads_graph(&query),
465 query,
466 });
467 }
468 out
469}
470
471fn function_param_names(shapes: &Loaded, func: &NamedOrBlankNode) -> Vec<String> {
474 let mut params: Vec<(i64, String)> = shapes
475 .objects(func, vocab::SH_PARAMETER)
476 .iter()
477 .filter_map(|p| {
478 let pn = term_to_node(p)?;
479 let order = match shapes.object(&pn, vocab::SH_ORDER) {
480 Some(Term::Literal(l)) => l.value().parse::<i64>().unwrap_or(0),
481 _ => 0,
482 };
483 let name = match shapes.object(&pn, vocab::SH_NAME) {
484 Some(Term::Literal(l)) => l.value().to_string(),
485 _ => match shapes.object(&pn, vocab::SH_PATH) {
486 Some(Term::NamedNode(n)) => local_name(n.as_str()).to_string(),
487 _ => return None,
488 },
489 };
490 Some((order, name))
491 })
492 .collect();
493 params.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
494 params.into_iter().map(|(_, name)| name).collect()
495}
496
497struct Reporter<'a> {
498 shapes: &'a Loaded,
499 focus_data: &'a Graph,
500 sparql: SparqlExecutor,
501 needs_sparql: bool,
502 class_index: HashMap<Term, Vec<Term>>,
505 path_cache: RefCell<HashMap<NamedOrBlankNode, PathCacheEntry>>,
508 components: Vec<CustomComponent>,
511}
512
513type Visited = HashSet<(NamedOrBlankNode, Term)>;
514
515type PathCacheEntry = (Option<Term>, Option<Path>);
517
518impl Reporter<'_> {
519 fn frozen(&self) -> &FrozenIndexedDataset {
520 self.sparql
521 .frozen()
522 .expect("report validation always has a frozen dataset")
523 }
524
525 fn target_shapes(&self) -> Vec<NamedOrBlankNode> {
526 let mut found: HashSet<NamedOrBlankNode> = HashSet::new();
527 for t in self.shapes.graph.iter() {
528 let p = t.predicate;
529 if p == vocab::SH_TARGET_NODE
530 || p == vocab::SH_TARGET_CLASS
531 || p == vocab::SH_TARGET_SUBJECTS_OF
532 || p == vocab::SH_TARGET_OBJECTS_OF
533 {
534 found.insert(t.subject.into_owned());
535 }
536 if p == vocab::SH_TARGET
538 && let Some(target) = term_to_node(&t.object.into_owned())
539 && self.shapes.object(&target, vocab::SH_SELECT).is_some()
540 {
541 found.insert(t.subject.into_owned());
542 }
543 if p == vocab::RDF_TYPE {
545 let s = t.subject.into_owned();
546 if self.is_class(&s) && self.is_shape(&s) {
547 found.insert(s);
548 }
549 }
550 }
551 let mut v: Vec<_> = found.into_iter().collect();
552 v.sort_by_key(|n| n.to_string());
553 v
554 }
555
556 fn is_shape(&self, n: &NamedOrBlankNode) -> bool {
558 is_shape_node(self.shapes, n)
559 }
560
561 fn is_class(&self, n: &NamedOrBlankNode) -> bool {
562 self.shapes.is_instance_of(n, vocab::RDFS_CLASS)
563 || self.shapes.is_instance_of(n, vocab::OWL_CLASS)
564 }
565
566 fn deactivated(&self, n: &NamedOrBlankNode) -> bool {
567 matches!(self.shapes.object(n, vocab::SH_DEACTIVATED),
568 Some(Term::Literal(ref l)) if l.value() == "true")
569 }
570
571 fn focus_nodes(&self, shape: &NamedOrBlankNode) -> Vec<Term> {
572 let mut nodes = Vec::new();
573 nodes.extend(self.shapes.objects(shape, vocab::SH_TARGET_NODE));
574 for c in self.shapes.objects(shape, vocab::SH_TARGET_CLASS) {
575 if let Some(instances) = self.class_index.get(&c) {
576 nodes.extend(instances.iter().cloned());
577 }
578 }
579 for p in self.shapes.objects(shape, vocab::SH_TARGET_SUBJECTS_OF) {
580 if let Term::NamedNode(n) = p {
581 nodes.extend(
582 self.focus_data
583 .triples_for_predicate(n.as_ref())
584 .map(|t| node_term(t.subject)),
585 );
586 }
587 }
588 for p in self.shapes.objects(shape, vocab::SH_TARGET_OBJECTS_OF) {
589 if let Term::NamedNode(n) = p {
590 nodes.extend(
591 self.focus_data
592 .triples_for_predicate(n.as_ref())
593 .map(|t| t.object.into_owned()),
594 );
595 }
596 }
597 if self.needs_sparql {
600 let exec = &self.sparql;
601 for target in self.shapes.objects(shape, vocab::SH_TARGET) {
602 let Some(target_node) = term_to_node(&target) else {
603 continue;
604 };
605 let Some(Term::Literal(query)) = self.shapes.object(&target_node, vocab::SH_SELECT)
606 else {
607 continue;
608 };
609 let Ok((_, canonical)) =
611 canonical_sparql_query(self.shapes, &target_node, query.value())
612 else {
613 continue;
614 };
615 if let Ok(found) = exec.target_nodes(&canonical) {
616 nodes.extend(found);
617 }
618 }
619 }
620 if let NamedOrBlankNode::NamedNode(n) = shape
622 && self.is_class(shape)
623 {
624 let class = Term::NamedNode(n.clone());
625 if let Some(instances) = self.class_index.get(&class) {
626 nodes.extend(instances.iter().cloned());
627 }
628 }
629 let mut seen = HashSet::new();
630 nodes.retain(|t| seen.insert(t.clone()));
631 nodes
632 }
633
634 fn shape_path(&self, shape: &NamedOrBlankNode) -> (Option<Term>, Option<Path>) {
637 if let Some(cached) = self.path_cache.borrow().get(shape) {
638 return cached.clone();
639 }
640 let path_term = self.shapes.object(shape, vocab::SH_PATH);
641 let parsed = path_term
642 .as_ref()
643 .and_then(|t| parse_path(self.shapes, t).ok());
644 let entry = (path_term, parsed);
645 self.path_cache
646 .borrow_mut()
647 .insert(shape.clone(), entry.clone());
648 entry
649 }
650
651 fn collect(
653 &self,
654 shape: &NamedOrBlankNode,
655 focus: &Term,
656 out: &mut Vec<ValidationResult>,
657 visited: &mut Visited,
658 ) {
659 if self.deactivated(shape) {
660 return; }
662 let key = (shape.clone(), focus.clone());
663 if !visited.insert(key.clone()) {
664 return; }
666
667 let (path_term, parsed) = self.shape_path(shape);
668 let value_nodes: Vec<Term> = match &parsed {
669 Some(p) => succ(self.frozen(), focus, p).into_iter().collect(),
670 None => vec![focus.clone()],
671 };
672 let severity = self.severity(shape);
673 let messages = self.messages(shape);
674 let push = |out: &mut Vec<ValidationResult>, value, component| {
675 out.push(ValidationResult {
676 focus: focus.clone(),
677 path: path_term.clone(),
678 value,
679 component,
680 source_shape: node_term_ref(shape),
681 severity: severity.clone(),
682 messages: messages.clone(),
683 });
684 };
685
686 if parsed.is_some() {
688 if let Some(min) = self.int(shape, vocab::SH_MIN_COUNT)
689 && (value_nodes.len() as u64) < min
690 {
691 push(out, None, vocab::SH_CC_MIN_COUNT.into_owned());
692 }
693 if let Some(max) = self.int(shape, vocab::SH_MAX_COUNT)
694 && (value_nodes.len() as u64) > max
695 {
696 push(out, None, vocab::SH_CC_MAX_COUNT.into_owned());
697 }
698 }
699
700 for hv in self.shapes.objects(shape, vocab::SH_HAS_VALUE) {
702 if !value_nodes.contains(&hv) {
703 push(out, None, vocab::SH_CC_HAS_VALUE.into_owned());
704 }
705 }
706
707 self.collect_closed(shape, focus, &value_nodes, out);
708 self.collect_property_pairs(shape, focus, &path_term, &value_nodes, out);
709 self.collect_unique_lang(shape, focus, &path_term, &value_nodes, out);
710 self.collect_qualified_counts(shape, focus, &path_term, &value_nodes, out, visited);
711
712 for u in &value_nodes {
714 for (component, ok) in self.value_checks(shape, u, visited) {
715 if !ok {
716 push(out, Some(u.clone()), component);
717 }
718 }
719 }
720
721 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
723 if let Some(pn) = term_to_node(&prop) {
724 for u in &value_nodes {
725 self.collect(&pn, u, out, visited);
726 }
727 }
728 }
729
730 self.collect_sparql(shape, focus, &path_term, &parsed, out);
731 self.collect_expression(shape, focus, out, visited);
732 self.collect_components(shape, focus, &path_term, &parsed, &value_nodes, out);
733
734 visited.remove(&key);
735 }
736
737 fn collect_components(
744 &self,
745 shape: &NamedOrBlankNode,
746 focus: &Term,
747 path_term: &Option<Term>,
748 parsed_path: &Option<Path>,
749 value_nodes: &[Term],
750 out: &mut Vec<ValidationResult>,
751 ) {
752 if self.components.is_empty() {
753 return;
754 }
755 let is_property_shape = parsed_path.is_some();
756 for component in &self.components {
757 let mut params: Vec<(String, Term)> = Vec::new();
759 let mut activated = true;
760 for p in &component.params {
761 if let Some(value) = self.shapes.object(shape, p.path.as_ref()) {
762 params.push((p.var.clone(), value));
763 } else if !p.optional {
764 activated = false;
765 break;
766 }
767 }
768 if !activated {
769 continue;
770 }
771
772 let validator = if is_property_shape {
773 component
774 .property_validator
775 .as_ref()
776 .or(component.generic_validator.as_ref())
777 } else {
778 component
779 .node_validator
780 .as_ref()
781 .or(component.generic_validator.as_ref())
782 };
783 let Some(validator) = validator else { continue };
784
785 let mut base = params;
789 base.push(("currentShape".to_string(), node_term_ref(shape)));
790 let path = parsed_path.as_ref();
791
792 match validator.kind {
793 SparqlQueryKind::Ask => {
794 for value in value_nodes {
795 let mut bindings = base.clone();
796 bindings.push(("this".to_string(), focus.clone()));
797 bindings.push(("value".to_string(), value.clone()));
798 let violates = match self.sparql.eval_ask(&validator.query, path, &bindings)
800 {
801 Ok(conforms) => !conforms,
802 Err(_) => true,
803 };
804 if violates {
805 self.push_component_result(
806 out,
807 component,
808 shape,
809 focus,
810 path_term.clone(),
811 Some(value.clone()),
812 &bindings,
813 &validator.messages,
814 );
815 }
816 }
817 }
818 SparqlQueryKind::Select => {
819 let mut bindings = base.clone();
820 bindings.push(("this".to_string(), focus.clone()));
821 match self.sparql.eval_select(&validator.query, path, &bindings) {
822 Ok(rows) => {
823 for row in rows {
824 let value = row
827 .get("value")
828 .cloned()
829 .or_else(|| (!is_property_shape).then(|| focus.clone()));
830 let path = row.get("path").cloned().or_else(|| path_term.clone());
831 let mut binds = bindings.clone();
832 binds.extend(row);
833 self.push_component_result(
834 out,
835 component,
836 shape,
837 focus,
838 path,
839 value,
840 &binds,
841 &validator.messages,
842 );
843 }
844 }
845 Err(_) => self.push_component_result(
846 out,
847 component,
848 shape,
849 focus,
850 path_term.clone(),
851 None,
852 &bindings,
853 &validator.messages,
854 ),
855 }
856 }
857 }
858 }
859 }
860
861 #[allow(clippy::too_many_arguments)]
862 fn push_component_result(
863 &self,
864 out: &mut Vec<ValidationResult>,
865 component: &CustomComponent,
866 shape: &NamedOrBlankNode,
867 focus: &Term,
868 path: Option<Term>,
869 value: Option<Term>,
870 bindings: &[(String, Term)],
871 validator_messages: &[Term],
872 ) {
873 let raw = if validator_messages.is_empty() {
874 self.messages(shape)
875 } else {
876 validator_messages.to_vec()
877 };
878 let bind_map: HashMap<String, Term> = bindings.iter().cloned().collect();
879 let messages = substitute_messages(&raw, focus, &bind_map);
880 out.push(ValidationResult {
881 focus: focus.clone(),
882 path,
883 value,
884 component: component.iri.clone(),
885 source_shape: node_term_ref(shape),
886 severity: self.severity(shape),
887 messages,
888 });
889 }
890
891 fn collect_expression(
898 &self,
899 shape: &NamedOrBlankNode,
900 focus: &Term,
901 out: &mut Vec<ValidationResult>,
902 visited: &mut Visited,
903 ) {
904 for expr_term in self.shapes.objects(shape, vocab::SH_EXPRESSION) {
905 let Some(results) = self.eval_node_expr(&expr_term, focus, visited) else {
906 continue;
907 };
908 for value in results {
909 if is_boolean_true(&value) {
910 continue;
911 }
912 out.push(ValidationResult {
913 focus: focus.clone(),
914 path: None,
915 value: Some(value),
916 component: vocab::SH_CC_EXPRESSION.into_owned(),
917 source_shape: node_term_ref(shape),
918 severity: self.severity(shape),
919 messages: self.messages(shape),
920 });
921 }
922 }
923 }
924
925 fn eval_node_expr(
930 &self,
931 term: &Term,
932 focus: &Term,
933 visited: &mut Visited,
934 ) -> Option<Vec<Term>> {
935 match term {
936 Term::NamedNode(n) if n.as_ref() == vocab::SH_THIS => Some(vec![focus.clone()]),
937 Term::NamedNode(_) | Term::Literal(_) => Some(vec![term.clone()]),
938 Term::BlankNode(_) => {
939 let node = term_to_node(term)?;
940 if let Some(path_term) = self.shapes.object(&node, vocab::SH_PATH) {
941 let path = parse_path(self.shapes, &path_term).ok()?;
942 Some(succ(self.frozen(), focus, &path).into_iter().collect())
943 } else if let Some(filter_shape) = self.shapes.object(&node, vocab::SH_FILTER_SHAPE)
944 {
945 let filter_shape = term_to_node(&filter_shape)?;
946 let nodes_term = self.shapes.object(&node, vocab::SH_NODES)?;
947 let inputs = self.eval_node_expr(&nodes_term, focus, visited)?;
948 Some(
949 inputs
950 .into_iter()
951 .filter(|x| self.conforms(&filter_shape, x, visited))
952 .collect(),
953 )
954 } else if let Some(list) = self.shapes.object(&node, vocab::SH_INTERSECTION) {
955 self.eval_node_expr_set(&list, focus, visited, true)
956 } else if let Some(list) = self.shapes.object(&node, vocab::SH_UNION) {
957 self.eval_node_expr_set(&list, focus, visited, false)
958 } else {
959 None
961 }
962 }
963 }
964 }
965
966 fn eval_node_expr_set(
970 &self,
971 list_head: &Term,
972 focus: &Term,
973 visited: &mut Visited,
974 intersect: bool,
975 ) -> Option<Vec<Term>> {
976 let members = self.shapes.read_list(list_head);
977 if members.is_empty() {
978 return None;
979 }
980 let mut iter = members.iter();
981 let mut acc = self.eval_node_expr(iter.next().unwrap(), focus, visited)?;
982 for member in iter {
983 let next = self.eval_node_expr(member, focus, visited)?;
984 if intersect {
985 acc.retain(|x| next.contains(x));
986 } else {
987 for t in next {
988 if !acc.contains(&t) {
989 acc.push(t);
990 }
991 }
992 }
993 }
994 Some(acc)
995 }
996
997 fn build_sparql_constraint(
1006 &self,
1007 shape: &NamedOrBlankNode,
1008 constraint_node: &NamedOrBlankNode,
1009 parsed_path: &Option<Path>,
1010 ) -> Option<SparqlConstraint> {
1011 let (kind, raw) = if let Some(Term::Literal(query)) =
1012 self.shapes.object(constraint_node, vocab::SH_SELECT)
1013 {
1014 (SparqlQueryKind::Select, query.value().to_string())
1015 } else if let Some(Term::Literal(query)) =
1016 self.shapes.object(constraint_node, vocab::SH_ASK)
1017 {
1018 (SparqlQueryKind::Ask, query.value().to_string())
1019 } else {
1020 return None;
1021 };
1022 let (_, query) = canonical_sparql_query(self.shapes, constraint_node, &raw).ok()?;
1023 Some(SparqlConstraint {
1024 kind,
1025 query,
1026 path: parsed_path.clone(),
1027 shape: Some(node_term_ref(shape)),
1028 messages: Vec::new(),
1031 })
1032 }
1033
1034 fn prefetch_sparql(&self, shape: &NamedOrBlankNode, foci: &[Term]) {
1038 if !self.needs_sparql || foci.len() < 2 {
1039 return;
1040 }
1041 let (_, parsed_path) = self.shape_path(shape);
1042 for constraint_term in self.shapes.objects(shape, vocab::SH_SPARQL) {
1043 let Some(constraint_node) = term_to_node(&constraint_term) else {
1044 continue;
1045 };
1046 if let Some(constraint) =
1047 self.build_sparql_constraint(shape, &constraint_node, &parsed_path)
1048 {
1049 let _ = self.sparql.prefetch_constraint(&constraint, foci);
1050 }
1051 }
1052 }
1053
1054 fn collect_sparql(
1055 &self,
1056 shape: &NamedOrBlankNode,
1057 focus: &Term,
1058 path_term: &Option<Term>,
1059 parsed_path: &Option<Path>,
1060 out: &mut Vec<ValidationResult>,
1061 ) {
1062 if !self.needs_sparql {
1063 return;
1064 }
1065 let sparql = &self.sparql;
1066 let severity = self.severity(shape);
1067 for constraint_term in self.shapes.objects(shape, vocab::SH_SPARQL) {
1068 let Some(constraint_node) = term_to_node(&constraint_term) else {
1069 continue;
1070 };
1071 let Some(constraint) =
1072 self.build_sparql_constraint(shape, &constraint_node, parsed_path)
1073 else {
1074 continue;
1075 };
1076 let raw_messages = {
1079 let on_constraint = self.shapes.objects(&constraint_node, vocab::SH_MESSAGE);
1080 if on_constraint.is_empty() {
1081 self.messages(shape)
1082 } else {
1083 on_constraint
1084 }
1085 };
1086 match sparql.constraint_violations(&constraint, focus) {
1087 Ok(violations) => {
1088 for violation in violations {
1089 let messages =
1090 substitute_messages(&raw_messages, focus, &violation.bindings);
1091 let value = violation.value.or_else(|| match constraint.kind {
1094 SparqlQueryKind::Select => Some(focus.clone()),
1095 SparqlQueryKind::Ask => None,
1096 });
1097 out.push(ValidationResult {
1098 focus: focus.clone(),
1099 path: violation.path.or_else(|| path_term.clone()),
1100 value,
1101 component: vocab::SH_CC_SPARQL.into_owned(),
1102 source_shape: node_term_ref(shape),
1103 severity: severity.clone(),
1104 messages,
1105 });
1106 }
1107 }
1108 Err(error) => {
1112 let mut messages = raw_messages;
1113 messages.push(Term::Literal(Literal::new_simple_literal(format!(
1114 "SPARQL constraint evaluation failed: {error}"
1115 ))));
1116 out.push(ValidationResult {
1117 focus: focus.clone(),
1118 path: path_term.clone(),
1119 value: None,
1120 component: vocab::SH_CC_SPARQL.into_owned(),
1121 source_shape: node_term_ref(shape),
1122 severity: severity.clone(),
1123 messages,
1124 });
1125 }
1126 }
1127 }
1128 }
1129
1130 fn collect_closed(
1131 &self,
1132 shape: &NamedOrBlankNode,
1133 focus: &Term,
1134 value_nodes: &[Term],
1135 out: &mut Vec<ValidationResult>,
1136 ) {
1137 if !self.bool(shape, vocab::SH_CLOSED) {
1138 return;
1139 }
1140 let mut allowed = HashSet::new();
1141 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
1142 let Some(prop) = term_to_node(&prop) else {
1143 continue;
1144 };
1145 if let Some(Term::NamedNode(path)) = self.shapes.object(&prop, vocab::SH_PATH) {
1146 allowed.insert(path);
1147 }
1148 }
1149 for list in self.shapes.objects(shape, vocab::SH_IGNORED_PROPERTIES) {
1150 for term in self.shapes.read_list(&list) {
1151 if let Term::NamedNode(predicate) = term {
1152 allowed.insert(predicate);
1153 }
1154 }
1155 }
1156 for value_node in value_nodes {
1157 for (predicate, object) in self.frozen().outgoing(value_node) {
1158 if allowed.contains(&predicate) {
1159 continue;
1160 }
1161 out.push(ValidationResult {
1162 focus: focus.clone(),
1163 path: Some(Term::NamedNode(predicate)),
1164 value: Some(object),
1165 component: vocab::SH_CC_CLOSED.into_owned(),
1166 source_shape: node_term_ref(shape),
1167 severity: self.severity(shape),
1168 messages: self.messages(shape),
1169 });
1170 }
1171 }
1172 }
1173
1174 fn collect_property_pairs(
1175 &self,
1176 shape: &NamedOrBlankNode,
1177 focus: &Term,
1178 path: &Option<Term>,
1179 value_nodes: &[Term],
1180 out: &mut Vec<ValidationResult>,
1181 ) {
1182 for predicate in self.shapes.objects(shape, vocab::SH_EQUALS) {
1183 let Term::NamedNode(predicate) = predicate else {
1184 continue;
1185 };
1186 let other = succ(self.frozen(), focus, &Path::Pred(predicate));
1187 for value in value_nodes.iter().filter(|value| !other.contains(*value)) {
1188 self.push(
1189 out,
1190 shape,
1191 focus,
1192 path.clone(),
1193 Some((*value).clone()),
1194 vocab::SH_CC_EQUALS,
1195 );
1196 }
1197 for value in other.iter().filter(|value| !value_nodes.contains(*value)) {
1198 self.push(
1199 out,
1200 shape,
1201 focus,
1202 path.clone(),
1203 Some(value.clone()),
1204 vocab::SH_CC_EQUALS,
1205 );
1206 }
1207 }
1208 for predicate in self.shapes.objects(shape, vocab::SH_DISJOINT) {
1209 let Term::NamedNode(predicate) = predicate else {
1210 continue;
1211 };
1212 let other = succ(self.frozen(), focus, &Path::Pred(predicate));
1213 for value in value_nodes.iter().filter(|value| other.contains(*value)) {
1214 self.push(
1215 out,
1216 shape,
1217 focus,
1218 path.clone(),
1219 Some((*value).clone()),
1220 vocab::SH_CC_DISJOINT,
1221 );
1222 }
1223 }
1224 for (constraint, component, inclusive) in [
1225 (vocab::SH_LESS_THAN, vocab::SH_CC_LESS_THAN, false),
1226 (
1227 vocab::SH_LESS_THAN_OR_EQUALS,
1228 vocab::SH_CC_LESS_THAN_OR_EQUALS,
1229 true,
1230 ),
1231 ] {
1232 for predicate in self.shapes.objects(shape, constraint) {
1233 let Term::NamedNode(predicate) = predicate else {
1234 continue;
1235 };
1236 for left in value_nodes {
1237 for right in succ(self.frozen(), focus, &Path::Pred(predicate.clone())) {
1238 let ordering = compare_terms(left, &right);
1239 let passes = ordering == Some(Ordering::Less)
1240 || inclusive && ordering == Some(Ordering::Equal);
1241 if !passes {
1242 self.push(
1243 out,
1244 shape,
1245 focus,
1246 path.clone(),
1247 Some(left.clone()),
1248 component,
1249 );
1250 }
1251 }
1252 }
1253 }
1254 }
1255 }
1256
1257 fn collect_unique_lang(
1258 &self,
1259 shape: &NamedOrBlankNode,
1260 focus: &Term,
1261 path: &Option<Term>,
1262 value_nodes: &[Term],
1263 out: &mut Vec<ValidationResult>,
1264 ) {
1265 if !self.bool(shape, vocab::SH_UNIQUE_LANG) {
1266 return;
1267 }
1268 let mut counts = HashMap::new();
1269 for value in value_nodes {
1270 if let Term::Literal(literal) = value
1271 && let Some(language) = literal.language()
1272 {
1273 *counts
1274 .entry(language.to_ascii_lowercase())
1275 .or_insert(0usize) += 1;
1276 }
1277 }
1278 for _ in counts.values().filter(|count| **count > 1) {
1279 self.push(
1280 out,
1281 shape,
1282 focus,
1283 path.clone(),
1284 None,
1285 vocab::SH_CC_UNIQUE_LANG,
1286 );
1287 }
1288 }
1289
1290 fn collect_qualified_counts(
1291 &self,
1292 shape: &NamedOrBlankNode,
1293 focus: &Term,
1294 path: &Option<Term>,
1295 value_nodes: &[Term],
1296 out: &mut Vec<ValidationResult>,
1297 visited: &mut Visited,
1298 ) {
1299 for qualifier in self.shapes.objects(shape, vocab::SH_QUALIFIED_VALUE_SHAPE) {
1300 let Some(qualifier) = term_to_node(&qualifier) else {
1301 continue;
1302 };
1303 let siblings = if self.bool(shape, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
1304 self.sibling_qualified_shapes(shape, &qualifier)
1305 } else {
1306 Vec::new()
1307 };
1308 let count = value_nodes
1309 .iter()
1310 .filter(|value| {
1311 self.conforms(&qualifier, value, visited)
1312 && siblings
1313 .iter()
1314 .all(|sibling| !self.conforms(sibling, value, visited))
1315 })
1316 .count() as u64;
1317 if let Some(min) = self.int(shape, vocab::SH_QUALIFIED_MIN_COUNT)
1318 && count < min
1319 {
1320 self.push(
1321 out,
1322 shape,
1323 focus,
1324 path.clone(),
1325 None,
1326 vocab::SH_CC_QUALIFIED_MIN_COUNT,
1327 );
1328 }
1329 if let Some(max) = self.int(shape, vocab::SH_QUALIFIED_MAX_COUNT)
1330 && count > max
1331 {
1332 self.push(
1333 out,
1334 shape,
1335 focus,
1336 path.clone(),
1337 None,
1338 vocab::SH_CC_QUALIFIED_MAX_COUNT,
1339 );
1340 }
1341 }
1342 }
1343
1344 fn sibling_qualified_shapes(
1345 &self,
1346 shape: &NamedOrBlankNode,
1347 qualifier: &NamedOrBlankNode,
1348 ) -> Vec<NamedOrBlankNode> {
1349 let shape_term = node_term_ref(shape);
1350 let mut siblings = HashSet::new();
1351 for triple in self.shapes.graph.triples_for_predicate(vocab::SH_PROPERTY) {
1352 if triple.object != shape_term.as_ref() {
1353 continue;
1354 }
1355 let parent = triple.subject.into_owned();
1356 for property in self.shapes.objects(&parent, vocab::SH_PROPERTY) {
1357 let Some(property) = term_to_node(&property) else {
1358 continue;
1359 };
1360 for qualifier in self
1361 .shapes
1362 .objects(&property, vocab::SH_QUALIFIED_VALUE_SHAPE)
1363 {
1364 if let Some(qualifier) = term_to_node(&qualifier) {
1365 siblings.insert(qualifier);
1366 }
1367 }
1368 }
1369 }
1370 siblings.remove(qualifier);
1371 siblings.into_iter().collect()
1372 }
1373
1374 fn push(
1375 &self,
1376 out: &mut Vec<ValidationResult>,
1377 shape: &NamedOrBlankNode,
1378 focus: &Term,
1379 path: Option<Term>,
1380 value: Option<Term>,
1381 component: NamedNodeRef<'static>,
1382 ) {
1383 let mut bindings = HashMap::new();
1384 if let Some(v) = &value {
1385 bindings.insert("value".to_string(), v.clone());
1386 }
1387 if let Some(p) = &path {
1388 bindings.insert("path".to_string(), p.clone());
1389 }
1390 let raw = self.messages(shape);
1391 let messages = substitute_messages(&raw, focus, &bindings);
1392 out.push(ValidationResult {
1393 focus: focus.clone(),
1394 path,
1395 value,
1396 component: component.into_owned(),
1397 source_shape: node_term_ref(shape),
1398 severity: self.severity(shape),
1399 messages,
1400 });
1401 }
1402
1403 fn messages(&self, shape: &NamedOrBlankNode) -> Vec<Term> {
1405 self.shapes.objects(shape, vocab::SH_MESSAGE)
1406 }
1407
1408 fn conforms(&self, shape: &NamedOrBlankNode, focus: &Term, visited: &mut Visited) -> bool {
1409 let mut scratch = Vec::new();
1410 self.collect(shape, focus, &mut scratch, visited);
1411 scratch.is_empty()
1412 }
1413
1414 fn value_checks(
1417 &self,
1418 shape: &NamedOrBlankNode,
1419 u: &Term,
1420 visited: &mut Visited,
1421 ) -> Vec<(NamedNode, bool)> {
1422 let mut checks = Vec::new();
1423
1424 for c in self.shapes.objects(shape, vocab::SH_CLASS) {
1425 checks.push((vocab::SH_CC_CLASS.into_owned(), self.is_instance(u, &c)));
1426 }
1427 for d in self.shapes.objects(shape, vocab::SH_DATATYPE) {
1428 if let Term::NamedNode(dt) = d {
1429 let ok = value_type_holds(&ValueType::Datatype(dt), u);
1430 checks.push((vocab::SH_CC_DATATYPE.into_owned(), ok));
1431 }
1432 }
1433 for k in self.shapes.objects(shape, vocab::SH_NODE_KIND) {
1434 if let Some(set) = map_node_kind(&k) {
1435 checks.push((vocab::SH_CC_NODE_KIND.into_owned(), set.matches(u)));
1436 }
1437 }
1438 for (pred_iri, comp, inclusive) in [
1440 (vocab::SH_MIN_INCLUSIVE, vocab::SH_CC_MIN_INCLUSIVE, true),
1441 (vocab::SH_MIN_EXCLUSIVE, vocab::SH_CC_MIN_EXCLUSIVE, false),
1442 ] {
1443 if let Some(Term::Literal(b)) = self.shapes.object(shape, pred_iri) {
1444 let vt = ValueType::NumericRange {
1445 lo: Some(Bound {
1446 value: b,
1447 inclusive,
1448 }),
1449 hi: None,
1450 };
1451 checks.push((comp.into_owned(), value_type_holds(&vt, u)));
1452 }
1453 }
1454 for (pred_iri, comp, inclusive) in [
1455 (vocab::SH_MAX_INCLUSIVE, vocab::SH_CC_MAX_INCLUSIVE, true),
1456 (vocab::SH_MAX_EXCLUSIVE, vocab::SH_CC_MAX_EXCLUSIVE, false),
1457 ] {
1458 if let Some(Term::Literal(b)) = self.shapes.object(shape, pred_iri) {
1459 let vt = ValueType::NumericRange {
1460 lo: None,
1461 hi: Some(Bound {
1462 value: b,
1463 inclusive,
1464 }),
1465 };
1466 checks.push((comp.into_owned(), value_type_holds(&vt, u)));
1467 }
1468 }
1469 let min_len = self.int(shape, vocab::SH_MIN_LENGTH);
1471 let max_len = self.int(shape, vocab::SH_MAX_LENGTH);
1472 if let Some(m) = min_len {
1473 let vt = ValueType::Length {
1474 min: Some(m),
1475 max: None,
1476 };
1477 checks.push((
1478 vocab::SH_CC_MIN_LENGTH.into_owned(),
1479 value_type_holds(&vt, u),
1480 ));
1481 }
1482 if let Some(m) = max_len {
1483 let vt = ValueType::Length {
1484 min: None,
1485 max: Some(m),
1486 };
1487 checks.push((
1488 vocab::SH_CC_MAX_LENGTH.into_owned(),
1489 value_type_holds(&vt, u),
1490 ));
1491 }
1492 if let Some(Term::Literal(re)) = self.shapes.object(shape, vocab::SH_PATTERN) {
1493 let flags = match self.shapes.object(shape, vocab::SH_FLAGS) {
1494 Some(Term::Literal(f)) => f.value().to_string(),
1495 _ => String::new(),
1496 };
1497 let vt = ValueType::Pattern {
1498 regex: re.value().to_string(),
1499 flags,
1500 };
1501 checks.push((vocab::SH_CC_PATTERN.into_owned(), value_type_holds(&vt, u)));
1502 }
1503 for list in self.shapes.objects(shape, vocab::SH_IN) {
1505 let members = self.shapes.read_list(&list);
1506 checks.push((vocab::SH_CC_IN.into_owned(), members.contains(u)));
1507 }
1508 for list in self.shapes.objects(shape, vocab::SH_LANGUAGE_IN) {
1509 let languages = self
1510 .shapes
1511 .read_list(&list)
1512 .into_iter()
1513 .filter_map(|term| match term {
1514 Term::Literal(literal) => Some(literal.value().to_string()),
1515 _ => None,
1516 })
1517 .collect();
1518 checks.push((
1519 vocab::SH_CC_LANGUAGE_IN.into_owned(),
1520 value_type_holds(&ValueType::LangIn(languages), u),
1521 ));
1522 }
1523
1524 for list in self.shapes.objects(shape, vocab::SH_AND) {
1526 let ok = self
1527 .shapes
1528 .read_list(&list)
1529 .iter()
1530 .filter_map(term_to_node)
1531 .all(|m| self.conforms(&m, u, visited));
1532 checks.push((vocab::SH_CC_AND.into_owned(), ok));
1533 }
1534 for list in self.shapes.objects(shape, vocab::SH_OR) {
1535 let ok = self
1536 .shapes
1537 .read_list(&list)
1538 .iter()
1539 .filter_map(term_to_node)
1540 .any(|m| self.conforms(&m, u, visited));
1541 checks.push((vocab::SH_CC_OR.into_owned(), ok));
1542 }
1543 for list in self.shapes.objects(shape, vocab::SH_XONE) {
1544 let count = self
1545 .shapes
1546 .read_list(&list)
1547 .iter()
1548 .filter_map(term_to_node)
1549 .filter(|m| self.conforms(m, u, visited))
1550 .count();
1551 checks.push((vocab::SH_CC_XONE.into_owned(), count == 1));
1552 }
1553 for n in self.shapes.objects(shape, vocab::SH_NOT) {
1554 if let Some(nn) = term_to_node(&n) {
1555 checks.push((
1556 vocab::SH_CC_NOT.into_owned(),
1557 !self.conforms(&nn, u, visited),
1558 ));
1559 }
1560 }
1561 for n in self.shapes.objects(shape, vocab::SH_NODE) {
1562 if let Some(nn) = term_to_node(&n) {
1563 checks.push((
1564 vocab::SH_CC_NODE.into_owned(),
1565 self.conforms(&nn, u, visited),
1566 ));
1567 }
1568 }
1569
1570 checks
1571 }
1572
1573 fn is_instance(&self, u: &Term, class: &Term) -> bool {
1574 succ(self.frozen(), u, &class_path()).contains(class)
1575 }
1576
1577 fn int(&self, s: &NamedOrBlankNode, p: NamedNodeRef) -> Option<u64> {
1578 match self.shapes.object(s, p) {
1579 Some(Term::Literal(l)) => l.value().parse().ok(),
1580 _ => None,
1581 }
1582 }
1583
1584 fn bool(&self, s: &NamedOrBlankNode, p: NamedNodeRef) -> bool {
1585 matches!(
1586 self.shapes.object(s, p),
1587 Some(Term::Literal(ref literal)) if matches!(literal.value(), "true" | "1")
1588 )
1589 }
1590
1591 fn severity(&self, shape: &NamedOrBlankNode) -> NamedNode {
1594 match self.shapes.object(shape, vocab::SH_SEVERITY) {
1595 Some(Term::NamedNode(n)) => n,
1596 _ => vocab::SH_VIOLATION.into_owned(),
1597 }
1598 }
1599}
1600
1601fn is_shape_node(shapes: &Loaded, node: &NamedOrBlankNode) -> bool {
1602 shapes.has_type(node, vocab::SH_NODE_SHAPE)
1603 || shapes.has_type(node, vocab::SH_PROPERTY_SHAPE)
1604 || [
1605 vocab::SH_PROPERTY,
1606 vocab::SH_NODE,
1607 vocab::SH_AND,
1608 vocab::SH_OR,
1609 vocab::SH_NOT,
1610 vocab::SH_XONE,
1611 vocab::SH_DATATYPE,
1612 vocab::SH_CLASS,
1613 vocab::SH_NODE_KIND,
1614 vocab::SH_IN,
1615 vocab::SH_HAS_VALUE,
1616 vocab::SH_PROPERTY,
1617 ]
1618 .iter()
1619 .any(|predicate| shapes.object(node, *predicate).is_some())
1620}
1621
1622fn shapes_reference_shapes_graph(shapes: &Loaded) -> bool {
1625 [vocab::SH_SELECT, vocab::SH_ASK].iter().any(|predicate| {
1626 shapes.graph.triples_for_predicate(*predicate).any(
1627 |t| matches!(t.object, oxrdf::TermRef::Literal(l) if l.value().contains("shapesGraph")),
1628 )
1629 })
1630}
1631
1632fn class_path() -> Path {
1633 Path::seq(vec![
1634 Path::Pred(vocab::rdf_type()),
1635 Path::star(Path::Pred(vocab::rdfs_subclassof())),
1636 ])
1637}
1638
1639fn build_class_index(
1648 focus_data: &Graph,
1649 frozen: &FrozenIndexedDataset,
1650) -> HashMap<Term, Vec<Term>> {
1651 let focus_nodes = graph_nodes(focus_data);
1652 let subclass_star = Path::star(Path::Pred(vocab::rdfs_subclassof()));
1653 let mut supers: HashMap<Term, Vec<Term>> = HashMap::new();
1654 let mut index: HashMap<Term, Vec<Term>> = HashMap::new();
1655 let mut seen: HashSet<(Term, Term)> = HashSet::new();
1656 for (node, ty) in frozen.triples_for_predicate(&vocab::rdf_type()) {
1657 if !focus_nodes.contains(&node) {
1658 continue;
1659 }
1660 let classes = supers
1661 .entry(ty.clone())
1662 .or_insert_with(|| succ(frozen, &ty, &subclass_star).into_iter().collect());
1663 for class in classes.iter() {
1664 if seen.insert((class.clone(), node.clone())) {
1665 index.entry(class.clone()).or_default().push(node.clone());
1666 }
1667 }
1668 }
1669 index
1670}
1671
1672fn graph_nodes(graph: &Graph) -> HashSet<Term> {
1673 let mut nodes = HashSet::new();
1674 for triple in graph.iter() {
1675 nodes.insert(node_term(triple.subject));
1676 nodes.insert(triple.object.into_owned());
1677 }
1678 nodes
1679}
1680
1681fn node_term(s: oxrdf::NamedOrBlankNodeRef) -> Term {
1682 crate::path::term_of(s.into_owned())
1683}
1684
1685fn node_term_ref(s: &NamedOrBlankNode) -> Term {
1686 match s {
1687 NamedOrBlankNode::NamedNode(n) => Term::NamedNode(n.clone()),
1688 NamedOrBlankNode::BlankNode(b) => Term::BlankNode(b.clone()),
1689 }
1690}
1691
1692fn map_node_kind(term: &Term) -> Option<NodeKindSet> {
1693 let Term::NamedNode(n) = term else {
1694 return None;
1695 };
1696 let r = n.as_ref();
1697 Some(if r == vocab::SH_IRI {
1698 NodeKindSet::IRI
1699 } else if r == vocab::SH_BLANK_NODE {
1700 NodeKindSet::BLANK_NODE
1701 } else if r == vocab::SH_LITERAL {
1702 NodeKindSet::LITERAL
1703 } else if r == vocab::SH_BLANK_NODE_OR_IRI {
1704 NodeKindSet::BLANK_NODE_OR_IRI
1705 } else if r == vocab::SH_BLANK_NODE_OR_LITERAL {
1706 NodeKindSet::BLANK_NODE_OR_LITERAL
1707 } else if r == vocab::SH_IRI_OR_LITERAL {
1708 NodeKindSet::IRI_OR_LITERAL
1709 } else {
1710 return None;
1711 })
1712}