1use std::collections::{HashMap, HashSet};
2
3use oxigraph::model::*;
4use oxigraph::sparql::QueryResults;
5
6use crate::compile::{CompiledShapes, Shape, Target};
7use crate::constraints::Constraint;
8use crate::functions::Functions;
9use crate::graph::*;
10use crate::path::Path;
11use crate::{Error, Result};
12
13#[cfg(test)]
14#[path = "execute_tests.rs"]
15mod coverage_tests;
16
17#[derive(Clone, Debug)]
18pub struct Options {
19 pub iterate_rules: bool,
20 pub max_rule_iterations: usize,
21 pub max_validation_depth: usize,
22 pub allow_infos: bool,
23 pub allow_warnings: bool,
24 pub abort_on_first: bool,
25 pub focus_nodes: Vec<Term>,
26 pub use_shapes: Vec<Term>,
27 pub namespaces: Vec<(String, String)>,
28}
29
30impl Default for Options {
31 fn default() -> Self {
32 Self {
33 iterate_rules: false,
34 max_rule_iterations: 100,
35 max_validation_depth: 15,
36 allow_infos: false,
37 allow_warnings: false,
38 abort_on_first: false,
39 focus_nodes: Vec::new(),
40 use_shapes: Vec::new(),
41 namespaces: Vec::new(),
42 }
43 }
44}
45
46pub struct ValidationReport {
47 pub conforms: bool,
48 pub graph: Dataset,
49 pub text: String,
50 pub recursion_warnings: Vec<String>,
51}
52
53pub struct RuleExpansion {
54 pub graph: Dataset,
55 pub recursion_warnings: Vec<String>,
56}
57
58#[derive(Clone, Copy)]
59struct EvaluationStep {
60 shape: usize,
61 component: &'static str,
62}
63
64struct Evaluation {
65 path_values: HashMap<(Path, Term), Vec<Term>>,
66 memberships: HashMap<(Term, Term), bool>,
67 class_targets: HashMap<Term, HashSet<Term>>,
68 cached_terms: usize,
69 functions: Functions,
70 path: Vec<EvaluationStep>,
71 max_depth: usize,
72 warnings: Vec<String>,
73 batch: Option<EvaluationBatch>,
74}
75
76#[derive(Clone, Copy)]
77struct EvaluationBatch {
78 shape: usize,
79 depth: usize,
80 has_values: bool,
81}
82
83impl Evaluation {
84 fn new(max_depth: usize) -> Self {
85 Self {
86 path_values: HashMap::new(),
87 memberships: HashMap::new(),
88 class_targets: HashMap::new(),
89 cached_terms: 0,
90 functions: Functions::default(),
91 path: Vec::new(),
92 max_depth,
93 warnings: Vec::new(),
94 batch: None,
95 }
96 }
97
98 fn clear_traversals(&mut self) {
99 self.cached_terms = 0;
100 self.path_values.clear();
101 self.memberships.clear();
102 self.class_targets.clear();
103 }
104
105 fn reserve_traversal(&mut self, terms: usize) -> bool {
106 if terms > 32768 {
107 return false;
108 }
109 if self.path_values.len() + self.memberships.len() + self.class_targets.len() >= 1024
110 || self.cached_terms + terms > 32768
111 {
112 self.clear_traversals();
113 }
114 self.cached_terms += terms;
115 true
116 }
117
118 fn values(&mut self, path: &Path, data: &Dataset, focus: &Term) -> Result<Vec<Term>> {
119 if matches!(path, Path::Predicate(_)) {
120 return path.values(data, focus);
121 }
122 let key = (path.clone(), focus.clone());
123 if let Some(values) = self.path_values.get(&key) {
124 return Ok(values.clone());
125 }
126 let values = path.values(data, focus)?;
127 if self.reserve_traversal(values.len()) {
128 self.path_values.insert(key, values.clone());
129 }
130 Ok(values)
131 }
132
133 fn is_instance(&mut self, data: &Dataset, value: &Term, class: &Term) -> Result<bool> {
134 let key = (value.clone(), class.clone());
135 if let Some(result) = self.memberships.get(&key) {
136 return Ok(*result);
137 }
138 let result = is_instance(data, value, class)?;
139 if self.reserve_traversal(1) {
140 self.memberships.insert(key, result);
141 }
142 Ok(result)
143 }
144
145 fn targets(&mut self, data: &Dataset, class: &Term) -> Result<HashSet<Term>> {
146 if let Some(nodes) = self.class_targets.get(class) {
147 return Ok(nodes.clone());
148 }
149 let nodes = instances(data, class)?;
150 if self.reserve_traversal(nodes.len()) {
151 self.class_targets.insert(class.clone(), nodes.clone());
152 }
153 Ok(nodes)
154 }
155
156 fn describe_path(&self, shapes: &[Shape], current: Option<EvaluationStep>) -> String {
157 self.path
158 .iter()
159 .copied()
160 .chain(current)
161 .map(|step| format!("{} -> {}", shapes[step.shape].node, step.component))
162 .collect::<Vec<_>>()
163 .join(" -> ")
164 }
165
166 fn skip_recursive(
168 &mut self,
169 shape: usize,
170 component: &'static str,
171 target: usize,
172 shapes: &[Shape],
173 ) -> bool {
174 let alternating = self.path.last().is_some_and(|previous| {
175 matches!(
176 (previous.component, component),
177 ("NodeConstraintComponent", "PropertyConstraintComponent")
178 | ("PropertyConstraintComponent", "NodeConstraintComponent")
179 )
180 });
181 let depth = self.path.len() + 1;
182 if depth < if alternating { 6 } else { 3 } {
183 return false;
184 }
185 let recursive = self.path.iter().enumerate().any(|(i, step)| {
186 step.shape == shape
187 && step.component == component
188 && self.path.get(i + 1).map_or(shape, |next| next.shape) == target
189 });
190 if recursive {
191 let path = self.describe_path(shapes, Some(EvaluationStep { shape, component }));
192 self.warnings.push(format!(
193 "Warning, A Recursive Shape was detected executing a recursive validation sequence {} levels deep. Backing out.\n{path}\nFor reference, see https://www.w3.org/TR/shacl/#shapes-recursion",
194 depth * 2
195 ));
196 }
197 recursive
198 }
199}
200
201struct Violation {
202 shape: usize,
203 focus: Term,
204 component: NamedNode,
205 message: String,
206 value: Option<Term>,
207 path: Option<Term>,
208 details: Vec<Violation>,
209 source_constraint: Option<Term>,
210 messages: Option<Vec<Term>>,
211}
212
213impl CompiledShapes {
214 pub fn validate_store(
217 &self,
218 store: &oxigraph::store::Store,
219 options: Options,
220 inplace: bool,
221 ) -> Result<ValidationReport> {
222 let before = store_snapshot(store)?;
223 let mut working = before.clone();
224 let report = self.validate(&mut working, options)?;
225 if inplace {
226 commit_store(store, &before, &working)?;
227 }
228 Ok(report)
229 }
230
231 pub fn expand_store(
233 &self,
234 store: &oxigraph::store::Store,
235 options: Options,
236 inplace: bool,
237 ) -> Result<RuleExpansion> {
238 let before = store_snapshot(store)?;
239 let mut working = before.clone();
240 let recursion_warnings = self.expand_rules(&mut working, options)?;
241 if inplace {
242 commit_store(store, &before, &working)?;
243 }
244 Ok(RuleExpansion {
245 graph: working,
246 recursion_warnings,
247 })
248 }
249
250 pub fn shape_focus_nodes(
253 &self,
254 node: &str,
255 data: &Dataset,
256 advanced: bool,
257 ) -> Result<Vec<Term>> {
258 let shape = self
259 .shapes
260 .iter()
261 .find(|shape| shape.node.to_string() == node)
262 .ok_or_else(|| Error::Invalid(format!("Unknown shape: {node}")))?;
263 self.focus_nodes_with_advanced(shape, data, advanced, &mut Evaluation::new(15))
264 }
265
266 fn focus_nodes(
267 &self,
268 shape: &Shape,
269 data: &Dataset,
270 evaluation: &mut Evaluation,
271 ) -> Result<Vec<Term>> {
272 self.focus_nodes_with_advanced(shape, data, true, evaluation)
273 }
274
275 pub fn targeting_index(
278 &self,
279 data: &Dataset,
280 skip: &HashSet<Term>,
281 ) -> Result<HashMap<Term, Vec<Term>>> {
282 let mut evaluation = Evaluation::new(15);
283 let mut index: HashMap<Term, Vec<Term>> = HashMap::new();
284 for shape in &self.shapes {
285 if shape.deactivated || shape.path.is_some() || skip.contains(&shape.node) {
286 continue;
287 }
288 for node in self.focus_nodes(shape, data, &mut evaluation)? {
289 index.entry(node).or_default().push(shape.node.clone());
290 }
291 }
292 for shapes in index.values_mut() {
293 shapes.sort_by_key(ToString::to_string);
294 }
295 Ok(index)
296 }
297
298 fn focus_nodes_with_advanced(
299 &self,
300 shape: &Shape,
301 data: &Dataset,
302 advanced: bool,
303 evaluation: &mut Evaluation,
304 ) -> Result<Vec<Term>> {
305 let mut nodes = HashSet::new();
306 for target in &shape.targets {
307 match target {
308 Target::Node(node) => {
309 nodes.insert(node.clone());
310 }
311 Target::Class(class) => {
312 nodes.extend(evaluation.targets(data, class)?);
313 }
314 Target::Subjects(predicate) => {
315 nodes.extend(subjects(data, predicate, None)?);
316 }
317 Target::Objects(predicate) => {
318 for quad in data.quads_for_pattern(
319 None,
320 Some(predicate.as_ref()),
321 None,
322 Some(GraphNameRef::DefaultGraph),
323 ) {
324 nodes.insert(quad.object.into_owned());
325 }
326 }
327 Target::Sparql { query, bindings } => {
328 if !advanced {
329 continue;
330 }
331 let functions = self.functions.for_dataset(data);
332 let mut evaluation = query.prepare(&functions, data).on_queryable_dataset(data);
333 for (variable, value) in bindings {
334 evaluation =
335 evaluation.substitute_variable(variable.clone(), value.clone());
336 }
337 let QueryResults::Solutions(solutions) = evaluation.execute()? else {
338 return Err(Error::Invalid(
339 "SPARQL target did not return SELECT results".into(),
340 ));
341 };
342 if !solutions.variables().iter().any(|v| v.as_str() == "this") {
343 return Err(Error::Invalid("SPARQL target must project ?this".into()));
344 }
345 for solution in solutions {
346 let solution = solution?;
347 let node = solution.get("this").ok_or_else(|| {
348 Error::Invalid("SPARQL target returned an unbound ?this".into())
349 })?;
350 nodes.insert(node.clone());
351 }
352 }
353 }
354 }
355 let mut nodes: Vec<_> = nodes.into_iter().collect();
356 nodes.sort_by_key(ToString::to_string);
357 Ok(nodes)
358 }
359
360 #[allow(clippy::too_many_arguments)]
361 fn evaluate_child(
362 &self,
363 parent: usize,
364 component: &'static str,
365 index: usize,
366 focus: &Term,
367 data: &Dataset,
368 results: &mut Vec<Violation>,
369 evaluation: &mut Evaluation,
370 ) -> Result<()> {
371 evaluation.path.push(EvaluationStep {
372 shape: parent,
373 component,
374 });
375 let result = self.evaluate_batch(
376 index,
377 std::slice::from_ref(focus),
378 data,
379 results,
380 evaluation,
381 );
382 evaluation.path.pop();
383 result
384 }
385
386 fn evaluate_batch(
387 &self,
388 index: usize,
389 focus: &[Term],
390 data: &Dataset,
391 results: &mut Vec<Violation>,
392 evaluation: &mut Evaluation,
393 ) -> Result<()> {
394 let shape = &self.shapes[index];
395 let batch = if focus.len() > 1
396 && !shape.deactivated
397 && shape
398 .constraints
399 .iter()
400 .any(|constraint| matches!(constraint, Constraint::Qualified { .. }))
401 {
402 let mut has_values = shape.path.is_none();
406 if let Some(path) = &shape.path {
407 for node in focus {
408 if !evaluation.values(path, data, node)?.is_empty() {
409 has_values = true;
410 break;
411 }
412 }
413 }
414 Some(EvaluationBatch {
415 shape: index,
416 depth: evaluation.path.len(),
417 has_values,
418 })
419 } else {
420 None
421 };
422 let previous = std::mem::replace(&mut evaluation.batch, batch);
423 let result = focus
424 .iter()
425 .try_for_each(|node| self.evaluate_inner(index, node, data, results, evaluation));
426 evaluation.batch = previous;
427 result
428 }
429
430 fn evaluate_inner(
431 &self,
432 index: usize,
433 focus: &Term,
434 data: &Dataset,
435 results: &mut Vec<Violation>,
436 evaluation: &mut Evaluation,
437 ) -> Result<()> {
438 let functions = evaluation.functions.clone();
439 let shape = &self.shapes[index];
440 if shape.deactivated {
441 return Ok(());
442 }
443 if evaluation.path.len() >= evaluation.max_depth {
444 return Err(Error::Runtime(format!(
445 "Validation path too deep!\n{}",
446 evaluation.describe_path(&self.shapes, None)
447 )));
448 }
449 let values = if let Some(path) = &shape.path {
450 evaluation.values(path, data, focus)?
451 } else {
452 vec![focus.clone()]
453 };
454 let violation = |component, value: Option<&Term>| Violation {
455 shape: index,
456 focus: focus.clone(),
457 component: iri(SH, component),
458 message: String::new(),
459 value: value.cloned(),
460 path: shape.path_node.clone(),
461 details: Vec::new(),
462 source_constraint: None,
463 messages: None,
464 };
465 for constraint in &shape.constraints {
466 match constraint {
467 Constraint::NodeExpression {
468 expression,
469 node,
470 messages,
471 } => {
472 for value in &values {
473 let nodes = expression.evaluate(value, data, &functions, &mut |i, n| {
474 let mut nested = Vec::new();
475 self.evaluate_child(
476 index,
477 "ExpressionConstraintComponent",
478 i,
479 n,
480 data,
481 &mut nested,
482 evaluation,
483 )?;
484 Ok(nested.is_empty())
485 })?;
486 if nodes != HashSet::from([Literal::from(true).into()]) {
487 let mut result =
488 violation("ExpressionConstraintComponent", Some(value));
489 result.source_constraint = Some(node.clone());
490 if !messages.is_empty() {
491 result.messages = Some(messages.clone());
492 }
493 results.push(result);
494 }
495 }
496 }
497 Constraint::Sparql(query) => {
498 let inputs = if query.per_value {
499 values.iter().map(Some).collect()
500 } else {
501 vec![None]
502 };
503 for input in inputs {
504 for row in query.evaluate(
505 data,
506 &self.graph,
507 &shape.node,
508 focus,
509 &shape.messages,
510 input,
511 &functions,
512 )? {
513 let mut result =
514 violation("SPARQLConstraintComponent", row.value.as_ref());
515 result.focus = row.focus;
516 if result.value.is_none() && shape.path.is_none() {
517 result.value = Some(focus.clone());
518 }
519 if query.per_value && shape.path.is_some() {
520 result.value = None;
523 }
524 if row.path.is_some() {
525 result.path = row.path;
526 }
527 if let Some(component) = &query.component {
528 result.component = component.clone();
529 } else {
530 result.source_constraint = Some(query.node.clone());
531 }
532 if !row.messages.is_empty() {
533 result.messages = Some(row.messages);
534 }
535 results.push(result);
536 }
537 }
538 }
539 Constraint::Count { minimum, count } => {
540 if !count.allows(values.len(), *minimum) {
541 results.push(violation(
542 if *minimum {
543 "MinCountConstraintComponent"
544 } else {
545 "MaxCountConstraintComponent"
546 },
547 None,
548 ));
549 }
550 }
551 Constraint::HasValue(required) => {
552 if !values.contains(required) {
553 results.push(violation("HasValueConstraintComponent", None));
554 }
555 }
556 Constraint::Class(class) => {
557 for value in &values {
558 if !evaluation.is_instance(data, value, class)? {
559 results.push(violation("ClassConstraintComponent", Some(value)));
560 }
561 }
562 }
563 Constraint::Datatype(datatype) => {
564 for value in &values {
565 if !crate::value::datatype(value, datatype) {
566 results.push(violation("DatatypeConstraintComponent", Some(value)));
567 }
568 }
569 }
570 Constraint::NodeKind(kind) => {
571 for value in &values {
572 let valid = match value {
573 Term::NamedNode(_) => {
574 ["IRI", "BlankNodeOrIRI", "IRIOrLiteral"].contains(&kind.as_str())
575 }
576 Term::BlankNode(_) => {
577 ["BlankNode", "BlankNodeOrIRI", "BlankNodeOrLiteral"]
578 .contains(&kind.as_str())
579 }
580 Term::Literal(_) => ["Literal", "BlankNodeOrLiteral", "IRIOrLiteral"]
581 .contains(&kind.as_str()),
582 };
583 if !valid {
584 results.push(violation("NodeKindConstraintComponent", Some(value)));
585 }
586 }
587 }
588 Constraint::Pattern(regex) => {
589 for value in &values {
590 let valid = crate::value::lexical(value)
591 .map(|text| regex.is_match(text))
592 .transpose()?
593 .unwrap_or(false);
594 if !valid {
595 results.push(violation("PatternConstraintComponent", Some(value)));
596 }
597 }
598 }
599 Constraint::Expression { component, query } => {
600 for value in &values {
601 if !crate::value::expression(query, data, value, None)? {
602 results.push(violation(component, Some(value)));
603 }
604 }
605 }
606 Constraint::Length { minimum, count } => {
607 for value in &values {
608 let valid = (*minimum && count.is_zero())
609 || crate::value::lexical(value)
610 .is_some_and(|s| count.allows(s.chars().count(), *minimum));
611 if !valid {
612 results.push(violation(
613 if *minimum {
614 "MinLengthConstraintComponent"
615 } else {
616 "MaxLengthConstraintComponent"
617 },
618 Some(value),
619 ));
620 }
621 }
622 }
623 Constraint::LanguageIn(ranges) => {
624 for value in &values {
625 let valid = if let Term::Literal(l) = value {
626 l.language().is_some_and(|lang| {
627 ranges.iter().any(|r| {
628 r == "*"
629 || lang == r
630 || lang
631 .strip_prefix(r)
632 .is_some_and(|suffix| suffix.starts_with('-'))
633 })
634 })
635 } else {
636 false
637 };
638 if !valid {
639 results.push(violation("LanguageInConstraintComponent", Some(value)));
640 }
641 }
642 }
643 Constraint::UniqueLang => {
644 let mut seen = HashSet::new();
645 let mut repeated = HashSet::new();
646 for value in &values {
647 if let Term::Literal(l) = value
648 && let Some(lang) = l.language()
649 && !seen.insert(lang)
650 {
651 repeated.insert(lang);
652 }
653 }
654 for _ in repeated {
655 results.push(violation("UniqueLangConstraintComponent", None));
656 }
657 }
658 Constraint::In(allowed) => {
659 for value in &values {
660 if !allowed.contains(value) {
661 results.push(violation("InConstraintComponent", Some(value)));
662 }
663 }
664 }
665 Constraint::Pair {
666 kind,
667 predicate,
668 query,
669 } => {
670 let others = objects(data, focus, predicate)?;
671 if let Some(query) = query {
672 for value in &values {
673 for other in &others {
674 if !crate::value::expression(query, data, value, Some(other))? {
675 results.push(violation(kind, Some(value)));
676 }
677 }
678 }
679 } else if *kind == "EqualsConstraintComponent" {
680 for value in values
681 .iter()
682 .filter(|v| !others.contains(v))
683 .chain(others.iter().filter(|v| !values.contains(v)))
684 {
685 results.push(violation(kind, Some(value)));
686 }
687 } else {
688 for value in values.iter().filter(|v| others.contains(v)) {
689 results.push(violation(kind, Some(value)));
690 }
691 }
692 }
693 Constraint::Node(target) => {
694 if !values.is_empty()
695 && evaluation.skip_recursive(
696 index,
697 "NodeConstraintComponent",
698 *target,
699 &self.shapes,
700 )
701 {
702 continue;
703 }
704 for value in &values {
705 let mut details = Vec::new();
706 self.evaluate_child(
707 index,
708 "NodeConstraintComponent",
709 *target,
710 value,
711 data,
712 &mut details,
713 evaluation,
714 )?;
715 if !details.is_empty() {
716 let mut parent = violation("NodeConstraintComponent", Some(value));
717 parent.details = details;
718 results.push(parent);
719 }
720 }
721 }
722 Constraint::Logical { kind, shapes } => {
723 if *kind == "NotConstraintComponent"
726 && shapes.iter().any(|&target| {
727 evaluation.skip_recursive(index, kind, target, &self.shapes)
728 })
729 {
730 continue;
731 }
732 for value in &values {
733 let mut count = 0;
734 for &target in shapes {
735 let mut nested = Vec::new();
736 self.evaluate_child(
737 index,
738 kind,
739 target,
740 value,
741 data,
742 &mut nested,
743 evaluation,
744 )?;
745 count += usize::from(nested.is_empty());
746 }
747 let valid = match *kind {
748 "NotConstraintComponent" => count == 0,
749 "AndConstraintComponent" => count == shapes.len(),
750 "OrConstraintComponent" => count > 0,
751 "XoneConstraintComponent" => count == 1,
752 _ => unreachable!(),
753 };
754 if !valid {
755 results.push(violation(kind, Some(value)));
756 }
757 }
758 }
759 Constraint::Qualified {
760 shape: target,
761 minimum,
762 maximum,
763 disjoint,
764 siblings,
765 } => {
766 let has_values = evaluation
767 .batch
768 .filter(|batch| {
769 batch.shape == index && batch.depth == evaluation.path.len()
770 })
771 .map_or(!values.is_empty(), |batch| batch.has_values);
772 if !has_values
773 && !disjoint
774 && minimum.as_ref().is_none_or(|min| min.allows(0, true))
775 {
776 continue;
777 }
778 if evaluation.skip_recursive(
779 index,
780 "QualifiedValueShapeConstraintComponent",
781 *target,
782 &self.shapes,
783 ) {
784 continue;
785 }
786 let mut count = 0usize;
787 for value in &values {
788 let mut nested = Vec::new();
789 self.evaluate_child(
790 index,
791 "QualifiedValueShapeConstraintComponent",
792 *target,
793 value,
794 data,
795 &mut nested,
796 evaluation,
797 )?;
798 if !nested.is_empty() {
799 continue;
800 }
801 let mut disjoint = true;
802 for &sibling in siblings {
803 let mut sibling_results = Vec::new();
804 self.evaluate_child(
805 index,
806 "QualifiedValueShapeConstraintComponent",
807 sibling,
808 value,
809 data,
810 &mut sibling_results,
811 evaluation,
812 )?;
813 if sibling_results.is_empty() {
814 disjoint = false;
815 break;
816 }
817 }
818 count += usize::from(disjoint);
819 }
820 if minimum.as_ref().is_some_and(|min| !min.allows(count, true)) {
821 results.push(violation("QualifiedMinCountConstraintComponent", None));
822 }
823 if maximum
824 .as_ref()
825 .is_some_and(|max| !max.allows(count, false))
826 {
827 results.push(violation("QualifiedMaxCountConstraintComponent", None));
828 }
829 }
830 Constraint::Closed(allowed) => {
831 for value in &values {
832 let Ok(node) = subject(value) else {
833 continue;
834 };
835 for quad in data.quads_for_pattern(
836 Some(node.as_ref()),
837 None,
838 None,
839 Some(GraphNameRef::DefaultGraph),
840 ) {
841 let quad = quad.into_owned();
842 if quad.predicate == iri(RDF, "type")
843 && quad.object == Term::from(iri(RDFS, "Resource"))
844 {
845 continue;
846 }
847 if !allowed.contains(&quad.predicate) {
848 let mut result =
849 violation("ClosedConstraintComponent", Some(&quad.object));
850 result.path = Some(quad.predicate.into());
851 results.push(result);
852 }
853 }
854 }
855 }
856 }
857 }
858 for &property in &shape.properties {
859 if !values.is_empty()
860 && evaluation.skip_recursive(
861 index,
862 "PropertyConstraintComponent",
863 property,
864 &self.shapes,
865 )
866 {
867 continue;
868 }
869 for value in &values {
870 self.evaluate_child(
871 index,
872 "PropertyConstraintComponent",
873 property,
874 value,
875 data,
876 results,
877 evaluation,
878 )?;
879 }
880 }
881 Ok(())
882 }
883
884 pub fn has_active_rules(&self, options: &Options) -> bool {
888 self.execution_order.iter().any(|&index| {
889 let shape = &self.shapes[index];
890 (options.use_shapes.is_empty() || options.use_shapes.contains(&shape.node))
891 && shape.rules.iter().any(|rule| !rule.deactivated)
892 })
893 }
894
895 pub fn expand_rules(&self, data: &mut Dataset, options: Options) -> Result<Vec<String>> {
896 if self.has_active_rules(&options) && data.iter().any(|q| !q.graph_name.is_default_graph())
897 {
898 return Ok(self.expand_rules_delta(data, options)?.recursion_warnings);
899 }
900 self.expand_rules_inner(data, options, None)
901 }
902
903 pub fn expand_rules_delta(
905 &self,
906 data: &mut Dataset,
907 options: Options,
908 ) -> Result<RuleExpansion> {
909 let mut additions = Dataset::new();
910 let recursion_warnings = if self.has_active_rules(&options)
911 && data.iter().any(|q| !q.graph_name.is_default_graph())
912 {
913 let mut working = with_union(data);
914 let warnings = self.expand_rules_inner(&mut working, options, Some(&mut additions))?;
915 data.extend(additions.iter());
916 warnings
917 } else {
918 self.expand_rules_inner(data, options, Some(&mut additions))?
919 };
920 Ok(RuleExpansion {
921 graph: additions,
922 recursion_warnings,
923 })
924 }
925
926 fn expand_rules_inner(
927 &self,
928 data: &mut Dataset,
929 options: Options,
930 mut delta: Option<&mut Dataset>,
931 ) -> Result<Vec<String>> {
932 if options.max_rule_iterations == 0 {
933 return Err(Error::Invalid(
934 "max_rule_iterations must be positive".into(),
935 ));
936 }
937 let mut evaluation = Evaluation::new(options.max_validation_depth);
938 for &index in &self.execution_order {
939 let shape = &self.shapes[index];
940 if !options.use_shapes.is_empty() && !options.use_shapes.contains(&shape.node) {
941 continue;
942 }
943 if shape.rules.is_empty() {
944 continue;
945 }
946 let mut pass = 0;
947 loop {
948 if pass == options.max_rule_iterations {
949 return Err(Error::Runtime(format!(
950 "Rule iteration limit ({}) exceeded for {}",
951 options.max_rule_iterations, shape.node
952 )));
953 }
954 pass += 1;
955 let mut changed = false;
956 for rule in &shape.rules {
957 if rule.deactivated {
958 continue;
959 }
960 let focus =
961 self.selected_focus_nodes(shape, data, &options, &mut evaluation)?;
962 let functions = self.functions.for_dataset(data);
963 evaluation.functions = functions.clone();
964 let mut additions = HashSet::new();
965 for node in focus {
966 let mut condition_results = Vec::new();
967 for &condition in &rule.conditions {
968 self.evaluate_batch(
969 condition,
970 std::slice::from_ref(&node),
971 data,
972 &mut condition_results,
973 &mut evaluation,
974 )?;
975 }
976 if !condition_results.is_empty() {
977 continue;
978 }
979 if let Some(expressions) = &rule.triple {
980 let mut filter = |i, n: &Term| {
981 let mut results = Vec::new();
982 self.evaluate_batch(
983 i,
984 std::slice::from_ref(n),
985 data,
986 &mut results,
987 &mut evaluation,
988 )?;
989 Ok(results.is_empty())
990 };
991 let subjects =
992 expressions[0].evaluate(&node, data, &functions, &mut filter)?;
993 let predicates =
994 expressions[1].evaluate(&node, data, &functions, &mut filter)?;
995 let objects =
996 expressions[2].evaluate(&node, data, &functions, &mut filter)?;
997 for s in subjects {
998 for p in &predicates {
999 for o in &objects {
1000 additions.insert(Quad::new(
1001 subject(&s)?,
1002 crate::constraints::named(
1003 p.clone(),
1004 "TripleRule predicate",
1005 )?,
1006 o.clone(),
1007 GraphName::DefaultGraph,
1008 ));
1009 }
1010 }
1011 }
1012 }
1013 for query in &rule.queries {
1014 let result = query
1015 .prepare(&functions, data)
1016 .on_queryable_dataset(&*data)
1017 .substitute_variable(Variable::new_unchecked("this"), node.clone())
1018 .execute()?;
1019 let QueryResults::Graph(triples) = result else {
1020 return Err(Error::Invalid(
1021 "SPARQL rule did not return CONSTRUCT triples".into(),
1022 ));
1023 };
1024 for triple in triples {
1025 let triple = triple?;
1026 additions.insert(Quad::new(
1027 triple.subject,
1028 triple.predicate,
1029 triple.object,
1030 GraphName::DefaultGraph,
1031 ));
1032 }
1033 }
1034 }
1035 let mut rule_changed = false;
1038 for quad in additions {
1039 if data.insert(&quad) {
1040 rule_changed = true;
1041 if let Some(delta) = delta.as_deref_mut() {
1042 delta.insert(&quad);
1043 }
1044 changed = true;
1045 }
1046 }
1047 if rule_changed {
1048 evaluation.clear_traversals();
1049 }
1050 }
1051 if !options.iterate_rules || !changed {
1052 break;
1053 }
1054 }
1055 }
1056 Ok(evaluation.warnings)
1057 }
1058
1059 fn selected_focus_nodes(
1060 &self,
1061 shape: &Shape,
1062 data: &Dataset,
1063 options: &Options,
1064 evaluation: &mut Evaluation,
1065 ) -> Result<Vec<Term>> {
1066 if !options.use_shapes.is_empty() && !options.focus_nodes.is_empty() {
1067 return Ok(options.focus_nodes.clone());
1068 }
1069 let mut nodes = self.focus_nodes(shape, data, evaluation)?;
1070 if !options.focus_nodes.is_empty() {
1071 nodes.retain(|node| options.focus_nodes.contains(node));
1072 }
1073 Ok(nodes)
1074 }
1075
1076 pub fn validate(&self, data: &mut Dataset, options: Options) -> Result<ValidationReport> {
1078 let warnings = self.expand_rules(data, options.clone())?;
1079 self.validate_expanded(data, options, warnings)
1080 }
1081
1082 pub fn validate_readonly(&self, data: &Dataset, options: Options) -> Result<ValidationReport> {
1085 if options.max_rule_iterations == 0 {
1086 return Err(Error::Invalid(
1087 "max_rule_iterations must be positive".into(),
1088 ));
1089 }
1090 if self.has_active_rules(&options) {
1091 return Err(Error::Invalid(
1092 "Readonly validation cannot execute active rules".into(),
1093 ));
1094 }
1095 self.validate_expanded(data, options, Vec::new())
1096 }
1097
1098 fn validate_expanded(
1099 &self,
1100 data: &Dataset,
1101 options: Options,
1102 warnings: Vec<String>,
1103 ) -> Result<ValidationReport> {
1104 let mut evaluation = Evaluation::new(options.max_validation_depth);
1105 evaluation.warnings = warnings;
1106 let display_data = data;
1107 let joined;
1108 let data = if data.iter().any(|q| !q.graph_name.is_default_graph()) {
1109 joined = with_union(data);
1110 &joined
1111 } else {
1112 data
1113 };
1114 evaluation.functions = self.functions.for_dataset(data);
1115 let mut violations = Vec::new();
1116 for &index in &self.execution_order {
1117 let shape = &self.shapes[index];
1118 if !options.use_shapes.is_empty() && !options.use_shapes.contains(&shape.node) {
1119 continue;
1120 }
1121 if shape.deactivated {
1122 continue;
1123 }
1124 let focus_nodes = self.selected_focus_nodes(shape, data, &options, &mut evaluation)?;
1125 self.evaluate_batch(index, &focus_nodes, data, &mut violations, &mut evaluation)?;
1126 if options.abort_on_first && !violations.is_empty() {
1127 break;
1128 }
1129 }
1130 let conforms = violations.iter().all(|v| {
1131 let severity = &self.shapes[v.shape].severity;
1132 (options.allow_warnings && severity == &iri(SH, "Warning"))
1133 || ((options.allow_infos || options.allow_warnings) && severity == &iri(SH, "Info"))
1134 });
1135 let mut report = Dataset::new();
1136 let root: Term = BlankNode::default().into();
1137 add(
1138 &mut report,
1139 &root,
1140 iri(RDF, "type"),
1141 iri(SH, "ValidationReport").into(),
1142 )?;
1143 add(
1144 &mut report,
1145 &root,
1146 iri(SH, "conforms"),
1147 Literal::from(conforms).into(),
1148 )?;
1149 let mut text = format!(
1150 "Validation Report\nConforms: {}\n",
1151 if conforms { "True" } else { "False" }
1152 );
1153 if !violations.is_empty() {
1154 text.push_str(&format!("Results ({}):\n", violations.len()));
1155 }
1156 let mut shape_namespaces = self.namespaces.clone();
1157 let mut data_namespaces = options.namespaces;
1158 for (p, ns) in [("sh", SH), ("rdf", RDF), ("rdfs", RDFS), ("xsd", XSD)] {
1159 shape_namespaces.push((p.into(), ns.into()));
1160 data_namespaces.push((p.into(), ns.into()));
1161 }
1162 let display = crate::display::Display {
1163 shape_namespaces,
1164 data_namespaces,
1165 data: display_data,
1166 shapes: &self.graph,
1167 };
1168 let mut clones = ReportClones::default();
1169 let mut descriptions = Vec::new();
1170 for violation in &violations {
1171 let mut description = String::new();
1172 self.write_result(
1173 &mut report,
1174 &root,
1175 "result",
1176 violation,
1177 data,
1178 &mut description,
1179 &mut clones,
1180 &display,
1181 )?;
1182 descriptions.push(description);
1183 }
1184 descriptions.sort();
1185 for description in descriptions {
1186 text.push_str(&description);
1187 }
1188 Ok(ValidationReport {
1189 conforms,
1190 graph: report,
1191 text,
1192 recursion_warnings: evaluation.warnings,
1193 })
1194 }
1195 #[allow(clippy::too_many_arguments)]
1196 fn write_result(
1197 &self,
1198 report: &mut Dataset,
1199 parent: &Term,
1200 edge: &str,
1201 violation: &Violation,
1202 data: &Dataset,
1203 text: &mut String,
1204 clones: &mut ReportClones,
1205 display: &crate::display::Display<'_>,
1206 ) -> Result<()> {
1207 let shape = &self.shapes[violation.shape];
1208 let result: Term = BlankNode::default().into();
1209 add(report, parent, iri(SH, edge), result.clone())?;
1210 for (predicate, object) in [
1211 (iri(RDF, "type"), iri(SH, "ValidationResult").into()),
1212 (
1213 iri(SH, "sourceConstraintComponent"),
1214 violation.component.clone().into(),
1215 ),
1216 (iri(SH, "sourceShape"), shape.node.clone()),
1217 (iri(SH, "resultSeverity"), shape.severity.clone().into()),
1218 (iri(SH, "focusNode"), violation.focus.clone()),
1219 ] {
1220 add(report, &result, predicate, object)?;
1221 }
1222 clones.copy(&self.graph, report, &shape.node)?;
1223 clones.copy(data, report, &violation.focus)?;
1224 if let Some(path) = &violation.path {
1225 add(report, &result, iri(SH, "resultPath"), path.clone())?;
1226 clones.copy(&self.graph, report, path)?;
1227 }
1228 if let Some(value) = &violation.value {
1229 add(report, &result, iri(SH, "value"), value.clone())?;
1230 clones.copy(data, report, value)?;
1231 }
1232 if let Some(constraint) = &violation.source_constraint {
1233 add(
1234 report,
1235 &result,
1236 iri(SH, "sourceConstraint"),
1237 constraint.clone(),
1238 )?;
1239 clones.copy(&self.graph, report, constraint)?;
1240 }
1241 let messages = if let Some(messages) = &violation.messages {
1242 messages.clone()
1243 } else if shape.messages.is_empty() {
1244 let message = if !violation.message.is_empty() {
1245 violation.message.clone()
1246 } else {
1247 let focus = display.term(&violation.focus, false);
1248 let component = violation
1249 .component
1250 .as_str()
1251 .strip_prefix(SH)
1252 .unwrap_or(violation.component.as_str());
1253 match component {
1254 "MinCountConstraintComponent" | "MaxCountConstraintComponent" => {
1255 let minimum = component == "MinCountConstraintComponent";
1256 let count = shape
1257 .constraints
1258 .iter()
1259 .find_map(|constraint| match constraint {
1260 Constraint::Count {
1261 minimum: is_minimum,
1262 count,
1263 } if *is_minimum == minimum => Some(count),
1264 _ => None,
1265 })
1266 .expect("cardinality violation requires a compiled count constraint");
1267 let direction = if minimum { "Less" } else { "More" };
1268 let location = if let Some(path) = &violation.path {
1269 format!("{focus}->{}", display.term(path, true))
1270 } else {
1271 focus
1272 };
1273 format!("{direction} than {count} values on {location}")
1274 }
1275 "MinLengthConstraintComponent" | "MaxLengthConstraintComponent" => {
1276 let minimum = component == "MinLengthConstraintComponent";
1277 let count = shape
1278 .constraints
1279 .iter()
1280 .find_map(|constraint| match constraint {
1281 Constraint::Length {
1282 minimum: is_minimum,
1283 count,
1284 } if *is_minimum == minimum => Some(count),
1285 _ => None,
1286 })
1287 .expect("length violation requires a compiled length constraint");
1288 let number = count.number.to_string();
1289 let lexical = count.literal.value();
1290 let value = if lexical == number {
1291 format!("\"{lexical}\"")
1292 } else {
1293 format!("\"{lexical}\" = {number}")
1294 };
1295 let operator = if minimum { ">=" } else { "<=" };
1296 format!(
1297 "String length not {operator} Literal({value}, datatype={})",
1298 display.iri(&count.literal.datatype().into_owned())
1299 )
1300 }
1301 "QualifiedMinCountConstraintComponent"
1302 | "QualifiedMaxCountConstraintComponent" => {
1303 let (target, minimum, maximum) = shape
1304 .constraints
1305 .iter()
1306 .find_map(|constraint| match constraint {
1307 Constraint::Qualified {
1308 shape,
1309 minimum,
1310 maximum,
1311 ..
1312 } => Some((*shape, minimum, maximum)),
1313 _ => None,
1314 })
1315 .expect("qualified violation requires a compiled qualified constraint");
1316 let mut counts = String::new();
1317 if let Some(minimum) = minimum {
1318 counts.push_str(&format!(" MinCount {minimum}"));
1319 }
1320 if let Some(maximum) = maximum {
1321 counts.push_str(&format!(" MaxCount {maximum}"));
1322 }
1323 format!(
1324 "Focus node does not conform to shape{counts}: {}",
1325 display.term(&self.shapes[target].node, true)
1326 )
1327 }
1328 "PatternConstraintComponent" => {
1329 let patterns = values(&self.graph, &shape.node, "pattern")?
1330 .iter()
1331 .filter_map(crate::value::lexical)
1332 .map(str::to_owned)
1333 .collect::<Vec<_>>();
1334 if patterns.len() == 1 {
1335 format!("Value does not match pattern '{}'", patterns[0])
1336 } else {
1337 format!(
1338 "Value does not match every pattern in ('{}')",
1339 patterns.join("', '")
1340 )
1341 }
1342 }
1343 "ClassConstraintComponent" => {
1344 let mut classes = values(&self.graph, &shape.node, "class")?
1345 .iter()
1346 .map(|n| display.term(n, true))
1347 .collect::<Vec<_>>();
1348 classes.sort();
1349 if classes.len() == 1 {
1350 format!("Value does not have class {}", classes[0])
1351 } else {
1352 format!("Value class is not in classes ({})", classes.join(", "))
1353 }
1354 }
1355 "NodeConstraintComponent" => {
1356 let mut nodes = values(&self.graph, &shape.node, "node")?
1357 .iter()
1358 .map(|n| format!("'{}'", display.term(n, true)))
1359 .collect::<Vec<_>>();
1360 nodes.sort();
1361 if nodes.len() == 1 {
1362 format!(
1363 "Value does not conform to Shape {}. See details for more information.",
1364 nodes[0]
1365 )
1366 } else {
1367 format!(
1368 "Value must conform to every Shape in ({})",
1369 nodes.join(", ")
1370 )
1371 }
1372 }
1373 "NotConstraintComponent" => {
1374 let mut nodes = values(&self.graph, &shape.node, "not")?
1375 .iter()
1376 .map(|n| display.term(n, true))
1377 .collect::<Vec<_>>();
1378 nodes.sort();
1379 format!(
1380 "Node {focus} must not conform to shape {}",
1381 nodes.join(", ")
1382 )
1383 }
1384 _ => format!("{component} violated at {focus}"),
1385 }
1386 };
1387 vec![Literal::new_simple_literal(message).into()]
1388 } else {
1389 shape.messages.clone()
1390 };
1391 let component = violation
1392 .component
1393 .as_str()
1394 .strip_prefix(SH)
1395 .unwrap_or(violation.component.as_str());
1396 let title = if [
1397 "QualifiedMinCountConstraintComponent",
1398 "QualifiedMaxCountConstraintComponent",
1399 ]
1400 .contains(&component)
1401 {
1402 "QualifiedValueShapeConstraintComponent"
1403 } else {
1404 component
1405 };
1406 let category = if shape.severity == iri(SH, "Violation") {
1407 "Constraint Violation"
1408 } else {
1409 "Validation Result"
1410 };
1411 text.push_str(&format!(
1412 "{category} in {title} ({}):\n\tSeverity: {}\n\tSource Shape: {}\n\tFocus Node: {}\n",
1413 violation.component.as_str(),
1414 display.iri(&shape.severity),
1415 display.term(&shape.node, true),
1416 display.term(&violation.focus, false)
1417 ));
1418 if let Some(value) = &violation.value {
1419 text.push_str(&format!("\tValue Node: {}\n", display.term(value, false)));
1420 }
1421 if let Some(path) = &violation.path {
1422 text.push_str(&format!("\tResult Path: {}\n", display.term(path, true)));
1423 }
1424 if let Some(source) = &violation.source_constraint {
1425 text.push_str(&format!(
1426 "\tSource Constraint: {}\n",
1427 display.term(source, true)
1428 ));
1429 }
1430 let mut messages = messages;
1431 messages.sort_by_key(ToString::to_string);
1432 for message in messages {
1433 let printed = match &message {
1434 Term::Literal(l) => l.value().to_owned(),
1435 _ => message.to_string(),
1436 };
1437 text.push_str(&format!("\tMessage: {printed}\n"));
1438 add(report, &result, iri(SH, "resultMessage"), message)?;
1439 }
1440 let mut descriptions = Vec::new();
1441 for detail in &violation.details {
1442 let mut description = String::new();
1443 self.write_result(
1444 report,
1445 &result,
1446 "detail",
1447 detail,
1448 data,
1449 &mut description,
1450 clones,
1451 display,
1452 )?;
1453 descriptions.push(description);
1454 }
1455 descriptions.sort();
1456 if !descriptions.is_empty() {
1457 text.push_str("\tDetails:\n");
1458 }
1459 for description in descriptions {
1460 for line in description.lines() {
1461 text.push_str(&format!("\t\t{line}\n"));
1462 }
1463 }
1464 Ok(())
1465 }
1466}
1467
1468fn is_instance(data: &Dataset, value: &Term, class: &Term) -> Result<bool> {
1469 let mut pending = objects(data, value, &iri(RDF, "type"))?;
1470 let mut seen = HashSet::new();
1471 while let Some(current) = pending.pop() {
1472 if ¤t == class {
1473 return Ok(true);
1474 }
1475 if seen.insert(current.clone()) {
1476 pending.extend(objects(data, ¤t, &iri(RDFS, "subClassOf"))?);
1477 }
1478 }
1479 Ok(false)
1480}
1481
1482fn store_snapshot(store: &oxigraph::store::Store) -> Result<Dataset> {
1483 store
1484 .iter()
1485 .map(|q| q.map(store_quad))
1486 .collect::<std::result::Result<Dataset, _>>()
1487 .map_err(|e| Error::Runtime(format!("Store snapshot failed: {e}")))
1488}
1489
1490fn commit_store(store: &oxigraph::store::Store, before: &Dataset, after: &Dataset) -> Result<()> {
1491 store
1492 .extend(
1493 after
1494 .iter()
1495 .filter(|q| !before.contains(*q))
1496 .map(|q| q.into_owned()),
1497 )
1498 .map_err(|e| Error::Runtime(format!("Store commit failed: {e}")))
1499}
1500
1501#[cfg(test)]
1502mod union_tests {
1503 use super::*;
1504 use oxigraph::io::RdfFormat;
1505
1506 #[test]
1507 fn validation_without_active_rules_builds_only_its_validation_union() {
1508 let shapes = parse("@prefix sh:<http://www.w3.org/ns/shacl#> . <urn:S> sh:targetNode <urn:s>; sh:path <urn:p>; sh:minCount 1.", RdfFormat::Turtle, None).unwrap();
1509 let program = CompiledShapes::compile(shapes, true).unwrap();
1510 let mut data =
1511 parse_dataset("<urn:g> { <urn:s> <urn:p> 1 }", RdfFormat::TriG, None).unwrap();
1512 UNION_BUILDS.with(|count| count.set(0));
1513 assert!(
1514 program
1515 .validate(&mut data, Options::default())
1516 .unwrap()
1517 .conforms
1518 );
1519 assert_eq!(UNION_BUILDS.with(std::cell::Cell::get), 1);
1520 assert_eq!(data.len(), 1);
1521 assert!(!data.iter().any(|q| q.graph_name.is_default_graph()));
1522 assert!(
1523 program
1524 .expand_rules(
1525 &mut data,
1526 Options {
1527 max_rule_iterations: 0,
1528 ..Options::default()
1529 }
1530 )
1531 .is_err()
1532 );
1533 }
1534}
1535
1536#[cfg(test)]
1537mod traversal_tests {
1538 use super::*;
1539 use crate::path::{PATH_EVALUATIONS, Path};
1540
1541 #[test]
1542 fn traversal_cache_retention_is_bounded() {
1543 let mut evaluation = Evaluation::new(15);
1544 let path = Path::OneOrMore(Box::new(Path::Predicate(iri("urn:", "p"))));
1545 let data = Dataset::new();
1546 for index in 0..1025 {
1547 let focus = Term::from(iri("urn:node:", &index.to_string()));
1548 assert!(evaluation.values(&path, &data, &focus).unwrap().is_empty());
1549 assert!(evaluation.path_values.len() <= 1024);
1550 }
1551 assert!(!evaluation.reserve_traversal(32769));
1552 evaluation.clear_traversals();
1553 assert!(evaluation.path_values.is_empty());
1554 assert_eq!(evaluation.cached_terms, 0);
1555 }
1556
1557 #[test]
1558 fn property_paths_are_reused_only_until_the_phase_changes() {
1559 let mut evaluation = Evaluation::new(15);
1560 let path = Path::OneOrMore(Box::new(Path::Predicate(iri("urn:", "next"))));
1561 let focus = Term::from(iri("urn:", "s"));
1562 let mut data = parse(
1563 "<urn:s> <urn:next> <urn:t> .",
1564 oxigraph::io::RdfFormat::Turtle,
1565 None,
1566 )
1567 .unwrap();
1568 PATH_EVALUATIONS.with(|count| count.set(0));
1569 assert_eq!(evaluation.values(&path, &data, &focus).unwrap().len(), 1);
1570 assert_eq!(evaluation.values(&path, &data, &focus).unwrap().len(), 1);
1571 assert_eq!(PATH_EVALUATIONS.with(std::cell::Cell::get), 1);
1572 data.insert(
1573 Quad::new(
1574 iri("urn:", "t"),
1575 iri("urn:", "next"),
1576 iri("urn:", "u"),
1577 GraphName::DefaultGraph,
1578 )
1579 .as_ref(),
1580 );
1581 evaluation.clear_traversals();
1582 assert_eq!(evaluation.values(&path, &data, &focus).unwrap().len(), 2);
1583 assert_eq!(PATH_EVALUATIONS.with(std::cell::Cell::get), 2);
1584 }
1585}