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