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 planned_validation_preserves_severity_and_applies_threshold() {
70 let ttl = format!(
71 "{PREFIXES}
72 ex:S a sh:NodeShape ;
73 sh:targetNode ex:x ;
74 sh:property ex:InfoShape, ex:WarningShape .
75 ex:InfoShape a sh:PropertyShape ;
76 sh:path ex:required ;
77 sh:minCount 1 ;
78 sh:severity sh:Info .
79 ex:WarningShape a sh:PropertyShape ;
80 sh:path ex:required ;
81 sh:minCount 1 ;
82 sh:severity sh:Warning .
83 "
84 );
85 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
86 let parsed = shifty_parse::parse_loaded(&loaded);
87 let normalized = shifty_opt::normalize(&parsed.schema);
88 let plan = shifty_opt::plan(&normalized);
89
90 let info = validate_plan_with_options(
91 &loaded.graph,
92 &plan,
93 &ValidationOptions {
94 minimum_severity: shifty_algebra::Severity::Info,
95 sort_results: true,
96 },
97 )
98 .unwrap();
99 assert!(!info.conforms);
100 assert_eq!(info.violations.len(), 1);
101 assert_eq!(
102 info.violations[0].severity,
103 shifty_algebra::Severity::Warning
104 );
105 let mut severities: Vec<_> = info.violations[0]
106 .reasons
107 .iter()
108 .map(|reason| reason.severity.clone())
109 .collect();
110 severities.sort_by_key(shifty_algebra::Severity::rank);
111 assert_eq!(
112 severities,
113 vec![
114 shifty_algebra::Severity::Info,
115 shifty_algebra::Severity::Warning
116 ]
117 );
118
119 let warning = validate_plan_with_options(
120 &loaded.graph,
121 &plan,
122 &ValidationOptions {
123 minimum_severity: shifty_algebra::Severity::Warning,
124 sort_results: true,
125 },
126 )
127 .unwrap();
128 assert!(!warning.conforms);
129
130 let violation = validate_plan_with_options(
131 &loaded.graph,
132 &plan,
133 &ValidationOptions {
134 minimum_severity: shifty_algebra::Severity::Violation,
135 sort_results: true,
136 },
137 )
138 .unwrap();
139 assert!(violation.conforms);
140 assert_eq!(violation.violations.len(), 1);
141
142 let report = validate_report_with_options(
143 &loaded,
144 &loaded.graph,
145 &ValidationOptions {
146 minimum_severity: shifty_algebra::Severity::Violation,
147 sort_results: true,
148 },
149 );
150 assert!(report.conforms);
151 assert_eq!(report.results.len(), 2);
152 }
153
154 #[test]
155 fn validation_findings_sort_by_severity_then_focus_node() {
156 let ttl = format!(
157 "{PREFIXES}
158 ex:InfoShape a sh:NodeShape ;
159 sh:targetNode ex:a ;
160 sh:nodeKind sh:Literal ;
161 sh:severity sh:Info .
162 ex:WarningShape a sh:NodeShape ;
163 sh:targetNode ex:z ;
164 sh:nodeKind sh:Literal ;
165 sh:severity sh:Warning .
166 ex:ViolationShape a sh:NodeShape ;
167 sh:targetNode ex:m ;
168 sh:nodeKind sh:Literal .
169 "
170 );
171 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
172 let parsed = shifty_parse::parse_loaded(&loaded);
173 let plan = shifty_opt::plan(&shifty_opt::normalize(&parsed.schema));
174 let outcome = validate_plan(&loaded.graph, &plan).unwrap();
175
176 let ordered: Vec<_> = outcome
177 .violations
178 .iter()
179 .map(|finding| (finding.severity.clone(), finding.focus.to_string()))
180 .collect();
181 assert_eq!(
182 ordered,
183 vec![
184 (
185 shifty_algebra::Severity::Violation,
186 "<http://ex/m>".to_string()
187 ),
188 (
189 shifty_algebra::Severity::Warning,
190 "<http://ex/z>".to_string()
191 ),
192 (shifty_algebra::Severity::Info, "<http://ex/a>".to_string()),
193 ]
194 );
195 }
196
197 #[test]
198 fn reports_specific_failing_constraints() {
199 let ttl = format!(
200 "{PREFIXES}
201 ex:S a sh:NodeShape ;
202 sh:targetNode ex:x ;
203 sh:closed true ;
204 sh:ignoredProperties ( rdf:type ) ;
205 sh:property [ sh:path ex:age ; sh:datatype xsd:integer ; sh:maxCount 1 ] .
206 ex:x ex:age \"foo\" , 5 ; ex:extra 1 .
207 "
208 );
209 let outcome = run(&ttl);
210 assert!(!outcome.conforms);
211 assert_eq!(outcome.violations.len(), 1);
212 let msgs: Vec<&str> = outcome.violations[0]
213 .reasons
214 .iter()
215 .map(|r| r.message.as_str())
216 .collect();
217 assert!(
219 msgs.iter().any(|m| m.contains("datatype(xsd:integer)")),
220 "missing datatype reason: {msgs:?}"
221 );
222 assert!(
223 msgs.iter().any(|m| m.contains("at most 1")),
224 "missing maxCount reason: {msgs:?}"
225 );
226 assert!(
227 msgs.iter()
228 .any(|m| m.contains("closed") && m.contains("extra")),
229 "missing closed reason: {msgs:?}"
230 );
231 }
232
233 #[test]
234 fn cardinality_and_datatype() {
235 let ttl = format!(
236 "{PREFIXES}
237 ex:S a sh:NodeShape ;
238 sh:targetNode ex:alice, ex:bob ;
239 sh:property [ sh:path ex:age ; sh:maxCount 1 ; sh:datatype xsd:integer ] .
240 ex:alice ex:age 30 .
241 ex:bob ex:age 30 ; ex:age 40 .
242 "
243 );
244 let outcome = run(&ttl);
245 assert!(!outcome.conforms);
246 let bad: Vec<_> = outcome
248 .violations
249 .iter()
250 .map(|r| r.focus.to_string())
251 .collect();
252 assert_eq!(bad, vec!["<http://ex/bob>".to_string()]);
253 }
254
255 #[test]
256 fn qualified_value_shape_disjoint_uses_all_sibling_property_shapes() {
257 let ttl = format!(
258 "{PREFIXES}
259 ex:S a sh:NodeShape ;
260 sh:targetNode ex:x ;
261 sh:property ex:A, ex:B .
262 ex:A a sh:PropertyShape ;
263 sh:path ex:p ;
264 sh:qualifiedValueShape [ sh:class ex:TypeA ] ;
265 sh:qualifiedValueShapesDisjoint true ;
266 sh:qualifiedMinCount 1 .
267 ex:B a sh:PropertyShape ;
268 sh:path ex:q ;
269 sh:qualifiedValueShape [ sh:class ex:TypeB ] ;
270 sh:qualifiedValueShapesDisjoint true ;
271 sh:qualifiedMaxCount 10 .
272 ex:x ex:p ex:value .
273 ex:value a ex:TypeA, ex:TypeB .
274 "
275 );
276 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
277 assert!(
278 parsed.diagnostics.is_empty(),
279 "diags: {:?}",
280 parsed.diagnostics
281 );
282 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
283
284 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
285 assert!(!algebra.conforms);
286
287 let report = validate_report(&loaded, &loaded.graph);
288 assert!(!report.conforms);
289 assert_eq!(report.results.len(), 1);
290 assert_eq!(
291 report.results[0].component.as_str(),
292 "http://www.w3.org/ns/shacl#QualifiedMinCountConstraintComponent"
293 );
294 }
295
296 #[test]
297 fn disjoint_on_node_shape_uses_the_focus_node_as_the_value() {
298 let ttl = format!(
299 "{PREFIXES}
300 ex:S a sh:NodeShape ;
301 sh:targetNode ex:valid, ex:invalid ;
302 sh:disjoint ex:p .
303 ex:valid ex:p ex:other .
304 ex:invalid ex:p ex:invalid .
305 "
306 );
307 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
308 assert!(
309 parsed.diagnostics.is_empty(),
310 "diags: {:?}",
311 parsed.diagnostics
312 );
313 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
314
315 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
316 assert!(!algebra.conforms);
317 assert_eq!(algebra.violations.len(), 1);
318 assert_eq!(
319 algebra.violations[0].focus.to_string(),
320 "<http://ex/invalid>"
321 );
322
323 let normalized = shifty_opt::normalize(&parsed.schema);
324 let plan = shifty_opt::plan(&normalized);
325 let planned = validate_plan(&loaded.graph, &plan).unwrap();
326 assert_eq!(planned.conforms, algebra.conforms);
327 assert_eq!(planned.violations.len(), algebra.violations.len());
328
329 let report = validate_report(&loaded, &loaded.graph);
330 assert!(!report.conforms);
331 assert_eq!(report.results.len(), 1);
332 assert_eq!(
333 report.results[0].component.as_str(),
334 "http://www.w3.org/ns/shacl#DisjointConstraintComponent"
335 );
336 assert_eq!(
337 report.results[0].value.as_ref().map(ToString::to_string),
338 Some("<http://ex/invalid>".to_string())
339 );
340 }
341
342 #[test]
343 fn equals_on_node_shape_uses_the_focus_node_as_the_value() {
344 let ttl = format!(
345 "{PREFIXES}
346 ex:S a sh:NodeShape ;
347 sh:targetNode ex:valid, ex:extra, ex:missing ;
348 sh:equals ex:p .
349 ex:valid ex:p ex:valid .
350 ex:extra ex:p ex:extra, ex:other .
351 "
352 );
353 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
354 assert!(
355 parsed.diagnostics.is_empty(),
356 "diags: {:?}",
357 parsed.diagnostics
358 );
359 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
360
361 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
362 assert!(!algebra.conforms);
363 let mut foci: Vec<_> = algebra
364 .violations
365 .iter()
366 .map(|violation| violation.focus.to_string())
367 .collect();
368 foci.sort();
369 assert_eq!(
370 foci,
371 [
372 "<http://ex/extra>".to_string(),
373 "<http://ex/missing>".to_string()
374 ]
375 );
376
377 let normalized = shifty_opt::normalize(&parsed.schema);
378 let plan = shifty_opt::plan(&normalized);
379 let planned = validate_plan(&loaded.graph, &plan).unwrap();
380 assert_eq!(planned.conforms, algebra.conforms);
381 assert_eq!(planned.violations.len(), algebra.violations.len());
382
383 let report = validate_report(&loaded, &loaded.graph);
384 assert!(!report.conforms);
385 assert_eq!(report.results.len(), 2);
386 assert!(report.results.iter().all(|result| result.component.as_str()
387 == "http://www.w3.org/ns/shacl#EqualsConstraintComponent"));
388 }
389
390 #[test]
391 fn datatype_violation() {
392 let ttl = format!(
393 "{PREFIXES}
394 ex:S a sh:NodeShape ;
395 sh:targetNode ex:x ;
396 sh:property [ sh:path ex:p ; sh:datatype xsd:integer ] .
397 ex:x ex:p \"hello\" .
398 "
399 );
400 assert!(!run(&ttl).conforms);
401 }
402
403 #[test]
404 fn nodekind_and_class_target() {
405 let ttl = format!(
406 "{PREFIXES}
407 ex:S a sh:NodeShape ;
408 sh:targetClass ex:Person ;
409 sh:property [ sh:path ex:knows ; sh:nodeKind sh:IRI ] .
410 ex:alice a ex:Person ; ex:knows ex:bob .
411 ex:carol a ex:Person ; ex:knows \"notaniri\" .
412 "
413 );
414 let outcome = run(&ttl);
415 assert!(!outcome.conforms);
416 let bad: Vec<_> = outcome
417 .violations
418 .iter()
419 .map(|r| r.focus.to_string())
420 .collect();
421 assert_eq!(bad, vec!["<http://ex/carol>".to_string()]);
422 }
423
424 #[test]
425 fn recursion_over_cyclic_data_terminates() {
426 let ttl = format!(
428 "{PREFIXES}
429 ex:S a sh:NodeShape ;
430 sh:targetNode ex:a ;
431 sh:property [ sh:path ex:knows ; sh:node ex:S ; sh:nodeKind sh:IRI ] .
432 ex:a ex:knows ex:b .
433 ex:b ex:knows ex:a .
434 "
435 );
436 assert!(run(&ttl).conforms);
439 }
440
441 #[test]
442 fn empty_graph_conforms() {
443 let outcome = validate(&Graph::new(), &shifty_algebra::Schema::new()).unwrap();
444 assert!(outcome.conforms);
445 }
446
447 #[test]
448 fn non_stratifiable_schema_is_diagnosed() {
449 let ttl = format!(
451 "{PREFIXES}
452 ex:S a sh:NodeShape ;
453 sh:targetNode ex:x ;
454 sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ; sh:qualifiedMinCount 1 ] .
455 ex:x ex:p ex:y .
456 "
457 );
458 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
459 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
460 assert!(validate(&loaded.graph, &out.schema).is_err());
461 }
462
463 fn triple(s: &str, p: &str, o: &str) -> oxrdf::Triple {
464 use oxrdf::NamedNode;
465 oxrdf::Triple::new(
466 NamedNode::new(s).unwrap(),
467 NamedNode::new(p).unwrap(),
468 NamedNode::new(o).unwrap(),
469 )
470 }
471
472 #[test]
473 fn triple_rule_infers_from_path() {
474 let ttl = format!(
476 "{PREFIXES}
477 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
478 sh:rule [ a sh:TripleRule ;
479 sh:subject sh:this ; sh:predicate ex:knows2 ;
480 sh:object [ sh:path ex:knows ] ] .
481 ex:a a ex:Person ; ex:knows ex:b .
482 "
483 );
484 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
485 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
486 let outcome = infer(&loaded.graph, &out.schema).unwrap();
487 assert_eq!(outcome.inferred.len(), 1);
488 assert!(
489 outcome
490 .graph
491 .contains(&triple("http://ex/a", "http://ex/knows2", "http://ex/b"))
492 );
493 }
494
495 #[test]
496 fn inference_reaches_a_fixpoint() {
497 let ttl = format!(
500 "{PREFIXES}
501 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
502 sh:rule [ a sh:TripleRule ;
503 sh:subject sh:this ; sh:predicate ex:reaches ;
504 sh:object [ sh:path [ sh:alternativePath ( ex:knows ( ex:knows ex:reaches ) ) ] ] ] .
505 ex:a a ex:Person ; ex:knows ex:b .
506 ex:b a ex:Person ; ex:knows ex:c .
507 ex:c a ex:Person .
508 "
509 );
510 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
511 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
512 let outcome = infer(&loaded.graph, &out.schema).unwrap();
513 assert!(
514 outcome
515 .graph
516 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/b"))
517 );
518 assert!(
519 outcome
520 .graph
521 .contains(&triple("http://ex/b", "http://ex/reaches", "http://ex/c"))
522 );
523 assert!(
525 outcome
526 .graph
527 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/c"))
528 );
529 }
530
531 #[test]
532 fn later_order_output_reactivates_an_earlier_rule() {
533 let ttl = format!(
534 "{PREFIXES}
535 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
536 sh:rule [
537 a sh:TripleRule ; sh:order 0 ;
538 sh:subject sh:this ; sh:predicate ex:done ;
539 sh:object [ sh:path ex:ready ]
540 ] ;
541 sh:rule [
542 a sh:TripleRule ; sh:order 1 ;
543 sh:subject sh:this ; sh:predicate ex:ready ;
544 sh:object ex:y
545 ] .
546 "
547 );
548 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
549 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
550 let outcome = infer(&loaded.graph, &out.schema).unwrap();
551
552 assert!(
553 outcome
554 .graph
555 .contains(&triple("http://ex/x", "http://ex/ready", "http://ex/y"))
556 );
557 assert!(
558 outcome
559 .graph
560 .contains(&triple("http://ex/x", "http://ex/done", "http://ex/y"))
561 );
562 }
563
564 #[test]
565 fn inferred_triples_can_create_new_rule_targets() {
566 let ttl = format!(
567 "{PREFIXES}
568 ex:Seed a sh:NodeShape ; sh:targetNode ex:x ;
569 sh:rule [
570 a sh:TripleRule ;
571 sh:subject sh:this ; sh:predicate ex:eligible ;
572 sh:object ex:y
573 ] .
574 ex:Eligible a sh:NodeShape ; sh:targetSubjectsOf ex:eligible ;
575 sh:rule [
576 a sh:TripleRule ;
577 sh:subject sh:this ; sh:predicate ex:classified ;
578 sh:object ex:yes
579 ] .
580 "
581 );
582 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
583 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
584 let outcome = infer(&loaded.graph, &out.schema).unwrap();
585
586 assert!(outcome.graph.contains(&triple(
587 "http://ex/x",
588 "http://ex/classified",
589 "http://ex/yes",
590 )));
591 }
592
593 #[test]
594 fn split_inference_uses_shapes_graph_as_rule_context() {
595 let shapes_ttl = format!(
596 "{PREFIXES}
597 ex:InverseShape a sh:NodeShape ;
598 sh:targetClass ex:Thing ;
599 sh:rule [
600 a sh:SPARQLRule ;
601 sh:construct \"\"\"
602 CONSTRUCT {{ ?o ?inverse $this }}
603 WHERE {{
604 $this ?predicate ?o .
605 ?predicate ex:inverseOf ?inverse .
606 }}
607 \"\"\"
608 ] .
609 ex:p ex:inverseOf ex:q .
610 "
611 );
612 let data_ttl = format!(
613 "{PREFIXES}
614 ex:a a ex:Thing ; ex:p ex:b .
615 "
616 );
617 let shapes = shifty_parse::load_turtle(shapes_ttl.as_bytes(), None).unwrap();
618 let parsed = shifty_parse::parse_loaded(&shapes);
619 let data = shifty_parse::load_turtle(data_ttl.as_bytes(), None).unwrap();
620
621 let outcome = infer_graphs(&data.graph, &shapes.graph, &parsed.schema).unwrap();
622
623 assert!(
624 outcome
625 .graph
626 .contains(&triple("http://ex/b", "http://ex/q", "http://ex/a"))
627 );
628 assert_eq!(outcome.inferred.len(), 1);
629 }
630}