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
56#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct PropertyWitness {
68 pub focus: Term,
69 pub shape: Term,
71 pub key: Term,
72 pub values: Vec<Term>,
78}
79
80pub fn validate_report(shapes: &Loaded, data: &Graph) -> ValidationReport {
82 validate_report_with_options(shapes, data, &ValidationOptions::default())
83}
84
85pub fn evaluate_function_expression(shapes: &Loaded, expr: &str) -> Result<Option<Term>, String> {
91 let frozen = FrozenIndexedDataset::from_graph(&shapes.graph);
92 let mut sparql = SparqlExecutor::from_frozen(frozen, false);
93 sparql.set_functions(collect_functions(shapes), UnsupportedPolicy::Ignore);
96 let mut prologue = String::new();
97 if let Some(base) = &shapes.base {
98 prologue.push_str(&format!("BASE <{base}>\n"));
99 }
100 for (prefix, namespace) in &shapes.prefixes {
101 prologue.push_str(&format!("PREFIX {prefix}: <{namespace}>\n"));
102 }
103 sparql.evaluate_expression(&prologue, expr)
104}
105
106pub fn validate_report_with_options(
108 shapes: &Loaded,
109 data: &Graph,
110 options: &ValidationOptions,
111) -> ValidationReport {
112 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
113 let frozen = if has_shapes_graph {
114 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
115 } else {
116 FrozenIndexedDataset::from_graph(data)
117 };
118 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
119}
120
121pub fn validate_report_graphs(shapes: &Loaded, data: &Graph) -> ValidationReport {
123 validate_report_graphs_with_mode_and_options(
124 shapes,
125 data,
126 ValidationGraphMode::default(),
127 &ValidationOptions::default(),
128 )
129}
130
131pub fn validate_report_graphs_with_mode(
133 shapes: &Loaded,
134 data: &Graph,
135 mode: ValidationGraphMode,
136) -> ValidationReport {
137 validate_report_graphs_with_mode_and_options(shapes, data, mode, &ValidationOptions::default())
138}
139
140pub fn validate_report_graphs_with_mode_and_options(
142 shapes: &Loaded,
143 data: &Graph,
144 mode: ValidationGraphMode,
145 options: &ValidationOptions,
146) -> ValidationReport {
147 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
148 match mode {
149 ValidationGraphMode::Data => {
150 let frozen = if has_shapes_graph {
151 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
152 } else {
153 FrozenIndexedDataset::from_graph(data)
154 };
155 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
156 }
157 ValidationGraphMode::Union => {
158 let frozen = if has_shapes_graph {
159 FrozenIndexedDataset::from_graph_union_with_shapes(data, &shapes.graph)
160 } else {
161 FrozenIndexedDataset::from_graph_union(data, &shapes.graph)
162 };
163 validate_report_context(shapes, data, frozen, has_shapes_graph, options)
164 }
165 ValidationGraphMode::UnionAll => {
166 let union = graph_union(data, &shapes.graph);
167 let frozen = if has_shapes_graph {
168 FrozenIndexedDataset::from_graphs(&union, &shapes.graph)
169 } else {
170 FrozenIndexedDataset::from_graph(&union)
171 };
172 validate_report_context(shapes, &union, frozen, has_shapes_graph, options)
173 }
174 }
175}
176
177pub fn property_witnesses_graphs_with_mode(
185 shapes: &Loaded,
186 data: &Graph,
187 mode: ValidationGraphMode,
188 key_path: Option<&Path>,
189) -> Vec<PropertyWitness> {
190 property_witnesses_graphs_with_mode_and_options(
191 shapes,
192 data,
193 mode,
194 key_path,
195 &ValidationOptions::default(),
196 )
197}
198
199pub fn property_witnesses_graphs_with_mode_and_options(
203 shapes: &Loaded,
204 data: &Graph,
205 mode: ValidationGraphMode,
206 key_path: Option<&Path>,
207 options: &ValidationOptions,
208) -> Vec<PropertyWitness> {
209 let has_shapes_graph = shapes_reference_shapes_graph(shapes);
210 let (focus_data, frozen, union_owner);
211 match mode {
212 ValidationGraphMode::Data => {
213 frozen = if has_shapes_graph {
214 FrozenIndexedDataset::from_graphs(data, &shapes.graph)
215 } else {
216 FrozenIndexedDataset::from_graph(data)
217 };
218 focus_data = data;
219 }
220 ValidationGraphMode::Union => {
221 frozen = if has_shapes_graph {
222 FrozenIndexedDataset::from_graph_union_with_shapes(data, &shapes.graph)
223 } else {
224 FrozenIndexedDataset::from_graph_union(data, &shapes.graph)
225 };
226 focus_data = data;
227 }
228 ValidationGraphMode::UnionAll => {
229 union_owner = graph_union(data, &shapes.graph);
230 frozen = if has_shapes_graph {
231 FrozenIndexedDataset::from_graphs(&union_owner, &shapes.graph)
232 } else {
233 FrozenIndexedDataset::from_graph(&union_owner)
234 };
235 focus_data = &union_owner;
236 }
237 }
238 let r = build_reporter(shapes, focus_data, frozen, has_shapes_graph, options);
239 let mut out = Vec::new();
240 for shape in r.target_shapes() {
241 let foci = r.focus_nodes(&shape);
242 r.prefetch_sparql(&shape, &foci);
243 for focus in &foci {
244 let mut check = HashSet::new();
245 let mut results = Vec::new();
246 r.collect(&shape, focus, &mut results, &mut check, &[]);
247 let conforms = !results.iter().any(|result| {
248 Severity::from_named_node(result.severity.clone()).meets(&options.minimum_severity)
249 });
250 if !conforms {
251 continue;
252 }
253 let mut visited = HashSet::new();
254 r.collect_property_witnesses(&shape, focus, &shape, key_path, &mut visited, &mut out);
255 }
256 }
257 out
258}
259
260fn build_reporter<'a>(
264 shapes: &'a Loaded,
265 focus_data: &'a Graph,
266 frozen: FrozenIndexedDataset,
267 has_shapes_graph: bool,
268 options: &ValidationOptions,
269) -> Reporter<'a> {
270 let needs_sparql = shapes
273 .graph
274 .triples_for_predicate(vocab::SH_SPARQL)
275 .next()
276 .is_some()
277 || shapes
278 .graph
279 .triples_for_predicate(vocab::SH_TARGET)
280 .next()
281 .is_some();
282 let mut sparql = SparqlExecutor::from_frozen(frozen, needs_sparql && has_shapes_graph);
283 sparql.set_functions(collect_functions(shapes), options.engine.unsupported);
284 let has_explicit_class_target = shapes
288 .graph
289 .triples_for_predicate(vocab::SH_TARGET_CLASS)
290 .next()
291 .is_some();
292 let has_implicit_class_target = shapes.graph.iter().any(|triple| {
293 let subject = triple.subject.into_owned();
294 is_shape_node(shapes, &subject)
295 && (shapes.is_instance_of(&subject, vocab::RDFS_CLASS)
296 || shapes.is_instance_of(&subject, vocab::OWL_CLASS))
297 });
298 let needs_class_index = has_explicit_class_target || has_implicit_class_target;
299 let class_index = if needs_class_index {
300 build_class_index(
301 focus_data,
302 sparql
303 .frozen()
304 .expect("report validation always has a frozen dataset"),
305 )
306 } else {
307 HashMap::new()
308 };
309 Reporter {
310 shapes,
311 focus_data,
312 sparql,
313 needs_sparql,
314 class_index,
315 path_cache: RefCell::new(HashMap::new()),
316 components: build_components(shapes, options.engine.unsupported),
317 }
318}
319
320fn validate_report_context(
321 shapes: &Loaded,
322 focus_data: &Graph,
323 frozen: FrozenIndexedDataset,
324 has_shapes_graph: bool,
325 options: &ValidationOptions,
326) -> ValidationReport {
327 let r = build_reporter(shapes, focus_data, frozen, has_shapes_graph, options);
328 let mut results = Vec::new();
329 for shape in r.target_shapes() {
330 let foci = r.focus_nodes(&shape);
331 r.prefetch_sparql(&shape, &foci);
332 for focus in &foci {
333 let mut visited = HashSet::new();
334 r.collect(&shape, focus, &mut results, &mut visited, &[]);
335 }
336 }
337 if options.sort_results {
338 results.sort_by(|left, right| {
339 Severity::from_named_node(right.severity.clone())
340 .rank()
341 .cmp(&Severity::from_named_node(left.severity.clone()).rank())
342 .then_with(|| left.focus.to_string().cmp(&right.focus.to_string()))
343 .then_with(|| {
344 left.source_shape
345 .to_string()
346 .cmp(&right.source_shape.to_string())
347 })
348 .then_with(|| left.component.as_str().cmp(right.component.as_str()))
349 });
350 }
351 ValidationReport {
352 conforms: !results.iter().any(|result| {
353 Severity::from_named_node(result.severity.clone()).meets(&options.minimum_severity)
354 }),
355 results,
356 }
357}
358
359pub fn report_to_graph(report: &ValidationReport) -> Graph {
361 let mut g = Graph::new();
362 let root = BlankNode::default();
363 let t = |s: NamedOrBlankNode, p: NamedNodeRef, o: Term| Triple::new(s, p.into_owned(), o);
364
365 g.insert(&t(
366 root.clone().into(),
367 vocab::RDF_TYPE,
368 vocab::SH_VALIDATION_REPORT.into_owned().into(),
369 ));
370 g.insert(&t(
371 root.clone().into(),
372 vocab::SH_CONFORMS,
373 Literal::from(report.conforms).into(),
374 ));
375
376 for r in &report.results {
377 let rn = BlankNode::default();
378 g.insert(&t(root.clone().into(), vocab::SH_RESULT, rn.clone().into()));
379 g.insert(&t(
380 rn.clone().into(),
381 vocab::RDF_TYPE,
382 vocab::SH_VALIDATION_RESULT.into_owned().into(),
383 ));
384 g.insert(&t(rn.clone().into(), vocab::SH_FOCUS_NODE, r.focus.clone()));
385 if let Some(path) = &r.path {
386 g.insert(&t(rn.clone().into(), vocab::SH_RESULT_PATH, path.clone()));
387 }
388 if let Some(value) = &r.value {
389 g.insert(&t(rn.clone().into(), vocab::SH_VALUE, value.clone()));
390 }
391 g.insert(&t(
392 rn.clone().into(),
393 vocab::SH_RESULT_SEVERITY,
394 r.severity.clone().into(),
395 ));
396 g.insert(&t(
397 rn.clone().into(),
398 vocab::SH_SOURCE_CONSTRAINT_COMPONENT,
399 r.component.clone().into(),
400 ));
401 for msg in &r.messages {
402 g.insert(&t(rn.clone().into(), vocab::SH_RESULT_MESSAGE, msg.clone()));
403 }
404 g.insert(&t(
405 rn.into(),
406 vocab::SH_SOURCE_SHAPE,
407 r.source_shape.clone(),
408 ));
409 }
410 g
411}
412
413fn substitute_messages(
420 messages: &[Term],
421 focus: &Term,
422 bindings: &HashMap<String, Term>,
423) -> Vec<Term> {
424 messages
425 .iter()
426 .map(|msg| {
427 let Term::Literal(lit) = msg else {
428 return msg.clone();
429 };
430 let text = lit.value();
431 let substituted = apply_message_template(text, focus, bindings);
432 if substituted == text {
433 msg.clone()
434 } else {
435 Term::Literal(Literal::new_simple_literal(&substituted))
436 }
437 })
438 .collect()
439}
440
441struct CustomComponent {
445 iri: NamedNode,
447 params: Vec<ComponentParam>,
448 node_validator: Option<ComponentValidator>,
450 property_validator: Option<ComponentValidator>,
452 generic_validator: Option<ComponentValidator>,
454}
455
456struct ComponentParam {
457 path: NamedNode,
459 var: String,
461 optional: bool,
462}
463
464struct ComponentValidator {
465 kind: SparqlQueryKind,
466 query: String,
468 messages: Vec<Term>,
469}
470
471fn resolve_validator(
472 shapes: &Loaded,
473 node: Term,
474 component_iri: &NamedNode,
475 policy: UnsupportedPolicy,
476) -> Option<ComponentValidator> {
477 match parse_validator(shapes, &node) {
478 Ok(v) => Some(v),
479 Err(e) => {
480 assert!(
481 policy != UnsupportedPolicy::Error,
482 "invalid SPARQL in custom constraint component <{component_iri}>: {e}"
483 );
484 None
485 }
486 }
487}
488
489fn build_components(shapes: &Loaded, policy: UnsupportedPolicy) -> Vec<CustomComponent> {
500 let mut out = Vec::new();
501 let mut seen = HashSet::new();
502 for triple in shapes.graph.triples_for_predicate(vocab::SH_PARAMETER) {
503 let subject = triple.subject.into_owned();
504 if !seen.insert(subject.clone()) {
505 continue;
506 }
507 let NamedOrBlankNode::NamedNode(iri) = &subject else {
508 continue; };
510 if vocab::NATIVE_CONSTRAINT_COMPONENTS.contains(&iri.as_ref()) {
511 continue; }
513
514 let node_validator = shapes
515 .object(&subject, vocab::SH_NODE_VALIDATOR)
516 .and_then(|v| resolve_validator(shapes, v, iri, policy));
517 let property_validator = shapes
518 .object(&subject, vocab::SH_PROPERTY_VALIDATOR)
519 .and_then(|v| resolve_validator(shapes, v, iri, policy));
520 let generic_validator = shapes
521 .object(&subject, vocab::SH_VALIDATOR)
522 .and_then(|v| resolve_validator(shapes, v, iri, policy));
523 if node_validator.is_none() && property_validator.is_none() && generic_validator.is_none() {
524 continue; }
526 let mut params = Vec::new();
527 for p in shapes.objects(&subject, vocab::SH_PARAMETER) {
528 let Some(pn) = term_to_node(&p) else { continue };
529 let Some(Term::NamedNode(path)) = shapes.object(&pn, vocab::SH_PATH) else {
530 continue;
531 };
532 let var = local_name(path.as_str()).to_string();
533 let optional = matches!(
534 shapes.object(&pn, vocab::SH_OPTIONAL),
535 Some(Term::Literal(ref l)) if l.value() == "true"
536 );
537 params.push(ComponentParam {
538 path,
539 var,
540 optional,
541 });
542 }
543 if params.is_empty() {
544 continue;
545 }
546 out.push(CustomComponent {
547 iri: iri.clone(),
548 params,
549 node_validator,
550 property_validator,
551 generic_validator,
552 });
553 }
554 out.sort_by(|a, b| a.iri.as_str().cmp(b.iri.as_str()));
555 out
556}
557
558fn parse_validator(shapes: &Loaded, node: &Term) -> Result<ComponentValidator, String> {
562 let node = term_to_node(node)
563 .ok_or_else(|| "validator node is not an IRI or blank node".to_string())?;
564 let (kind, raw) = if let Some(Term::Literal(q)) = shapes.object(&node, vocab::SH_ASK) {
565 (SparqlQueryKind::Ask, q.value().to_string())
566 } else if let Some(Term::Literal(q)) = shapes.object(&node, vocab::SH_SELECT) {
567 (SparqlQueryKind::Select, q.value().to_string())
568 } else {
569 return Err("validator has neither sh:ask nor sh:select".to_string());
570 };
571 let (_, query) = canonical_sparql_query(shapes, &node, &raw)
572 .map_err(|e| format!("invalid SPARQL in {node}: {e}"))?;
573 let messages = shapes.objects(&node, vocab::SH_MESSAGE);
574 Ok(ComponentValidator {
575 kind,
576 query,
577 messages,
578 })
579}
580
581fn local_name(iri: &str) -> &str {
584 iri.rsplit(['#', '/']).next().unwrap_or(iri)
585}
586
587pub(crate) fn collect_functions(shapes: &Loaded) -> Vec<FunctionDef> {
592 let mut out = Vec::new();
593 for func in shapes
594 .graph
595 .subjects_for_predicate_object(vocab::RDF_TYPE, vocab::SH_SPARQL_FUNCTION)
596 .map(|s| s.into_owned())
597 .collect::<Vec<_>>()
598 {
599 let NamedOrBlankNode::NamedNode(iri) = &func else {
600 continue;
601 };
602 let raw = match shapes
603 .object(&func, vocab::SH_SELECT)
604 .or_else(|| shapes.object(&func, vocab::SH_ASK))
605 {
606 Some(Term::Literal(q)) => q.value().to_string(),
607 _ => continue,
608 };
609 let Ok((_, query)) = canonical_sparql_query(shapes, &func, &raw) else {
610 continue;
611 };
612 out.push(FunctionDef {
613 iri: iri.clone(),
614 params: function_param_names(shapes, &func),
615 reads_graph: crate::sparql::query_reads_graph(&query),
616 query,
617 });
618 }
619 out
620}
621
622fn function_param_names(shapes: &Loaded, func: &NamedOrBlankNode) -> Vec<String> {
625 let mut params: Vec<(i64, String)> = shapes
626 .objects(func, vocab::SH_PARAMETER)
627 .iter()
628 .filter_map(|p| {
629 let pn = term_to_node(p)?;
630 let order = match shapes.object(&pn, vocab::SH_ORDER) {
631 Some(Term::Literal(l)) => l.value().parse::<i64>().unwrap_or(0),
632 _ => 0,
633 };
634 let name = match shapes.object(&pn, vocab::SH_NAME) {
635 Some(Term::Literal(l)) => l.value().to_string(),
636 _ => match shapes.object(&pn, vocab::SH_PATH) {
637 Some(Term::NamedNode(n)) => local_name(n.as_str()).to_string(),
638 _ => return None,
639 },
640 };
641 Some((order, name))
642 })
643 .collect();
644 params.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
645 params.into_iter().map(|(_, name)| name).collect()
646}
647
648struct Reporter<'a> {
649 shapes: &'a Loaded,
650 focus_data: &'a Graph,
651 sparql: SparqlExecutor,
652 needs_sparql: bool,
653 class_index: HashMap<Term, Vec<Term>>,
656 path_cache: RefCell<HashMap<NamedOrBlankNode, PathCacheEntry>>,
659 components: Vec<CustomComponent>,
662}
663
664type Visited = HashSet<(NamedOrBlankNode, Term)>;
665
666type PathCacheEntry = (Option<Term>, Option<Path>);
668
669impl Reporter<'_> {
670 fn frozen(&self) -> &FrozenIndexedDataset {
671 self.sparql
672 .frozen()
673 .expect("report validation always has a frozen dataset")
674 }
675
676 fn target_shapes(&self) -> Vec<NamedOrBlankNode> {
677 let mut found: HashSet<NamedOrBlankNode> = HashSet::new();
678 for t in self.shapes.graph.iter() {
679 let p = t.predicate;
680 if p == vocab::SH_TARGET_NODE
681 || p == vocab::SH_TARGET_CLASS
682 || p == vocab::SH_TARGET_SUBJECTS_OF
683 || p == vocab::SH_TARGET_OBJECTS_OF
684 {
685 found.insert(t.subject.into_owned());
686 }
687 if p == vocab::SH_TARGET
689 && let Some(target) = term_to_node(&t.object.into_owned())
690 && self.shapes.object(&target, vocab::SH_SELECT).is_some()
691 {
692 found.insert(t.subject.into_owned());
693 }
694 if p == vocab::RDF_TYPE {
696 let s = t.subject.into_owned();
697 if self.is_class(&s) && self.is_shape(&s) {
698 found.insert(s);
699 }
700 }
701 }
702 let mut v: Vec<_> = found.into_iter().collect();
703 v.sort_by_key(|n| n.to_string());
704 v
705 }
706
707 fn is_shape(&self, n: &NamedOrBlankNode) -> bool {
709 is_shape_node(self.shapes, n)
710 }
711
712 fn is_class(&self, n: &NamedOrBlankNode) -> bool {
713 self.shapes.is_instance_of(n, vocab::RDFS_CLASS)
714 || self.shapes.is_instance_of(n, vocab::OWL_CLASS)
715 }
716
717 fn deactivated(&self, n: &NamedOrBlankNode) -> bool {
718 matches!(self.shapes.object(n, vocab::SH_DEACTIVATED),
719 Some(Term::Literal(ref l)) if l.value() == "true")
720 }
721
722 fn focus_nodes(&self, shape: &NamedOrBlankNode) -> Vec<Term> {
723 let mut nodes = Vec::new();
724 nodes.extend(self.shapes.objects(shape, vocab::SH_TARGET_NODE));
725 for c in self.shapes.objects(shape, vocab::SH_TARGET_CLASS) {
726 if let Some(instances) = self.class_index.get(&c) {
727 nodes.extend(instances.iter().cloned());
728 }
729 }
730 for p in self.shapes.objects(shape, vocab::SH_TARGET_SUBJECTS_OF) {
731 if let Term::NamedNode(n) = p {
732 nodes.extend(
733 self.focus_data
734 .triples_for_predicate(n.as_ref())
735 .map(|t| node_term(t.subject)),
736 );
737 }
738 }
739 for p in self.shapes.objects(shape, vocab::SH_TARGET_OBJECTS_OF) {
740 if let Term::NamedNode(n) = p {
741 nodes.extend(
742 self.focus_data
743 .triples_for_predicate(n.as_ref())
744 .map(|t| t.object.into_owned()),
745 );
746 }
747 }
748 if self.needs_sparql {
751 let exec = &self.sparql;
752 for target in self.shapes.objects(shape, vocab::SH_TARGET) {
753 let Some(target_node) = term_to_node(&target) else {
754 continue;
755 };
756 let Some(Term::Literal(query)) = self.shapes.object(&target_node, vocab::SH_SELECT)
757 else {
758 continue;
759 };
760 let Ok((_, canonical)) =
762 canonical_sparql_query(self.shapes, &target_node, query.value())
763 else {
764 continue;
765 };
766 if let Ok(found) = exec.target_nodes(&canonical) {
767 nodes.extend(found);
768 }
769 }
770 }
771 if let NamedOrBlankNode::NamedNode(n) = shape
773 && self.is_class(shape)
774 {
775 let class = Term::NamedNode(n.clone());
776 if let Some(instances) = self.class_index.get(&class) {
777 nodes.extend(instances.iter().cloned());
778 }
779 }
780 let mut seen = HashSet::new();
781 nodes.retain(|t| seen.insert(t.clone()));
782 nodes
783 }
784
785 fn shape_path(&self, shape: &NamedOrBlankNode) -> (Option<Term>, Option<Path>) {
788 if let Some(cached) = self.path_cache.borrow().get(shape) {
789 return cached.clone();
790 }
791 let path_term = self.shapes.object(shape, vocab::SH_PATH);
792 let parsed = path_term
793 .as_ref()
794 .and_then(|t| parse_path(self.shapes, t).ok());
795 let entry = (path_term, parsed);
796 self.path_cache
797 .borrow_mut()
798 .insert(shape.clone(), entry.clone());
799 entry
800 }
801
802 fn messages_or_inherited(&self, shape: &NamedOrBlankNode, inherited: &[Term]) -> Vec<Term> {
808 let own = self.messages(shape);
809 if own.is_empty() {
810 inherited.to_vec()
811 } else {
812 own
813 }
814 }
815
816 fn collect(
821 &self,
822 shape: &NamedOrBlankNode,
823 focus: &Term,
824 out: &mut Vec<ValidationResult>,
825 visited: &mut Visited,
826 inherited: &[Term],
827 ) {
828 if self.deactivated(shape) {
829 return; }
831 let key = (shape.clone(), focus.clone());
832 if !visited.insert(key.clone()) {
833 return; }
835
836 let (path_term, parsed) = self.shape_path(shape);
837 let value_nodes: Vec<Term> = match &parsed {
838 Some(p) => succ(self.frozen(), focus, p).into_iter().collect(),
839 None => vec![focus.clone()],
840 };
841 let severity = self.severity(shape);
842 let messages = self.messages_or_inherited(shape, inherited);
843 let push = |out: &mut Vec<ValidationResult>, value, component| {
844 out.push(ValidationResult {
845 focus: focus.clone(),
846 path: path_term.clone(),
847 value,
848 component,
849 source_shape: node_term_ref(shape),
850 severity: severity.clone(),
851 messages: messages.clone(),
852 });
853 };
854
855 if parsed.is_some() {
857 if let Some(min) = self.int(shape, vocab::SH_MIN_COUNT)
858 && (value_nodes.len() as u64) < min
859 {
860 push(out, None, vocab::SH_CC_MIN_COUNT.into_owned());
861 }
862 if let Some(max) = self.int(shape, vocab::SH_MAX_COUNT)
863 && (value_nodes.len() as u64) > max
864 {
865 push(out, None, vocab::SH_CC_MAX_COUNT.into_owned());
866 }
867 }
868
869 for hv in self.shapes.objects(shape, vocab::SH_HAS_VALUE) {
871 if !value_nodes.contains(&hv) {
872 push(out, None, vocab::SH_CC_HAS_VALUE.into_owned());
873 }
874 }
875
876 self.collect_closed(shape, focus, &value_nodes, out, &messages);
877 self.collect_property_pairs(shape, focus, &path_term, &value_nodes, out, &messages);
878 self.collect_unique_lang(shape, focus, &path_term, &value_nodes, out, &messages);
879 self.collect_qualified_counts(
880 shape,
881 focus,
882 &path_term,
883 &value_nodes,
884 out,
885 visited,
886 &messages,
887 );
888
889 for u in &value_nodes {
891 for (component, ok) in self.value_checks(shape, u, visited) {
892 if !ok {
893 push(out, Some(u.clone()), component);
894 }
895 }
896 }
897
898 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
901 if let Some(pn) = term_to_node(&prop) {
902 for u in &value_nodes {
903 self.collect(&pn, u, out, visited, &messages);
904 }
905 }
906 }
907
908 self.collect_sparql(shape, focus, &path_term, &parsed, out, &messages);
909 self.collect_expression(shape, focus, out, visited, &messages);
910 self.collect_components(
911 shape,
912 focus,
913 &path_term,
914 &parsed,
915 &value_nodes,
916 out,
917 &messages,
918 );
919
920 visited.remove(&key);
921 }
922
923 fn collect_property_witnesses(
930 &self,
931 shape: &NamedOrBlankNode,
932 focus: &Term,
933 profile: &NamedOrBlankNode,
934 key_path: Option<&Path>,
935 visited: &mut Visited,
936 out: &mut Vec<PropertyWitness>,
937 ) {
938 if self.deactivated(shape) {
939 return;
940 }
941 let key = (shape.clone(), focus.clone());
942 if !visited.insert(key.clone()) {
943 return;
944 }
945
946 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
947 if let Some(pn) = term_to_node(&prop) {
948 self.collect_property_binding(&pn, focus, profile, key_path, visited, out);
949 }
950 }
951 for list in self.shapes.objects(shape, vocab::SH_AND) {
952 for member in self.shapes.read_list(&list) {
953 if let Some(mn) = term_to_node(&member) {
954 self.collect_property_witnesses(&mn, focus, profile, key_path, visited, out);
955 }
956 }
957 }
958 for n in self.shapes.objects(shape, vocab::SH_NODE) {
959 if let Some(nn) = term_to_node(&n) {
960 self.collect_property_witnesses(&nn, focus, profile, key_path, visited, out);
961 }
962 }
963
964 visited.remove(&key);
965 }
966
967 fn collect_property_binding(
974 &self,
975 pn: &NamedOrBlankNode,
976 focus: &Term,
977 profile: &NamedOrBlankNode,
978 key_path: Option<&Path>,
979 visited: &mut Visited,
980 out: &mut Vec<PropertyWitness>,
981 ) {
982 if self.deactivated(pn) {
983 return;
984 }
985 let (_, parsed_path) = self.shape_path(pn);
986 let Some(path) = parsed_path else { return };
987
988 let key = key_path
994 .and_then(|kp| {
995 let mut matches: Vec<Term> = succ(&self.shapes.graph, &node_term_ref(pn), kp)
996 .into_iter()
997 .collect();
998 matches.sort_by_key(ToString::to_string);
999 matches.into_iter().next()
1000 })
1001 .unwrap_or_else(|| node_term_ref(pn));
1002
1003 let value_nodes: Vec<Term> = succ(self.frozen(), focus, &path).into_iter().collect();
1004 let values = match self.shapes.object(pn, vocab::SH_QUALIFIED_VALUE_SHAPE) {
1005 Some(qualifier_term) => {
1006 let Some(qualifier) = term_to_node(&qualifier_term) else {
1007 return;
1008 };
1009 let siblings = if self.bool(pn, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
1010 self.sibling_qualified_shapes(pn, &qualifier)
1011 } else {
1012 Vec::new()
1013 };
1014 value_nodes
1015 .into_iter()
1016 .filter(|v| {
1017 self.conforms(&qualifier, v, visited)
1018 && siblings
1019 .iter()
1020 .all(|sibling| !self.conforms(sibling, v, visited))
1021 })
1022 .collect()
1023 }
1024 None => value_nodes,
1025 };
1026
1027 out.push(PropertyWitness {
1028 focus: focus.clone(),
1029 shape: node_term_ref(profile),
1030 key,
1031 values,
1032 });
1033 }
1034
1035 #[allow(clippy::too_many_arguments)]
1042 fn collect_components(
1043 &self,
1044 shape: &NamedOrBlankNode,
1045 focus: &Term,
1046 path_term: &Option<Term>,
1047 parsed_path: &Option<Path>,
1048 value_nodes: &[Term],
1049 out: &mut Vec<ValidationResult>,
1050 inherited: &[Term],
1051 ) {
1052 if self.components.is_empty() {
1053 return;
1054 }
1055 let is_property_shape = parsed_path.is_some();
1056 for component in &self.components {
1057 let mut params: Vec<(String, Term)> = Vec::new();
1059 let mut activated = true;
1060 for p in &component.params {
1061 if let Some(value) = self.shapes.object(shape, p.path.as_ref()) {
1062 params.push((p.var.clone(), value));
1063 } else if !p.optional {
1064 activated = false;
1065 break;
1066 }
1067 }
1068 if !activated {
1069 continue;
1070 }
1071
1072 let validator = if is_property_shape {
1073 component
1074 .property_validator
1075 .as_ref()
1076 .or(component.generic_validator.as_ref())
1077 } else {
1078 component
1079 .node_validator
1080 .as_ref()
1081 .or(component.generic_validator.as_ref())
1082 };
1083 let Some(validator) = validator else { continue };
1084
1085 let mut base = params;
1089 base.push(("currentShape".to_string(), node_term_ref(shape)));
1090 let path = parsed_path.as_ref();
1091
1092 match validator.kind {
1093 SparqlQueryKind::Ask => {
1094 for value in value_nodes {
1095 let mut bindings = base.clone();
1096 bindings.push(("this".to_string(), focus.clone()));
1097 bindings.push(("value".to_string(), value.clone()));
1098 let violates = match self.sparql.eval_ask(&validator.query, path, &bindings)
1100 {
1101 Ok(conforms) => !conforms,
1102 Err(_) => true,
1103 };
1104 if violates {
1105 self.push_component_result(
1106 out,
1107 component,
1108 shape,
1109 focus,
1110 path_term.clone(),
1111 Some(value.clone()),
1112 &bindings,
1113 &validator.messages,
1114 inherited,
1115 );
1116 }
1117 }
1118 }
1119 SparqlQueryKind::Select => {
1120 let mut bindings = base.clone();
1121 bindings.push(("this".to_string(), focus.clone()));
1122 match self.sparql.eval_select(&validator.query, path, &bindings) {
1123 Ok(rows) => {
1124 for row in rows {
1125 let value = row
1128 .get("value")
1129 .cloned()
1130 .or_else(|| (!is_property_shape).then(|| focus.clone()));
1131 let path = row.get("path").cloned().or_else(|| path_term.clone());
1132 let mut binds = bindings.clone();
1133 binds.extend(row);
1134 self.push_component_result(
1135 out,
1136 component,
1137 shape,
1138 focus,
1139 path,
1140 value,
1141 &binds,
1142 &validator.messages,
1143 inherited,
1144 );
1145 }
1146 }
1147 Err(_) => self.push_component_result(
1148 out,
1149 component,
1150 shape,
1151 focus,
1152 path_term.clone(),
1153 None,
1154 &bindings,
1155 &validator.messages,
1156 inherited,
1157 ),
1158 }
1159 }
1160 }
1161 }
1162 }
1163
1164 #[allow(clippy::too_many_arguments)]
1165 fn push_component_result(
1166 &self,
1167 out: &mut Vec<ValidationResult>,
1168 component: &CustomComponent,
1169 shape: &NamedOrBlankNode,
1170 focus: &Term,
1171 path: Option<Term>,
1172 value: Option<Term>,
1173 bindings: &[(String, Term)],
1174 validator_messages: &[Term],
1175 inherited: &[Term],
1176 ) {
1177 let raw = if validator_messages.is_empty() {
1178 self.messages_or_inherited(shape, inherited)
1179 } else {
1180 validator_messages.to_vec()
1181 };
1182 let bind_map: HashMap<String, Term> = bindings.iter().cloned().collect();
1183 let messages = substitute_messages(&raw, focus, &bind_map);
1184 out.push(ValidationResult {
1185 focus: focus.clone(),
1186 path,
1187 value,
1188 component: component.iri.clone(),
1189 source_shape: node_term_ref(shape),
1190 severity: self.severity(shape),
1191 messages,
1192 });
1193 }
1194
1195 fn collect_expression(
1202 &self,
1203 shape: &NamedOrBlankNode,
1204 focus: &Term,
1205 out: &mut Vec<ValidationResult>,
1206 visited: &mut Visited,
1207 inherited: &[Term],
1208 ) {
1209 for expr_term in self.shapes.objects(shape, vocab::SH_EXPRESSION) {
1210 let Some(results) = self.eval_node_expr(&expr_term, focus, visited) else {
1211 continue;
1212 };
1213 for value in results {
1214 if is_boolean_true(&value) {
1215 continue;
1216 }
1217 out.push(ValidationResult {
1218 focus: focus.clone(),
1219 path: None,
1220 value: Some(value),
1221 component: vocab::SH_CC_EXPRESSION.into_owned(),
1222 source_shape: node_term_ref(shape),
1223 severity: self.severity(shape),
1224 messages: self.messages_or_inherited(shape, inherited),
1225 });
1226 }
1227 }
1228 }
1229
1230 fn eval_node_expr(
1235 &self,
1236 term: &Term,
1237 focus: &Term,
1238 visited: &mut Visited,
1239 ) -> Option<Vec<Term>> {
1240 match term {
1241 Term::NamedNode(n) if n.as_ref() == vocab::SH_THIS => Some(vec![focus.clone()]),
1242 Term::NamedNode(_) | Term::Literal(_) => Some(vec![term.clone()]),
1243 Term::BlankNode(_) => {
1244 let node = term_to_node(term)?;
1245 if let Some(path_term) = self.shapes.object(&node, vocab::SH_PATH) {
1246 let path = parse_path(self.shapes, &path_term).ok()?;
1247 Some(succ(self.frozen(), focus, &path).into_iter().collect())
1248 } else if let Some(filter_shape) = self.shapes.object(&node, vocab::SH_FILTER_SHAPE)
1249 {
1250 let filter_shape = term_to_node(&filter_shape)?;
1251 let nodes_term = self.shapes.object(&node, vocab::SH_NODES)?;
1252 let inputs = self.eval_node_expr(&nodes_term, focus, visited)?;
1253 Some(
1254 inputs
1255 .into_iter()
1256 .filter(|x| self.conforms(&filter_shape, x, visited))
1257 .collect(),
1258 )
1259 } else if let Some(list) = self.shapes.object(&node, vocab::SH_INTERSECTION) {
1260 self.eval_node_expr_set(&list, focus, visited, true)
1261 } else if let Some(list) = self.shapes.object(&node, vocab::SH_UNION) {
1262 self.eval_node_expr_set(&list, focus, visited, false)
1263 } else {
1264 None
1266 }
1267 }
1268 }
1269 }
1270
1271 fn eval_node_expr_set(
1275 &self,
1276 list_head: &Term,
1277 focus: &Term,
1278 visited: &mut Visited,
1279 intersect: bool,
1280 ) -> Option<Vec<Term>> {
1281 let members = self.shapes.read_list(list_head);
1282 if members.is_empty() {
1283 return None;
1284 }
1285 let mut iter = members.iter();
1286 let mut acc = self.eval_node_expr(iter.next().unwrap(), focus, visited)?;
1287 for member in iter {
1288 let next = self.eval_node_expr(member, focus, visited)?;
1289 if intersect {
1290 acc.retain(|x| next.contains(x));
1291 } else {
1292 for t in next {
1293 if !acc.contains(&t) {
1294 acc.push(t);
1295 }
1296 }
1297 }
1298 }
1299 Some(acc)
1300 }
1301
1302 fn build_sparql_constraint(
1311 &self,
1312 shape: &NamedOrBlankNode,
1313 constraint_node: &NamedOrBlankNode,
1314 parsed_path: &Option<Path>,
1315 ) -> Option<SparqlConstraint> {
1316 let (kind, raw) = if let Some(Term::Literal(query)) =
1317 self.shapes.object(constraint_node, vocab::SH_SELECT)
1318 {
1319 (SparqlQueryKind::Select, query.value().to_string())
1320 } else if let Some(Term::Literal(query)) =
1321 self.shapes.object(constraint_node, vocab::SH_ASK)
1322 {
1323 (SparqlQueryKind::Ask, query.value().to_string())
1324 } else {
1325 return None;
1326 };
1327 let (_, query) = canonical_sparql_query(self.shapes, constraint_node, &raw).ok()?;
1328 Some(SparqlConstraint {
1329 kind,
1330 query,
1331 path: parsed_path.clone(),
1332 shape: Some(node_term_ref(shape)),
1333 messages: Vec::new(),
1336 extra_bindings: Vec::new(),
1337 bind_value_to_this: false,
1338 })
1339 }
1340
1341 fn prefetch_sparql(&self, shape: &NamedOrBlankNode, foci: &[Term]) {
1345 if !self.needs_sparql || foci.len() < 2 {
1346 return;
1347 }
1348 let (_, parsed_path) = self.shape_path(shape);
1349 for constraint_term in self.shapes.objects(shape, vocab::SH_SPARQL) {
1350 let Some(constraint_node) = term_to_node(&constraint_term) else {
1351 continue;
1352 };
1353 if let Some(constraint) =
1354 self.build_sparql_constraint(shape, &constraint_node, &parsed_path)
1355 {
1356 let _ = self.sparql.prefetch_constraint(&constraint, foci);
1357 }
1358 }
1359 }
1360
1361 fn collect_sparql(
1362 &self,
1363 shape: &NamedOrBlankNode,
1364 focus: &Term,
1365 path_term: &Option<Term>,
1366 parsed_path: &Option<Path>,
1367 out: &mut Vec<ValidationResult>,
1368 inherited: &[Term],
1369 ) {
1370 if !self.needs_sparql {
1371 return;
1372 }
1373 let sparql = &self.sparql;
1374 let severity = self.severity(shape);
1375 for constraint_term in self.shapes.objects(shape, vocab::SH_SPARQL) {
1376 let Some(constraint_node) = term_to_node(&constraint_term) else {
1377 continue;
1378 };
1379 let Some(constraint) =
1380 self.build_sparql_constraint(shape, &constraint_node, parsed_path)
1381 else {
1382 continue;
1383 };
1384 let raw_messages = {
1388 let on_constraint = self.shapes.objects(&constraint_node, vocab::SH_MESSAGE);
1389 if on_constraint.is_empty() {
1390 self.messages_or_inherited(shape, inherited)
1391 } else {
1392 on_constraint
1393 }
1394 };
1395 match sparql.constraint_violations(&constraint, focus) {
1396 Ok(violations) => {
1397 for violation in violations {
1398 let messages =
1399 substitute_messages(&raw_messages, focus, &violation.bindings);
1400 let value = violation.value.or_else(|| match constraint.kind {
1403 SparqlQueryKind::Select => Some(focus.clone()),
1404 SparqlQueryKind::Ask => None,
1405 });
1406 out.push(ValidationResult {
1407 focus: focus.clone(),
1408 path: violation.path.or_else(|| path_term.clone()),
1409 value,
1410 component: vocab::SH_CC_SPARQL.into_owned(),
1411 source_shape: node_term_ref(shape),
1412 severity: severity.clone(),
1413 messages,
1414 });
1415 }
1416 }
1417 Err(error) => {
1421 let mut messages = raw_messages;
1422 messages.push(Term::Literal(Literal::new_simple_literal(format!(
1423 "SPARQL constraint evaluation failed: {error}"
1424 ))));
1425 out.push(ValidationResult {
1426 focus: focus.clone(),
1427 path: path_term.clone(),
1428 value: None,
1429 component: vocab::SH_CC_SPARQL.into_owned(),
1430 source_shape: node_term_ref(shape),
1431 severity: severity.clone(),
1432 messages,
1433 });
1434 }
1435 }
1436 }
1437 }
1438
1439 fn collect_closed(
1440 &self,
1441 shape: &NamedOrBlankNode,
1442 focus: &Term,
1443 value_nodes: &[Term],
1444 out: &mut Vec<ValidationResult>,
1445 inherited: &[Term],
1446 ) {
1447 if !self.bool(shape, vocab::SH_CLOSED) {
1448 return;
1449 }
1450 let mut allowed = HashSet::new();
1451 for prop in self.shapes.objects(shape, vocab::SH_PROPERTY) {
1452 let Some(prop) = term_to_node(&prop) else {
1453 continue;
1454 };
1455 if let Some(Term::NamedNode(path)) = self.shapes.object(&prop, vocab::SH_PATH) {
1456 allowed.insert(path);
1457 }
1458 }
1459 for list in self.shapes.objects(shape, vocab::SH_IGNORED_PROPERTIES) {
1460 for term in self.shapes.read_list(&list) {
1461 if let Term::NamedNode(predicate) = term {
1462 allowed.insert(predicate);
1463 }
1464 }
1465 }
1466 for value_node in value_nodes {
1467 for (predicate, object) in self.frozen().outgoing(value_node) {
1468 if allowed.contains(&predicate) {
1469 continue;
1470 }
1471 out.push(ValidationResult {
1472 focus: focus.clone(),
1473 path: Some(Term::NamedNode(predicate)),
1474 value: Some(object),
1475 component: vocab::SH_CC_CLOSED.into_owned(),
1476 source_shape: node_term_ref(shape),
1477 severity: self.severity(shape),
1478 messages: self.messages_or_inherited(shape, inherited),
1479 });
1480 }
1481 }
1482 }
1483
1484 #[allow(clippy::too_many_arguments)]
1485 fn collect_property_pairs(
1486 &self,
1487 shape: &NamedOrBlankNode,
1488 focus: &Term,
1489 path: &Option<Term>,
1490 value_nodes: &[Term],
1491 out: &mut Vec<ValidationResult>,
1492 inherited: &[Term],
1493 ) {
1494 for predicate in self.shapes.objects(shape, vocab::SH_EQUALS) {
1495 let Term::NamedNode(predicate) = predicate else {
1496 continue;
1497 };
1498 let other = succ(self.frozen(), focus, &Path::Pred(predicate));
1499 for value in value_nodes.iter().filter(|value| !other.contains(*value)) {
1500 self.push(
1501 out,
1502 shape,
1503 focus,
1504 path.clone(),
1505 Some((*value).clone()),
1506 vocab::SH_CC_EQUALS,
1507 inherited,
1508 );
1509 }
1510 for value in other.iter().filter(|value| !value_nodes.contains(*value)) {
1511 self.push(
1512 out,
1513 shape,
1514 focus,
1515 path.clone(),
1516 Some(value.clone()),
1517 vocab::SH_CC_EQUALS,
1518 inherited,
1519 );
1520 }
1521 }
1522 for predicate in self.shapes.objects(shape, vocab::SH_DISJOINT) {
1523 let Term::NamedNode(predicate) = predicate else {
1524 continue;
1525 };
1526 let other = succ(self.frozen(), focus, &Path::Pred(predicate));
1527 for value in value_nodes.iter().filter(|value| other.contains(*value)) {
1528 self.push(
1529 out,
1530 shape,
1531 focus,
1532 path.clone(),
1533 Some((*value).clone()),
1534 vocab::SH_CC_DISJOINT,
1535 inherited,
1536 );
1537 }
1538 }
1539 for (constraint, component, inclusive) in [
1540 (vocab::SH_LESS_THAN, vocab::SH_CC_LESS_THAN, false),
1541 (
1542 vocab::SH_LESS_THAN_OR_EQUALS,
1543 vocab::SH_CC_LESS_THAN_OR_EQUALS,
1544 true,
1545 ),
1546 ] {
1547 for predicate in self.shapes.objects(shape, constraint) {
1548 let Term::NamedNode(predicate) = predicate else {
1549 continue;
1550 };
1551 for left in value_nodes {
1552 for right in succ(self.frozen(), focus, &Path::Pred(predicate.clone())) {
1553 let ordering = compare_terms(left, &right);
1554 let passes = ordering == Some(Ordering::Less)
1555 || inclusive && ordering == Some(Ordering::Equal);
1556 if !passes {
1557 self.push(
1558 out,
1559 shape,
1560 focus,
1561 path.clone(),
1562 Some(left.clone()),
1563 component,
1564 inherited,
1565 );
1566 }
1567 }
1568 }
1569 }
1570 }
1571 }
1572
1573 #[allow(clippy::too_many_arguments)]
1574 fn collect_unique_lang(
1575 &self,
1576 shape: &NamedOrBlankNode,
1577 focus: &Term,
1578 path: &Option<Term>,
1579 value_nodes: &[Term],
1580 out: &mut Vec<ValidationResult>,
1581 inherited: &[Term],
1582 ) {
1583 if !self.bool(shape, vocab::SH_UNIQUE_LANG) {
1584 return;
1585 }
1586 let mut counts = HashMap::new();
1587 for value in value_nodes {
1588 if let Term::Literal(literal) = value
1589 && let Some(language) = literal.language()
1590 {
1591 *counts
1592 .entry(language.to_ascii_lowercase())
1593 .or_insert(0usize) += 1;
1594 }
1595 }
1596 for _ in counts.values().filter(|count| **count > 1) {
1597 self.push(
1598 out,
1599 shape,
1600 focus,
1601 path.clone(),
1602 None,
1603 vocab::SH_CC_UNIQUE_LANG,
1604 inherited,
1605 );
1606 }
1607 }
1608
1609 #[allow(clippy::too_many_arguments)]
1610 fn collect_qualified_counts(
1611 &self,
1612 shape: &NamedOrBlankNode,
1613 focus: &Term,
1614 path: &Option<Term>,
1615 value_nodes: &[Term],
1616 out: &mut Vec<ValidationResult>,
1617 visited: &mut Visited,
1618 inherited: &[Term],
1619 ) {
1620 for qualifier in self.shapes.objects(shape, vocab::SH_QUALIFIED_VALUE_SHAPE) {
1621 let Some(qualifier) = term_to_node(&qualifier) else {
1622 continue;
1623 };
1624 let siblings = if self.bool(shape, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
1625 self.sibling_qualified_shapes(shape, &qualifier)
1626 } else {
1627 Vec::new()
1628 };
1629 let count = value_nodes
1630 .iter()
1631 .filter(|value| {
1632 self.conforms(&qualifier, value, visited)
1633 && siblings
1634 .iter()
1635 .all(|sibling| !self.conforms(sibling, value, visited))
1636 })
1637 .count() as u64;
1638 if let Some(min) = self.int(shape, vocab::SH_QUALIFIED_MIN_COUNT)
1639 && count < min
1640 {
1641 self.push(
1642 out,
1643 shape,
1644 focus,
1645 path.clone(),
1646 None,
1647 vocab::SH_CC_QUALIFIED_MIN_COUNT,
1648 inherited,
1649 );
1650 }
1651 if let Some(max) = self.int(shape, vocab::SH_QUALIFIED_MAX_COUNT)
1652 && count > max
1653 {
1654 self.push(
1655 out,
1656 shape,
1657 focus,
1658 path.clone(),
1659 None,
1660 vocab::SH_CC_QUALIFIED_MAX_COUNT,
1661 inherited,
1662 );
1663 }
1664 }
1665 }
1666
1667 fn sibling_qualified_shapes(
1668 &self,
1669 shape: &NamedOrBlankNode,
1670 qualifier: &NamedOrBlankNode,
1671 ) -> Vec<NamedOrBlankNode> {
1672 let shape_term = node_term_ref(shape);
1673 let mut siblings = HashSet::new();
1674 for triple in self.shapes.graph.triples_for_predicate(vocab::SH_PROPERTY) {
1675 if triple.object != shape_term.as_ref() {
1676 continue;
1677 }
1678 let parent = triple.subject.into_owned();
1679 for property in self.shapes.objects(&parent, vocab::SH_PROPERTY) {
1680 let Some(property) = term_to_node(&property) else {
1681 continue;
1682 };
1683 for qualifier in self
1684 .shapes
1685 .objects(&property, vocab::SH_QUALIFIED_VALUE_SHAPE)
1686 {
1687 if let Some(qualifier) = term_to_node(&qualifier) {
1688 siblings.insert(qualifier);
1689 }
1690 }
1691 }
1692 }
1693 siblings.remove(qualifier);
1694 siblings.into_iter().collect()
1695 }
1696
1697 #[allow(clippy::too_many_arguments)]
1698 fn push(
1699 &self,
1700 out: &mut Vec<ValidationResult>,
1701 shape: &NamedOrBlankNode,
1702 focus: &Term,
1703 path: Option<Term>,
1704 value: Option<Term>,
1705 component: NamedNodeRef<'static>,
1706 inherited: &[Term],
1707 ) {
1708 let mut bindings = HashMap::new();
1709 if let Some(v) = &value {
1710 bindings.insert("value".to_string(), v.clone());
1711 }
1712 if let Some(p) = &path {
1713 bindings.insert("path".to_string(), p.clone());
1714 }
1715 let raw = self.messages_or_inherited(shape, inherited);
1716 let messages = substitute_messages(&raw, focus, &bindings);
1717 out.push(ValidationResult {
1718 focus: focus.clone(),
1719 path,
1720 value,
1721 component: component.into_owned(),
1722 source_shape: node_term_ref(shape),
1723 severity: self.severity(shape),
1724 messages,
1725 });
1726 }
1727
1728 fn messages(&self, shape: &NamedOrBlankNode) -> Vec<Term> {
1730 self.shapes.objects(shape, vocab::SH_MESSAGE)
1731 }
1732
1733 fn conforms(&self, shape: &NamedOrBlankNode, focus: &Term, visited: &mut Visited) -> bool {
1734 let mut scratch = Vec::new();
1735 self.collect(shape, focus, &mut scratch, visited, &[]);
1736 scratch.is_empty()
1737 }
1738
1739 fn value_checks(
1742 &self,
1743 shape: &NamedOrBlankNode,
1744 u: &Term,
1745 visited: &mut Visited,
1746 ) -> Vec<(NamedNode, bool)> {
1747 let mut checks = Vec::new();
1748
1749 for c in self.shapes.objects(shape, vocab::SH_CLASS) {
1750 checks.push((vocab::SH_CC_CLASS.into_owned(), self.is_instance(u, &c)));
1751 }
1752 for d in self.shapes.objects(shape, vocab::SH_DATATYPE) {
1753 if let Term::NamedNode(dt) = d {
1754 let ok = value_type_holds(&ValueType::Datatype(dt), u);
1755 checks.push((vocab::SH_CC_DATATYPE.into_owned(), ok));
1756 }
1757 }
1758 for k in self.shapes.objects(shape, vocab::SH_NODE_KIND) {
1759 if let Some(set) = map_node_kind(&k) {
1760 checks.push((vocab::SH_CC_NODE_KIND.into_owned(), set.matches(u)));
1761 }
1762 }
1763 for (pred_iri, comp, inclusive) in [
1765 (vocab::SH_MIN_INCLUSIVE, vocab::SH_CC_MIN_INCLUSIVE, true),
1766 (vocab::SH_MIN_EXCLUSIVE, vocab::SH_CC_MIN_EXCLUSIVE, false),
1767 ] {
1768 if let Some(Term::Literal(b)) = self.shapes.object(shape, pred_iri) {
1769 let vt = ValueType::NumericRange {
1770 lo: Some(Bound {
1771 value: b,
1772 inclusive,
1773 }),
1774 hi: None,
1775 };
1776 checks.push((comp.into_owned(), value_type_holds(&vt, u)));
1777 }
1778 }
1779 for (pred_iri, comp, inclusive) in [
1780 (vocab::SH_MAX_INCLUSIVE, vocab::SH_CC_MAX_INCLUSIVE, true),
1781 (vocab::SH_MAX_EXCLUSIVE, vocab::SH_CC_MAX_EXCLUSIVE, false),
1782 ] {
1783 if let Some(Term::Literal(b)) = self.shapes.object(shape, pred_iri) {
1784 let vt = ValueType::NumericRange {
1785 lo: None,
1786 hi: Some(Bound {
1787 value: b,
1788 inclusive,
1789 }),
1790 };
1791 checks.push((comp.into_owned(), value_type_holds(&vt, u)));
1792 }
1793 }
1794 let min_len = self.int(shape, vocab::SH_MIN_LENGTH);
1796 let max_len = self.int(shape, vocab::SH_MAX_LENGTH);
1797 if let Some(m) = min_len {
1798 let vt = ValueType::Length {
1799 min: Some(m),
1800 max: None,
1801 };
1802 checks.push((
1803 vocab::SH_CC_MIN_LENGTH.into_owned(),
1804 value_type_holds(&vt, u),
1805 ));
1806 }
1807 if let Some(m) = max_len {
1808 let vt = ValueType::Length {
1809 min: None,
1810 max: Some(m),
1811 };
1812 checks.push((
1813 vocab::SH_CC_MAX_LENGTH.into_owned(),
1814 value_type_holds(&vt, u),
1815 ));
1816 }
1817 if let Some(Term::Literal(re)) = self.shapes.object(shape, vocab::SH_PATTERN) {
1818 let flags = match self.shapes.object(shape, vocab::SH_FLAGS) {
1819 Some(Term::Literal(f)) => f.value().to_string(),
1820 _ => String::new(),
1821 };
1822 let vt = ValueType::Pattern {
1823 regex: re.value().to_string(),
1824 flags,
1825 };
1826 checks.push((vocab::SH_CC_PATTERN.into_owned(), value_type_holds(&vt, u)));
1827 }
1828 for list in self.shapes.objects(shape, vocab::SH_IN) {
1830 let members = self.shapes.read_list(&list);
1831 checks.push((vocab::SH_CC_IN.into_owned(), members.contains(u)));
1832 }
1833 for list in self.shapes.objects(shape, vocab::SH_LANGUAGE_IN) {
1834 let languages = self
1835 .shapes
1836 .read_list(&list)
1837 .into_iter()
1838 .filter_map(|term| match term {
1839 Term::Literal(literal) => Some(literal.value().to_string()),
1840 _ => None,
1841 })
1842 .collect();
1843 checks.push((
1844 vocab::SH_CC_LANGUAGE_IN.into_owned(),
1845 value_type_holds(&ValueType::LangIn(languages), u),
1846 ));
1847 }
1848
1849 for list in self.shapes.objects(shape, vocab::SH_AND) {
1851 let ok = self
1852 .shapes
1853 .read_list(&list)
1854 .iter()
1855 .filter_map(term_to_node)
1856 .all(|m| self.conforms(&m, u, visited));
1857 checks.push((vocab::SH_CC_AND.into_owned(), ok));
1858 }
1859 for list in self.shapes.objects(shape, vocab::SH_OR) {
1860 let ok = self
1861 .shapes
1862 .read_list(&list)
1863 .iter()
1864 .filter_map(term_to_node)
1865 .any(|m| self.conforms(&m, u, visited));
1866 checks.push((vocab::SH_CC_OR.into_owned(), ok));
1867 }
1868 for list in self.shapes.objects(shape, vocab::SH_XONE) {
1869 let count = self
1870 .shapes
1871 .read_list(&list)
1872 .iter()
1873 .filter_map(term_to_node)
1874 .filter(|m| self.conforms(m, u, visited))
1875 .count();
1876 checks.push((vocab::SH_CC_XONE.into_owned(), count == 1));
1877 }
1878 for n in self.shapes.objects(shape, vocab::SH_NOT) {
1879 if let Some(nn) = term_to_node(&n) {
1880 checks.push((
1881 vocab::SH_CC_NOT.into_owned(),
1882 !self.conforms(&nn, u, visited),
1883 ));
1884 }
1885 }
1886 for n in self.shapes.objects(shape, vocab::SH_NODE) {
1887 if let Some(nn) = term_to_node(&n) {
1888 checks.push((
1889 vocab::SH_CC_NODE.into_owned(),
1890 self.conforms(&nn, u, visited),
1891 ));
1892 }
1893 }
1894
1895 checks
1896 }
1897
1898 fn is_instance(&self, u: &Term, class: &Term) -> bool {
1899 succ(self.frozen(), u, &class_path()).contains(class)
1900 }
1901
1902 fn int(&self, s: &NamedOrBlankNode, p: NamedNodeRef) -> Option<u64> {
1903 match self.shapes.object(s, p) {
1904 Some(Term::Literal(l)) => l.value().parse().ok(),
1905 _ => None,
1906 }
1907 }
1908
1909 fn bool(&self, s: &NamedOrBlankNode, p: NamedNodeRef) -> bool {
1910 matches!(
1911 self.shapes.object(s, p),
1912 Some(Term::Literal(ref literal)) if matches!(literal.value(), "true" | "1")
1913 )
1914 }
1915
1916 fn severity(&self, shape: &NamedOrBlankNode) -> NamedNode {
1919 match self.shapes.object(shape, vocab::SH_SEVERITY) {
1920 Some(Term::NamedNode(n)) => n,
1921 _ => vocab::SH_VIOLATION.into_owned(),
1922 }
1923 }
1924}
1925
1926fn is_shape_node(shapes: &Loaded, node: &NamedOrBlankNode) -> bool {
1927 shapes.has_type(node, vocab::SH_NODE_SHAPE)
1928 || shapes.has_type(node, vocab::SH_PROPERTY_SHAPE)
1929 || [
1930 vocab::SH_PROPERTY,
1931 vocab::SH_NODE,
1932 vocab::SH_AND,
1933 vocab::SH_OR,
1934 vocab::SH_NOT,
1935 vocab::SH_XONE,
1936 vocab::SH_DATATYPE,
1937 vocab::SH_CLASS,
1938 vocab::SH_NODE_KIND,
1939 vocab::SH_IN,
1940 vocab::SH_HAS_VALUE,
1941 vocab::SH_PROPERTY,
1942 ]
1943 .iter()
1944 .any(|predicate| shapes.object(node, *predicate).is_some())
1945}
1946
1947fn shapes_reference_shapes_graph(shapes: &Loaded) -> bool {
1950 [vocab::SH_SELECT, vocab::SH_ASK].iter().any(|predicate| {
1951 shapes.graph.triples_for_predicate(*predicate).any(
1952 |t| matches!(t.object, oxrdf::TermRef::Literal(l) if l.value().contains("shapesGraph")),
1953 )
1954 })
1955}
1956
1957fn class_path() -> Path {
1958 Path::seq(vec![
1959 Path::Pred(vocab::rdf_type()),
1960 Path::star(Path::Pred(vocab::rdfs_subclassof())),
1961 ])
1962}
1963
1964fn build_class_index(
1973 focus_data: &Graph,
1974 frozen: &FrozenIndexedDataset,
1975) -> HashMap<Term, Vec<Term>> {
1976 let focus_nodes = graph_nodes(focus_data);
1977 let subclass_star = Path::star(Path::Pred(vocab::rdfs_subclassof()));
1978 let mut supers: HashMap<Term, Vec<Term>> = HashMap::new();
1979 let mut index: HashMap<Term, Vec<Term>> = HashMap::new();
1980 let mut seen: HashSet<(Term, Term)> = HashSet::new();
1981 for (node, ty) in frozen.triples_for_predicate(&vocab::rdf_type()) {
1982 if !focus_nodes.contains(&node) {
1983 continue;
1984 }
1985 let classes = supers
1986 .entry(ty.clone())
1987 .or_insert_with(|| succ(frozen, &ty, &subclass_star).into_iter().collect());
1988 for class in classes.iter() {
1989 if seen.insert((class.clone(), node.clone())) {
1990 index.entry(class.clone()).or_default().push(node.clone());
1991 }
1992 }
1993 }
1994 index
1995}
1996
1997fn graph_nodes(graph: &Graph) -> HashSet<Term> {
1998 let mut nodes = HashSet::new();
1999 for triple in graph.iter() {
2000 nodes.insert(node_term(triple.subject));
2001 nodes.insert(triple.object.into_owned());
2002 }
2003 nodes
2004}
2005
2006fn node_term(s: oxrdf::NamedOrBlankNodeRef) -> Term {
2007 crate::path::term_of(s.into_owned())
2008}
2009
2010fn node_term_ref(s: &NamedOrBlankNode) -> Term {
2011 match s {
2012 NamedOrBlankNode::NamedNode(n) => Term::NamedNode(n.clone()),
2013 NamedOrBlankNode::BlankNode(b) => Term::BlankNode(b.clone()),
2014 }
2015}
2016
2017fn map_node_kind(term: &Term) -> Option<NodeKindSet> {
2018 let Term::NamedNode(n) = term else {
2019 return None;
2020 };
2021 let r = n.as_ref();
2022 Some(if r == vocab::SH_IRI {
2023 NodeKindSet::IRI
2024 } else if r == vocab::SH_BLANK_NODE {
2025 NodeKindSet::BLANK_NODE
2026 } else if r == vocab::SH_LITERAL {
2027 NodeKindSet::LITERAL
2028 } else if r == vocab::SH_BLANK_NODE_OR_IRI {
2029 NodeKindSet::BLANK_NODE_OR_IRI
2030 } else if r == vocab::SH_BLANK_NODE_OR_LITERAL {
2031 NodeKindSet::BLANK_NODE_OR_LITERAL
2032 } else if r == vocab::SH_IRI_OR_LITERAL {
2033 NodeKindSet::IRI_OR_LITERAL
2034 } else {
2035 return None;
2036 })
2037}