1pub mod enumerate;
10pub mod frozen;
11pub mod gate;
12pub mod infer;
13mod native_exec;
14pub mod path;
15mod path_plan;
16pub mod profile;
17pub mod report;
18mod sparql;
19pub mod synthesize;
20pub mod validate;
21pub mod value;
22pub mod witness;
23
24pub use enumerate::{
25 EnumOptions, FixpointResult, RepairSolution, candidates, enumerate_repair, repair_to_fixpoint,
26};
27pub use gate::{RepairOutcome, apply, gate};
28pub use infer::{InferenceOutcome, infer, infer_graphs, infer_with_context};
29pub use report::{
30 ValidationReport, ValidationResult, report_to_graph, validate_report, validate_report_graphs,
31 validate_report_graphs_with_mode, validate_report_graphs_with_mode_and_options,
32 validate_report_with_options,
33};
34pub use synthesize::{synthesize, synthesize_focus};
35pub use validate::{
36 NonStratifiable, Reason, ValidationGraphMode, ValidationOptions, ValidationOutcome, Violation,
37 focus_nodes, validate, validate_graphs, validate_graphs_with_mode,
38 validate_graphs_with_mode_and_options, validate_plan, validate_plan_graphs,
39 validate_plan_graphs_with_mode, validate_plan_graphs_with_mode_and_options,
40 validate_plan_with_context, validate_plan_with_context_and_options, validate_plan_with_options,
41 validate_with_context, validate_with_context_and_options, validate_with_options,
42};
43pub use witness::{
44 BlockReason, FocusSat, FocusWitness, PathSupport, RelKind, SatTrace, Witness, satisfy_shape,
45 shape_id_for_iri, witness_node, witness_shape, witness_violations,
46};
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51 use oxrdf::Graph;
52 use shifty_parse::parse_turtle;
53
54 fn run(shapes_and_data: &str) -> ValidationOutcome {
55 let out = parse_turtle(shapes_and_data.as_bytes(), None).unwrap();
56 let loaded = shifty_parse::load_turtle(shapes_and_data.as_bytes(), None).unwrap();
58 validate(&loaded.graph, &out.schema).expect("stratifiable schema")
59 }
60
61 const PREFIXES: &str = r#"
62 @prefix sh: <http://www.w3.org/ns/shacl#> .
63 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
64 @prefix ex: <http://ex/> .
65 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
66 "#;
67
68 #[test]
69 fn inference_rules_fire_for_implicit_class_targets() {
70 let ttl = br#"
71 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
72 @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
73 @prefix owl: <http://www.w3.org/2002/07/owl#> .
74 @prefix sh: <http://www.w3.org/ns/shacl#> .
75 @prefix ex: <http://ex/> .
76
77 ex:Parent a owl:Class, sh:NodeShape ;
78 sh:rule [
79 a sh:TripleRule ;
80 sh:subject sh:this ;
81 sh:predicate ex:hasTag ;
82 sh:object ex:Tag
83 ] .
84
85 ex:Child rdfs:subClassOf ex:Parent .
86 ex:item a ex:Child .
87 "#;
88 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
89 let parsed = shifty_parse::parse_loaded(&loaded);
90 let normalized = shifty_opt::normalize(&parsed.schema);
91
92 let outcome = infer(&loaded.graph, &normalized).expect("stratifiable schema");
93
94 assert!(outcome.graph.contains(&oxrdf::Triple::new(
95 oxrdf::NamedNode::new_unchecked("http://ex/item"),
96 oxrdf::NamedNode::new_unchecked("http://ex/hasTag"),
97 oxrdf::NamedNode::new_unchecked("http://ex/Tag"),
98 )));
99 }
100
101 #[test]
102 fn planned_validation_preserves_severity_and_applies_threshold() {
103 let ttl = format!(
104 "{PREFIXES}
105 ex:S a sh:NodeShape ;
106 sh:targetNode ex:x ;
107 sh:property ex:InfoShape, ex:WarningShape .
108 ex:InfoShape a sh:PropertyShape ;
109 sh:path ex:required ;
110 sh:minCount 1 ;
111 sh:severity sh:Info .
112 ex:WarningShape a sh:PropertyShape ;
113 sh:path ex:required ;
114 sh:minCount 1 ;
115 sh:severity sh:Warning .
116 "
117 );
118 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
119 let parsed = shifty_parse::parse_loaded(&loaded);
120 let normalized = shifty_opt::normalize(&parsed.schema);
121 let plan = shifty_opt::plan(&normalized);
122
123 let info = validate_plan_with_options(
124 &loaded.graph,
125 &plan,
126 &ValidationOptions {
127 minimum_severity: shifty_algebra::Severity::Info,
128 sort_results: true,
129 },
130 )
131 .unwrap();
132 assert!(!info.conforms);
133 assert_eq!(info.violations.len(), 1);
134 assert_eq!(
135 info.violations[0].severity,
136 shifty_algebra::Severity::Warning
137 );
138 let mut severities: Vec<_> = info.violations[0]
139 .reasons
140 .iter()
141 .map(|reason| reason.severity.clone())
142 .collect();
143 severities.sort_by_key(shifty_algebra::Severity::rank);
144 assert_eq!(
145 severities,
146 vec![
147 shifty_algebra::Severity::Info,
148 shifty_algebra::Severity::Warning
149 ]
150 );
151
152 let warning = validate_plan_with_options(
153 &loaded.graph,
154 &plan,
155 &ValidationOptions {
156 minimum_severity: shifty_algebra::Severity::Warning,
157 sort_results: true,
158 },
159 )
160 .unwrap();
161 assert!(!warning.conforms);
162
163 let violation = validate_plan_with_options(
164 &loaded.graph,
165 &plan,
166 &ValidationOptions {
167 minimum_severity: shifty_algebra::Severity::Violation,
168 sort_results: true,
169 },
170 )
171 .unwrap();
172 assert!(violation.conforms);
173 assert_eq!(violation.violations.len(), 1);
174
175 let report = validate_report_with_options(
176 &loaded,
177 &loaded.graph,
178 &ValidationOptions {
179 minimum_severity: shifty_algebra::Severity::Violation,
180 sort_results: true,
181 },
182 );
183 assert!(report.conforms);
184 assert_eq!(report.results.len(), 2);
185 }
186
187 #[test]
188 fn validation_findings_sort_by_severity_then_focus_node() {
189 let ttl = format!(
190 "{PREFIXES}
191 ex:InfoShape a sh:NodeShape ;
192 sh:targetNode ex:a ;
193 sh:nodeKind sh:Literal ;
194 sh:severity sh:Info .
195 ex:WarningShape a sh:NodeShape ;
196 sh:targetNode ex:z ;
197 sh:nodeKind sh:Literal ;
198 sh:severity sh:Warning .
199 ex:ViolationShape a sh:NodeShape ;
200 sh:targetNode ex:m ;
201 sh:nodeKind sh:Literal .
202 "
203 );
204 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
205 let parsed = shifty_parse::parse_loaded(&loaded);
206 let plan = shifty_opt::plan(&shifty_opt::normalize(&parsed.schema));
207 let outcome = validate_plan(&loaded.graph, &plan).unwrap();
208
209 let ordered: Vec<_> = outcome
210 .violations
211 .iter()
212 .map(|finding| (finding.severity.clone(), finding.focus.to_string()))
213 .collect();
214 assert_eq!(
215 ordered,
216 vec![
217 (
218 shifty_algebra::Severity::Violation,
219 "<http://ex/m>".to_string()
220 ),
221 (
222 shifty_algebra::Severity::Warning,
223 "<http://ex/z>".to_string()
224 ),
225 (shifty_algebra::Severity::Info, "<http://ex/a>".to_string()),
226 ]
227 );
228 }
229
230 #[test]
231 fn reports_specific_failing_constraints() {
232 let ttl = format!(
233 "{PREFIXES}
234 ex:S a sh:NodeShape ;
235 sh:targetNode ex:x ;
236 sh:closed true ;
237 sh:ignoredProperties ( rdf:type ) ;
238 sh:property [ sh:path ex:age ; sh:datatype xsd:integer ; sh:maxCount 1 ] .
239 ex:x ex:age \"foo\" , 5 ; ex:extra 1 .
240 "
241 );
242 let outcome = run(&ttl);
243 assert!(!outcome.conforms);
244 assert_eq!(outcome.violations.len(), 1);
245 let msgs: Vec<&str> = outcome.violations[0]
246 .reasons
247 .iter()
248 .map(|r| r.message.as_str())
249 .collect();
250 assert!(
252 msgs.iter().any(|m| m.contains("datatype(xsd:integer)")),
253 "missing datatype reason: {msgs:?}"
254 );
255 assert!(
256 msgs.iter().any(|m| m.contains("at most 1")),
257 "missing maxCount reason: {msgs:?}"
258 );
259 assert!(
260 msgs.iter()
261 .any(|m| m.contains("closed") && m.contains("extra")),
262 "missing closed reason: {msgs:?}"
263 );
264 }
265
266 #[test]
267 fn cardinality_and_datatype() {
268 let ttl = format!(
269 "{PREFIXES}
270 ex:S a sh:NodeShape ;
271 sh:targetNode ex:alice, ex:bob ;
272 sh:property [ sh:path ex:age ; sh:maxCount 1 ; sh:datatype xsd:integer ] .
273 ex:alice ex:age 30 .
274 ex:bob ex:age 30 ; ex:age 40 .
275 "
276 );
277 let outcome = run(&ttl);
278 assert!(!outcome.conforms);
279 let bad: Vec<_> = outcome
281 .violations
282 .iter()
283 .map(|r| r.focus.to_string())
284 .collect();
285 assert_eq!(bad, vec!["<http://ex/bob>".to_string()]);
286 }
287
288 #[test]
289 fn qualified_value_shape_disjoint_uses_all_sibling_property_shapes() {
290 let ttl = format!(
291 "{PREFIXES}
292 ex:S a sh:NodeShape ;
293 sh:targetNode ex:x ;
294 sh:property ex:A, ex:B .
295 ex:A a sh:PropertyShape ;
296 sh:path ex:p ;
297 sh:qualifiedValueShape [ sh:class ex:TypeA ] ;
298 sh:qualifiedValueShapesDisjoint true ;
299 sh:qualifiedMinCount 1 .
300 ex:B a sh:PropertyShape ;
301 sh:path ex:q ;
302 sh:qualifiedValueShape [ sh:class ex:TypeB ] ;
303 sh:qualifiedValueShapesDisjoint true ;
304 sh:qualifiedMaxCount 10 .
305 ex:x ex:p ex:value .
306 ex:value a ex:TypeA, ex:TypeB .
307 "
308 );
309 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
310 assert!(
311 parsed.diagnostics.is_empty(),
312 "diags: {:?}",
313 parsed.diagnostics
314 );
315 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
316
317 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
318 assert!(!algebra.conforms);
319
320 let report = validate_report(&loaded, &loaded.graph);
321 assert!(!report.conforms);
322 assert_eq!(report.results.len(), 1);
323 assert_eq!(
324 report.results[0].component.as_str(),
325 "http://www.w3.org/ns/shacl#QualifiedMinCountConstraintComponent"
326 );
327 }
328
329 #[test]
330 fn disjoint_on_node_shape_uses_the_focus_node_as_the_value() {
331 let ttl = format!(
332 "{PREFIXES}
333 ex:S a sh:NodeShape ;
334 sh:targetNode ex:valid, ex:invalid ;
335 sh:disjoint ex:p .
336 ex:valid ex:p ex:other .
337 ex:invalid ex:p ex:invalid .
338 "
339 );
340 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
341 assert!(
342 parsed.diagnostics.is_empty(),
343 "diags: {:?}",
344 parsed.diagnostics
345 );
346 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
347
348 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
349 assert!(!algebra.conforms);
350 assert_eq!(algebra.violations.len(), 1);
351 assert_eq!(
352 algebra.violations[0].focus.to_string(),
353 "<http://ex/invalid>"
354 );
355
356 let normalized = shifty_opt::normalize(&parsed.schema);
357 let plan = shifty_opt::plan(&normalized);
358 let planned = validate_plan(&loaded.graph, &plan).unwrap();
359 assert_eq!(planned.conforms, algebra.conforms);
360 assert_eq!(planned.violations.len(), algebra.violations.len());
361
362 let report = validate_report(&loaded, &loaded.graph);
363 assert!(!report.conforms);
364 assert_eq!(report.results.len(), 1);
365 assert_eq!(
366 report.results[0].component.as_str(),
367 "http://www.w3.org/ns/shacl#DisjointConstraintComponent"
368 );
369 assert_eq!(
370 report.results[0].value.as_ref().map(ToString::to_string),
371 Some("<http://ex/invalid>".to_string())
372 );
373 }
374
375 #[test]
376 fn equals_on_node_shape_uses_the_focus_node_as_the_value() {
377 let ttl = format!(
378 "{PREFIXES}
379 ex:S a sh:NodeShape ;
380 sh:targetNode ex:valid, ex:extra, ex:missing ;
381 sh:equals ex:p .
382 ex:valid ex:p ex:valid .
383 ex:extra ex:p ex:extra, ex:other .
384 "
385 );
386 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
387 assert!(
388 parsed.diagnostics.is_empty(),
389 "diags: {:?}",
390 parsed.diagnostics
391 );
392 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
393
394 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
395 assert!(!algebra.conforms);
396 let mut foci: Vec<_> = algebra
397 .violations
398 .iter()
399 .map(|violation| violation.focus.to_string())
400 .collect();
401 foci.sort();
402 assert_eq!(
403 foci,
404 [
405 "<http://ex/extra>".to_string(),
406 "<http://ex/missing>".to_string()
407 ]
408 );
409
410 let normalized = shifty_opt::normalize(&parsed.schema);
411 let plan = shifty_opt::plan(&normalized);
412 let planned = validate_plan(&loaded.graph, &plan).unwrap();
413 assert_eq!(planned.conforms, algebra.conforms);
414 assert_eq!(planned.violations.len(), algebra.violations.len());
415
416 let report = validate_report(&loaded, &loaded.graph);
417 assert!(!report.conforms);
418 assert_eq!(report.results.len(), 2);
419 assert!(report.results.iter().all(|result| result.component.as_str()
420 == "http://www.w3.org/ns/shacl#EqualsConstraintComponent"));
421 }
422
423 #[test]
424 fn datatype_violation() {
425 let ttl = format!(
426 "{PREFIXES}
427 ex:S a sh:NodeShape ;
428 sh:targetNode ex:x ;
429 sh:property [ sh:path ex:p ; sh:datatype xsd:integer ] .
430 ex:x ex:p \"hello\" .
431 "
432 );
433 assert!(!run(&ttl).conforms);
434 }
435
436 #[test]
437 fn nodekind_and_class_target() {
438 let ttl = format!(
439 "{PREFIXES}
440 ex:S a sh:NodeShape ;
441 sh:targetClass ex:Person ;
442 sh:property [ sh:path ex:knows ; sh:nodeKind sh:IRI ] .
443 ex:alice a ex:Person ; ex:knows ex:bob .
444 ex:carol a ex:Person ; ex:knows \"notaniri\" .
445 "
446 );
447 let outcome = run(&ttl);
448 assert!(!outcome.conforms);
449 let bad: Vec<_> = outcome
450 .violations
451 .iter()
452 .map(|r| r.focus.to_string())
453 .collect();
454 assert_eq!(bad, vec!["<http://ex/carol>".to_string()]);
455 }
456
457 #[test]
458 fn recursion_over_cyclic_data_terminates() {
459 let ttl = format!(
461 "{PREFIXES}
462 ex:S a sh:NodeShape ;
463 sh:targetNode ex:a ;
464 sh:property [ sh:path ex:knows ; sh:node ex:S ; sh:nodeKind sh:IRI ] .
465 ex:a ex:knows ex:b .
466 ex:b ex:knows ex:a .
467 "
468 );
469 assert!(run(&ttl).conforms);
472 }
473
474 #[test]
475 fn empty_graph_conforms() {
476 let outcome = validate(&Graph::new(), &shifty_algebra::Schema::new()).unwrap();
477 assert!(outcome.conforms);
478 }
479
480 #[test]
481 fn non_stratifiable_schema_is_diagnosed() {
482 let ttl = format!(
484 "{PREFIXES}
485 ex:S a sh:NodeShape ;
486 sh:targetNode ex:x ;
487 sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ; sh:qualifiedMinCount 1 ] .
488 ex:x ex:p ex:y .
489 "
490 );
491 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
492 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
493 assert!(validate(&loaded.graph, &out.schema).is_err());
494 }
495
496 fn triple(s: &str, p: &str, o: &str) -> oxrdf::Triple {
497 use oxrdf::NamedNode;
498 oxrdf::Triple::new(
499 NamedNode::new(s).unwrap(),
500 NamedNode::new(p).unwrap(),
501 NamedNode::new(o).unwrap(),
502 )
503 }
504
505 #[test]
506 fn triple_rule_infers_from_path() {
507 let ttl = format!(
509 "{PREFIXES}
510 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
511 sh:rule [ a sh:TripleRule ;
512 sh:subject sh:this ; sh:predicate ex:knows2 ;
513 sh:object [ sh:path ex:knows ] ] .
514 ex:a a ex:Person ; ex:knows ex:b .
515 "
516 );
517 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
518 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
519 let outcome = infer(&loaded.graph, &out.schema).unwrap();
520 assert_eq!(outcome.inferred.len(), 1);
521 assert!(
522 outcome
523 .graph
524 .contains(&triple("http://ex/a", "http://ex/knows2", "http://ex/b"))
525 );
526 }
527
528 #[test]
529 fn inference_reaches_a_fixpoint() {
530 let ttl = format!(
533 "{PREFIXES}
534 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
535 sh:rule [ a sh:TripleRule ;
536 sh:subject sh:this ; sh:predicate ex:reaches ;
537 sh:object [ sh:path [ sh:alternativePath ( ex:knows ( ex:knows ex:reaches ) ) ] ] ] .
538 ex:a a ex:Person ; ex:knows ex:b .
539 ex:b a ex:Person ; ex:knows ex:c .
540 ex:c a ex:Person .
541 "
542 );
543 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
544 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
545 let outcome = infer(&loaded.graph, &out.schema).unwrap();
546 assert!(
547 outcome
548 .graph
549 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/b"))
550 );
551 assert!(
552 outcome
553 .graph
554 .contains(&triple("http://ex/b", "http://ex/reaches", "http://ex/c"))
555 );
556 assert!(
558 outcome
559 .graph
560 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/c"))
561 );
562 }
563
564 #[test]
565 fn later_order_output_reactivates_an_earlier_rule() {
566 let ttl = format!(
567 "{PREFIXES}
568 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
569 sh:rule [
570 a sh:TripleRule ; sh:order 0 ;
571 sh:subject sh:this ; sh:predicate ex:done ;
572 sh:object [ sh:path ex:ready ]
573 ] ;
574 sh:rule [
575 a sh:TripleRule ; sh:order 1 ;
576 sh:subject sh:this ; sh:predicate ex:ready ;
577 sh:object ex:y
578 ] .
579 "
580 );
581 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
582 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
583 let outcome = infer(&loaded.graph, &out.schema).unwrap();
584
585 assert!(
586 outcome
587 .graph
588 .contains(&triple("http://ex/x", "http://ex/ready", "http://ex/y"))
589 );
590 assert!(
591 outcome
592 .graph
593 .contains(&triple("http://ex/x", "http://ex/done", "http://ex/y"))
594 );
595 }
596
597 #[test]
598 fn inferred_triples_can_create_new_rule_targets() {
599 let ttl = format!(
600 "{PREFIXES}
601 ex:Seed a sh:NodeShape ; sh:targetNode ex:x ;
602 sh:rule [
603 a sh:TripleRule ;
604 sh:subject sh:this ; sh:predicate ex:eligible ;
605 sh:object ex:y
606 ] .
607 ex:Eligible a sh:NodeShape ; sh:targetSubjectsOf ex:eligible ;
608 sh:rule [
609 a sh:TripleRule ;
610 sh:subject sh:this ; sh:predicate ex:classified ;
611 sh:object ex:yes
612 ] .
613 "
614 );
615 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
616 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
617 let outcome = infer(&loaded.graph, &out.schema).unwrap();
618
619 assert!(outcome.graph.contains(&triple(
620 "http://ex/x",
621 "http://ex/classified",
622 "http://ex/yes",
623 )));
624 }
625
626 #[test]
627 fn split_inference_uses_shapes_graph_as_rule_context() {
628 let shapes_ttl = format!(
629 "{PREFIXES}
630 ex:InverseShape a sh:NodeShape ;
631 sh:targetClass ex:Thing ;
632 sh:rule [
633 a sh:SPARQLRule ;
634 sh:construct \"\"\"
635 CONSTRUCT {{ ?o ?inverse $this }}
636 WHERE {{
637 $this ?predicate ?o .
638 ?predicate ex:inverseOf ?inverse .
639 }}
640 \"\"\"
641 ] .
642 ex:p ex:inverseOf ex:q .
643 "
644 );
645 let data_ttl = format!(
646 "{PREFIXES}
647 ex:a a ex:Thing ; ex:p ex:b .
648 "
649 );
650 let shapes = shifty_parse::load_turtle(shapes_ttl.as_bytes(), None).unwrap();
651 let parsed = shifty_parse::parse_loaded(&shapes);
652 let data = shifty_parse::load_turtle(data_ttl.as_bytes(), None).unwrap();
653
654 let outcome = infer_graphs(&data.graph, &shapes.graph, &parsed.schema).unwrap();
655
656 assert!(
657 outcome
658 .graph
659 .contains(&triple("http://ex/b", "http://ex/q", "http://ex/a"))
660 );
661 assert_eq!(outcome.inferred.len(), 1);
662 }
663}