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::{
29 InferenceOutcome, infer, infer_graphs, infer_with_context, infer_with_context_and_options,
30 infer_with_options,
31};
32pub use report::{
33 PropertyWitness, ValidationReport, ValidationResult, evaluate_function_expression,
34 property_witnesses_graphs_with_mode, property_witnesses_graphs_with_mode_and_options,
35 report_to_graph, validate_report, validate_report_graphs, validate_report_graphs_with_mode,
36 validate_report_graphs_with_mode_and_options, validate_report_with_options,
37};
38pub use synthesize::{synthesize, synthesize_focus};
39pub use validate::{
40 EngineOptions, NonStratifiable, Reason, UnsupportedPolicy, ValidationGraphMode,
41 ValidationOptions, ValidationOutcome, Violation, focus_nodes, graph_union, validate,
42 validate_graphs, validate_graphs_with_mode, validate_graphs_with_mode_and_options,
43 validate_plan, validate_plan_graphs, validate_plan_graphs_with_mode,
44 validate_plan_graphs_with_mode_and_options, validate_plan_with_context,
45 validate_plan_with_context_and_options, validate_plan_with_options, validate_with_context,
46 validate_with_context_and_options, validate_with_options,
47};
48pub use witness::{
49 BlockReason, FocusSat, FocusWitness, PathSupport, RelKind, SatTrace, Witness, satisfy_shape,
50 shape_id_for_iri, witness_node, witness_shape, witness_violations,
51};
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use oxrdf::Graph;
57 use shifty_parse::parse_turtle;
58
59 fn run(shapes_and_data: &str) -> ValidationOutcome {
60 let out = parse_turtle(shapes_and_data.as_bytes(), None).unwrap();
61 let loaded = shifty_parse::load_turtle(shapes_and_data.as_bytes(), None).unwrap();
63 validate(&loaded.graph, &out.schema).expect("stratifiable schema")
64 }
65
66 fn run_normalized(shapes_and_data: &str) -> ValidationOutcome {
70 let out = parse_turtle(shapes_and_data.as_bytes(), None).unwrap();
71 let loaded = shifty_parse::load_turtle(shapes_and_data.as_bytes(), None).unwrap();
72 let normalized = shifty_opt::normalize(&out.schema);
73 validate(&loaded.graph, &normalized).expect("stratifiable schema")
74 }
75
76 const PREFIXES: &str = r#"
77 @prefix sh: <http://www.w3.org/ns/shacl#> .
78 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
79 @prefix ex: <http://ex/> .
80 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
81 "#;
82
83 #[test]
90 fn normalized_class_universal_names_the_required_class() {
91 let ttl = format!(
92 "{PREFIXES}
93 ex:SensorShape a sh:NodeShape ;
94 sh:targetClass ex:Sensor ;
95 sh:property [ sh:path ex:hasKind ; sh:class ex:QuantityKind ] .
96
97 ex:s1 a ex:Sensor ; ex:hasKind ex:Temperature .
98 # ex:Temperature is deliberately untyped (as if its ontology import
99 # were missing), so it fails the class check.
100 "
101 );
102
103 let outcome = run_normalized(&ttl);
104 let messages: Vec<&str> = outcome
105 .violations
106 .iter()
107 .flat_map(|v| v.reasons.iter())
108 .map(|r| r.message.as_str())
109 .collect();
110
111 assert!(
112 messages.contains(&"must be an instance of <http://ex/QuantityKind>"),
113 "expected an intuitive class message, got: {messages:?}"
114 );
115 assert!(
117 !messages.iter().any(|m| m.contains("at most 0")),
118 "raw ∃≤0 message leaked: {messages:?}"
119 );
120
121 let reason = outcome
123 .violations
124 .iter()
125 .flat_map(|v| &v.reasons)
126 .find(|r| r.message.starts_with("must be an instance of"))
127 .expect("class reason present");
128 assert_eq!(reason.value.to_string(), "<http://ex/Temperature>");
129 assert_eq!(reason.path.as_deref(), Some("<http://ex/hasKind>"));
130 }
131
132 #[test]
136 fn normalized_nodekind_universal_names_the_requirement() {
137 let ttl = format!(
138 "{PREFIXES}
139 ex:S a sh:NodeShape ;
140 sh:targetClass ex:T ;
141 sh:property [ sh:path ex:p ; sh:nodeKind sh:IRI ] .
142
143 ex:a a ex:T ; ex:p \"not-an-iri\" .
144 "
145 );
146
147 let outcome = run_normalized(&ttl);
148 let messages: Vec<&str> = outcome
149 .violations
150 .iter()
151 .flat_map(|v| v.reasons.iter())
152 .map(|r| r.message.as_str())
153 .collect();
154
155 assert!(
156 messages.contains(&"must satisfy `nodeKind(IRI)`"),
157 "expected a nodeKind requirement message, got: {messages:?}"
158 );
159 assert!(
160 !messages.iter().any(|m| m.contains("at most 0")),
161 "raw ∃≤0 message leaked: {messages:?}"
162 );
163 }
164
165 #[test]
170 fn author_sh_message_is_carried_onto_reasons() {
171 let ttl = format!(
172 "{PREFIXES}
173 ex:S a sh:NodeShape ;
174 sh:targetClass ex:T ;
175 sh:property [ sh:path ex:hasKind ; sh:class ex:QuantityKind ;
176 sh:message \"{{$this}} needs a known QuantityKind\" ] .
177
178 ex:a a ex:T ; ex:hasKind ex:Temperature .
179 "
180 );
181
182 let outcome = run_normalized(&ttl);
183 let reason = outcome
184 .violations
185 .iter()
186 .flat_map(|v| &v.reasons)
187 .find(|r| r.author_message.is_some())
188 .expect("author message present");
189
190 assert_eq!(
192 reason.author_message.as_deref(),
193 Some("<http://ex/a> needs a known QuantityKind")
194 );
195 assert_eq!(
196 reason.message,
197 "must be an instance of <http://ex/QuantityKind>"
198 );
199 }
200
201 #[test]
203 fn absent_sh_message_leaves_author_message_unset() {
204 let ttl = format!(
205 "{PREFIXES}
206 ex:S a sh:NodeShape ;
207 sh:targetClass ex:T ;
208 sh:property [ sh:path ex:hasKind ; sh:class ex:QuantityKind ] .
209
210 ex:a a ex:T ; ex:hasKind ex:Temperature .
211 "
212 );
213
214 let outcome = run_normalized(&ttl);
215 assert!(
216 outcome
217 .violations
218 .iter()
219 .flat_map(|v| &v.reasons)
220 .all(|r| r.author_message.is_none()),
221 "no author message expected"
222 );
223 }
224
225 #[test]
226 fn inference_rules_fire_for_implicit_class_targets() {
227 let ttl = br#"
228 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
229 @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
230 @prefix owl: <http://www.w3.org/2002/07/owl#> .
231 @prefix sh: <http://www.w3.org/ns/shacl#> .
232 @prefix ex: <http://ex/> .
233
234 ex:Parent a owl:Class, sh:NodeShape ;
235 sh:rule [
236 a sh:TripleRule ;
237 sh:subject sh:this ;
238 sh:predicate ex:hasTag ;
239 sh:object ex:Tag
240 ] .
241
242 ex:Child rdfs:subClassOf ex:Parent .
243 ex:item a ex:Child .
244 "#;
245 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
246 let parsed = shifty_parse::parse_loaded(&loaded);
247 let normalized = shifty_opt::normalize(&parsed.schema);
248
249 let outcome = infer(&loaded.graph, &normalized).expect("stratifiable schema");
250
251 assert!(outcome.graph.contains(&oxrdf::Triple::new(
252 oxrdf::NamedNode::new_unchecked("http://ex/item"),
253 oxrdf::NamedNode::new_unchecked("http://ex/hasTag"),
254 oxrdf::NamedNode::new_unchecked("http://ex/Tag"),
255 )));
256 }
257
258 fn infer_ttl(ttl: &[u8]) -> InferenceOutcome {
261 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
262 let parsed = shifty_parse::parse_loaded(&loaded);
263 assert!(
264 parsed.diagnostics.is_empty(),
265 "parse diagnostics: {:?}",
266 parsed.diagnostics
267 );
268 let normalized = shifty_opt::normalize(&parsed.schema);
269 infer(&loaded.graph, &normalized).expect("stratifiable schema")
270 }
271
272 fn triple_term(s: &str, p: &str, o: impl Into<oxrdf::Term>) -> oxrdf::Triple {
273 oxrdf::Triple::new(
274 oxrdf::NamedNode::new_unchecked(s),
275 oxrdf::NamedNode::new_unchecked(p),
276 o,
277 )
278 }
279
280 #[test]
281 fn rule_object_union_node_expression() {
282 let outcome = infer_ttl(
284 br#"
285 @prefix sh: <http://www.w3.org/ns/shacl#> .
286 @prefix ex: <http://ex/> .
287 ex:PersonShape a sh:NodeShape ;
288 sh:targetClass ex:Person ;
289 sh:rule [
290 a sh:TripleRule ;
291 sh:subject sh:this ;
292 sh:predicate ex:contact ;
293 sh:object [ sh:union ( [ sh:path ex:email ] [ sh:path ex:phone ] ) ]
294 ] .
295 ex:alice a ex:Person ; ex:email "a@x.org" ; ex:phone "555-1234" .
296 "#,
297 );
298 let email = oxrdf::Literal::new_simple_literal("a@x.org");
299 let phone = oxrdf::Literal::new_simple_literal("555-1234");
300 assert!(outcome.graph.contains(&triple_term(
301 "http://ex/alice",
302 "http://ex/contact",
303 email
304 )));
305 assert!(outcome.graph.contains(&triple_term(
306 "http://ex/alice",
307 "http://ex/contact",
308 phone
309 )));
310 }
311
312 #[test]
313 fn rule_object_intersection_node_expression() {
314 let outcome = infer_ttl(
316 br#"
317 @prefix sh: <http://www.w3.org/ns/shacl#> .
318 @prefix ex: <http://ex/> .
319 ex:S a sh:NodeShape ;
320 sh:targetClass ex:T ;
321 sh:rule [
322 a sh:TripleRule ;
323 sh:subject sh:this ;
324 sh:predicate ex:both ;
325 sh:object [ sh:intersection ( [ sh:path ex:a ] [ sh:path ex:b ] ) ]
326 ] .
327 ex:x a ex:T ; ex:a ex:shared, ex:onlyA ; ex:b ex:shared, ex:onlyB .
328 "#,
329 );
330 let both = "http://ex/both";
331 assert!(outcome.graph.contains(&triple_term(
332 "http://ex/x",
333 both,
334 oxrdf::NamedNode::new_unchecked("http://ex/shared")
335 )));
336 assert!(!outcome.graph.contains(&triple_term(
337 "http://ex/x",
338 both,
339 oxrdf::NamedNode::new_unchecked("http://ex/onlyA")
340 )));
341 assert!(!outcome.graph.contains(&triple_term(
342 "http://ex/x",
343 both,
344 oxrdf::NamedNode::new_unchecked("http://ex/onlyB")
345 )));
346 }
347
348 #[test]
349 fn rule_object_filter_node_expression() {
350 let outcome = infer_ttl(
352 br#"
353 @prefix sh: <http://www.w3.org/ns/shacl#> .
354 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
355 @prefix ex: <http://ex/> .
356 ex:IntShape a sh:NodeShape ; sh:datatype xsd:integer .
357 ex:S a sh:NodeShape ;
358 sh:targetClass ex:T ;
359 sh:rule [
360 a sh:TripleRule ;
361 sh:subject sh:this ;
362 sh:predicate ex:intValue ;
363 sh:object [ sh:filterShape ex:IntShape ; sh:nodes [ sh:path ex:value ] ]
364 ] .
365 ex:x a ex:T ; ex:value 42, "hello" .
366 "#,
367 );
368 let int_value = "http://ex/intValue";
369 assert!(outcome.graph.contains(&triple_term(
370 "http://ex/x",
371 int_value,
372 oxrdf::Literal::new_typed_literal(
373 "42",
374 oxrdf::NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#integer")
375 )
376 )));
377 assert!(!outcome.graph.contains(&triple_term(
378 "http://ex/x",
379 int_value,
380 oxrdf::Literal::new_simple_literal("hello")
381 )));
382 }
383
384 #[test]
385 fn planned_validation_preserves_severity_and_applies_threshold() {
386 let ttl = format!(
387 "{PREFIXES}
388 ex:S a sh:NodeShape ;
389 sh:targetNode ex:x ;
390 sh:property ex:InfoShape, ex:WarningShape .
391 ex:InfoShape a sh:PropertyShape ;
392 sh:path ex:required ;
393 sh:minCount 1 ;
394 sh:severity sh:Info .
395 ex:WarningShape a sh:PropertyShape ;
396 sh:path ex:required ;
397 sh:minCount 1 ;
398 sh:severity sh:Warning .
399 "
400 );
401 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
402 let parsed = shifty_parse::parse_loaded(&loaded);
403 let normalized = shifty_opt::normalize(&parsed.schema);
404 let plan = shifty_opt::plan(&normalized);
405
406 let info = validate_plan_with_options(
407 &loaded.graph,
408 &plan,
409 &ValidationOptions {
410 minimum_severity: shifty_algebra::Severity::Info,
411 sort_results: true,
412 ..Default::default()
413 },
414 )
415 .unwrap();
416 assert!(!info.conforms);
417 assert_eq!(info.violations.len(), 1);
418 assert_eq!(
419 info.violations[0].severity,
420 shifty_algebra::Severity::Warning
421 );
422 let mut severities: Vec<_> = info.violations[0]
423 .reasons
424 .iter()
425 .map(|reason| reason.severity.clone())
426 .collect();
427 severities.sort_by_key(shifty_algebra::Severity::rank);
428 assert_eq!(
429 severities,
430 vec![
431 shifty_algebra::Severity::Info,
432 shifty_algebra::Severity::Warning
433 ]
434 );
435
436 let warning = validate_plan_with_options(
437 &loaded.graph,
438 &plan,
439 &ValidationOptions {
440 minimum_severity: shifty_algebra::Severity::Warning,
441 sort_results: true,
442 ..Default::default()
443 },
444 )
445 .unwrap();
446 assert!(!warning.conforms);
447
448 let violation = validate_plan_with_options(
449 &loaded.graph,
450 &plan,
451 &ValidationOptions {
452 minimum_severity: shifty_algebra::Severity::Violation,
453 sort_results: true,
454 ..Default::default()
455 },
456 )
457 .unwrap();
458 assert!(violation.conforms);
459 assert_eq!(violation.violations.len(), 1);
460
461 let report = validate_report_with_options(
462 &loaded,
463 &loaded.graph,
464 &ValidationOptions {
465 minimum_severity: shifty_algebra::Severity::Violation,
466 sort_results: true,
467 ..Default::default()
468 },
469 );
470 assert!(report.conforms);
471 assert_eq!(report.results.len(), 2);
472 }
473
474 #[test]
475 fn validation_findings_sort_by_severity_then_focus_node() {
476 let ttl = format!(
477 "{PREFIXES}
478 ex:InfoShape a sh:NodeShape ;
479 sh:targetNode ex:a ;
480 sh:nodeKind sh:Literal ;
481 sh:severity sh:Info .
482 ex:WarningShape a sh:NodeShape ;
483 sh:targetNode ex:z ;
484 sh:nodeKind sh:Literal ;
485 sh:severity sh:Warning .
486 ex:ViolationShape a sh:NodeShape ;
487 sh:targetNode ex:m ;
488 sh:nodeKind sh:Literal .
489 "
490 );
491 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
492 let parsed = shifty_parse::parse_loaded(&loaded);
493 let plan = shifty_opt::plan(&shifty_opt::normalize(&parsed.schema));
494 let outcome = validate_plan(&loaded.graph, &plan).unwrap();
495
496 let ordered: Vec<_> = outcome
497 .violations
498 .iter()
499 .map(|finding| (finding.severity.clone(), finding.focus.to_string()))
500 .collect();
501 assert_eq!(
502 ordered,
503 vec![
504 (
505 shifty_algebra::Severity::Violation,
506 "<http://ex/m>".to_string()
507 ),
508 (
509 shifty_algebra::Severity::Warning,
510 "<http://ex/z>".to_string()
511 ),
512 (shifty_algebra::Severity::Info, "<http://ex/a>".to_string()),
513 ]
514 );
515 }
516
517 #[test]
518 fn reports_specific_failing_constraints() {
519 let ttl = format!(
520 "{PREFIXES}
521 ex:S a sh:NodeShape ;
522 sh:targetNode ex:x ;
523 sh:closed true ;
524 sh:ignoredProperties ( rdf:type ) ;
525 sh:property [ sh:path ex:age ; sh:datatype xsd:integer ; sh:maxCount 1 ] .
526 ex:x ex:age \"foo\" , 5 ; ex:extra 1 .
527 "
528 );
529 let outcome = run(&ttl);
530 assert!(!outcome.conforms);
531 assert_eq!(outcome.violations.len(), 1);
532 let msgs: Vec<&str> = outcome.violations[0]
533 .reasons
534 .iter()
535 .map(|r| r.message.as_str())
536 .collect();
537 assert!(
539 msgs.iter().any(|m| m.contains("datatype(xsd:integer)")),
540 "missing datatype reason: {msgs:?}"
541 );
542 assert!(
543 msgs.iter().any(|m| m.contains("at most 1")),
544 "missing maxCount reason: {msgs:?}"
545 );
546 assert!(
547 msgs.iter()
548 .any(|m| m.contains("closed") && m.contains("extra")),
549 "missing closed reason: {msgs:?}"
550 );
551 }
552
553 #[test]
554 fn cardinality_and_datatype() {
555 let ttl = format!(
556 "{PREFIXES}
557 ex:S a sh:NodeShape ;
558 sh:targetNode ex:alice, ex:bob ;
559 sh:property [ sh:path ex:age ; sh:maxCount 1 ; sh:datatype xsd:integer ] .
560 ex:alice ex:age 30 .
561 ex:bob ex:age 30 ; ex:age 40 .
562 "
563 );
564 let outcome = run(&ttl);
565 assert!(!outcome.conforms);
566 let bad: Vec<_> = outcome
568 .violations
569 .iter()
570 .map(|r| r.focus.to_string())
571 .collect();
572 assert_eq!(bad, vec!["<http://ex/bob>".to_string()]);
573 }
574
575 #[test]
576 fn qualified_value_shape_disjoint_uses_all_sibling_property_shapes() {
577 let ttl = format!(
578 "{PREFIXES}
579 ex:S a sh:NodeShape ;
580 sh:targetNode ex:x ;
581 sh:property ex:A, ex:B .
582 ex:A a sh:PropertyShape ;
583 sh:path ex:p ;
584 sh:qualifiedValueShape [ sh:class ex:TypeA ] ;
585 sh:qualifiedValueShapesDisjoint true ;
586 sh:qualifiedMinCount 1 .
587 ex:B a sh:PropertyShape ;
588 sh:path ex:q ;
589 sh:qualifiedValueShape [ sh:class ex:TypeB ] ;
590 sh:qualifiedValueShapesDisjoint true ;
591 sh:qualifiedMaxCount 10 .
592 ex:x ex:p ex:value .
593 ex:value a ex:TypeA, ex:TypeB .
594 "
595 );
596 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
597 assert!(
598 parsed.diagnostics.is_empty(),
599 "diags: {:?}",
600 parsed.diagnostics
601 );
602 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
603
604 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
605 assert!(!algebra.conforms);
606
607 let report = validate_report(&loaded, &loaded.graph);
608 assert!(!report.conforms);
609 assert_eq!(report.results.len(), 1);
610 assert_eq!(
611 report.results[0].component.as_str(),
612 "http://www.w3.org/ns/shacl#QualifiedMinCountConstraintComponent"
613 );
614 }
615
616 #[test]
617 fn disjoint_on_node_shape_uses_the_focus_node_as_the_value() {
618 let ttl = format!(
619 "{PREFIXES}
620 ex:S a sh:NodeShape ;
621 sh:targetNode ex:valid, ex:invalid ;
622 sh:disjoint ex:p .
623 ex:valid ex:p ex:other .
624 ex:invalid ex:p ex:invalid .
625 "
626 );
627 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
628 assert!(
629 parsed.diagnostics.is_empty(),
630 "diags: {:?}",
631 parsed.diagnostics
632 );
633 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
634
635 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
636 assert!(!algebra.conforms);
637 assert_eq!(algebra.violations.len(), 1);
638 assert_eq!(
639 algebra.violations[0].focus.to_string(),
640 "<http://ex/invalid>"
641 );
642
643 let normalized = shifty_opt::normalize(&parsed.schema);
644 let plan = shifty_opt::plan(&normalized);
645 let planned = validate_plan(&loaded.graph, &plan).unwrap();
646 assert_eq!(planned.conforms, algebra.conforms);
647 assert_eq!(planned.violations.len(), algebra.violations.len());
648
649 let report = validate_report(&loaded, &loaded.graph);
650 assert!(!report.conforms);
651 assert_eq!(report.results.len(), 1);
652 assert_eq!(
653 report.results[0].component.as_str(),
654 "http://www.w3.org/ns/shacl#DisjointConstraintComponent"
655 );
656 assert_eq!(
657 report.results[0].value.as_ref().map(ToString::to_string),
658 Some("<http://ex/invalid>".to_string())
659 );
660 }
661
662 #[test]
663 fn expression_constraint_reports_non_true_values() {
664 let ttl = format!(
666 "{PREFIXES}
667 ex:S a sh:NodeShape ;
668 sh:targetNode true, false ;
669 sh:expression sh:this .
670 "
671 );
672 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
673 assert!(
674 parsed.diagnostics.is_empty(),
675 "diags: {:?}",
676 parsed.diagnostics
677 );
678 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
679
680 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
682 assert!(!algebra.conforms);
683 assert_eq!(algebra.violations.len(), 1);
684 assert_eq!(
685 algebra.violations[0].focus,
686 oxrdf::Term::Literal(oxrdf::Literal::new_typed_literal(
687 "false",
688 oxrdf::vocab::xsd::BOOLEAN
689 ))
690 );
691
692 let normalized = shifty_opt::normalize(&parsed.schema);
694 let plan = shifty_opt::plan(&normalized);
695 let planned = validate_plan(&loaded.graph, &plan).unwrap();
696 assert_eq!(planned.conforms, algebra.conforms);
697 assert_eq!(planned.violations.len(), algebra.violations.len());
698
699 let report = validate_report(&loaded, &loaded.graph);
701 assert!(!report.conforms);
702 assert_eq!(report.results.len(), 1);
703 let result = &report.results[0];
704 assert_eq!(
705 result.component.as_str(),
706 "http://www.w3.org/ns/shacl#ExpressionConstraintComponent"
707 );
708 assert_eq!(result.path, None);
709 assert_eq!(
710 result.value.as_ref().map(ToString::to_string),
711 Some("\"false\"^^<http://www.w3.org/2001/XMLSchema#boolean>".to_string())
712 );
713 }
714
715 #[test]
716 fn expression_constraint_with_path_and_filter() {
717 let ttl = format!(
721 "{PREFIXES}
722 ex:S a sh:NodeShape ;
723 sh:targetNode ex:x ;
724 sh:expression [
725 sh:filterShape [ sh:datatype xsd:boolean ] ;
726 sh:nodes [ sh:path ex:flag ] ;
727 ] .
728 ex:x ex:flag true, false, \"not-a-bool\" .
729 "
730 );
731 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
732 assert!(
733 parsed.diagnostics.is_empty(),
734 "diags: {:?}",
735 parsed.diagnostics
736 );
737 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
738
739 let report = validate_report(&loaded, &loaded.graph);
742 assert!(!report.conforms);
743 assert_eq!(report.results.len(), 1);
744 assert_eq!(
745 report.results[0].component.as_str(),
746 "http://www.w3.org/ns/shacl#ExpressionConstraintComponent"
747 );
748 assert_eq!(
749 report.results[0].value.as_ref().map(ToString::to_string),
750 Some("\"false\"^^<http://www.w3.org/2001/XMLSchema#boolean>".to_string())
751 );
752
753 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
755 assert!(!algebra.conforms);
756 assert_eq!(algebra.violations.len(), 1);
757 }
758
759 #[test]
760 fn expression_constraint_with_function_is_diagnosed() {
761 let ttl = format!(
764 "{PREFIXES}
765 ex:S a sh:NodeShape ;
766 sh:targetNode ex:x ;
767 sh:expression [ ex:fn ( sh:this ) ] .
768 "
769 );
770 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
771 assert!(
772 parsed
773 .diagnostics
774 .iter()
775 .any(|d| d.message.contains("sh:expression")),
776 "expected an unsupported-expression diagnostic, got: {:?}",
777 parsed.diagnostics
778 );
779 }
780
781 #[test]
782 fn sparql_function_expression_evaluates() {
783 let ttl = br#"
786 @prefix sh: <http://www.w3.org/ns/shacl#> .
787 @prefix ex: <http://ex/> .
788 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
789 ex:booleanFunction a sh:SPARQLFunction ;
790 sh:returnType xsd:boolean ;
791 sh:ask "ASK { FILTER (true) }" .
792 ex:withArguments a sh:SPARQLFunction ;
793 sh:parameter [ sh:name "arg1" ; sh:path ex:arg1 ] ,
794 [ sh:name "arg2" ; sh:path ex:arg2 ] ;
795 sh:returnType xsd:string ;
796 sh:select "SELECT ?result WHERE { BIND (CONCAT($arg1, \"-\", $arg2) AS ?result) }" .
797 "#;
798 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
799
800 let b = evaluate_function_expression(&loaded, "ex:booleanFunction()").unwrap();
801 assert_eq!(b, Some(oxrdf::Term::Literal(oxrdf::Literal::from(true))));
802
803 let s = evaluate_function_expression(&loaded, "ex:withArguments(\"A\", \"B\")").unwrap();
804 assert_eq!(
805 s,
806 Some(oxrdf::Term::Literal(oxrdf::Literal::new_simple_literal(
807 "A-B"
808 )))
809 );
810 }
811
812 #[test]
813 fn sparql_function_called_from_sparql_constraint() {
814 let ttl = br#"
817 @prefix sh: <http://www.w3.org/ns/shacl#> .
818 @prefix ex: <http://ex/> .
819 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
820 ex:isOk a sh:SPARQLFunction ;
821 sh:parameter [ sh:path ex:arg ] ;
822 sh:returnType xsd:boolean ;
823 sh:ask "ASK { FILTER (STR($arg) = \"ok\") }" .
824 ex:S a sh:NodeShape ;
825 sh:targetNode ex:x, ex:y ;
826 sh:sparql [ sh:select """SELECT $this ?value WHERE {
827 $this <http://ex/val> ?value .
828 FILTER (! <http://ex/isOk>(?value))
829 }""" ] .
830 ex:x <http://ex/val> "ok" .
831 ex:y <http://ex/val> "bad" .
832 "#;
833 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
834 let report = validate_report(&loaded, &loaded.graph);
835 assert!(!report.conforms);
836 assert_eq!(report.results.len(), 1, "results: {:?}", report.results);
837 assert_eq!(report.results[0].focus.to_string(), "<http://ex/y>");
838 assert_eq!(
839 report.results[0].value.as_ref().map(ToString::to_string),
840 Some("\"bad\"".to_string())
841 );
842 }
843
844 #[test]
845 fn graph_reading_function_policy_gates_loud_failure() {
846 let ttl = br#"
850 @prefix sh: <http://www.w3.org/ns/shacl#> .
851 @prefix ex: <http://ex/> .
852 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
853 ex:exists a sh:SPARQLFunction ;
854 sh:returnType xsd:boolean ;
855 sh:ask "ASK { ?s <http://ex/marker> ?o }" .
856 ex:S a sh:NodeShape ;
857 sh:targetNode ex:x ;
858 sh:sparql [ sh:select
859 "SELECT $this WHERE { $this a <http://ex/T> . FILTER (<http://ex/exists>()) }" ] .
860 ex:x a <http://ex/T> .
861 ex:y <http://ex/marker> "m" .
862 "#;
863 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
864
865 let lenient = validate_report(&loaded, &loaded.graph);
868 assert!(lenient.conforms, "results: {:?}", lenient.results);
869
870 let strict_opts = ValidationOptions {
873 engine: EngineOptions {
874 unsupported: UnsupportedPolicy::Error,
875 },
876 ..Default::default()
877 };
878 let strict = validate_report_with_options(&loaded, &loaded.graph, &strict_opts);
879 assert!(!strict.conforms);
880 assert_eq!(strict.results.len(), 1, "results: {:?}", strict.results);
881 }
882
883 #[test]
884 fn custom_component_ask_validator() {
885 let ttl = br#"
888 @prefix sh: <http://www.w3.org/ns/shacl#> .
889 @prefix ex: <http://ex/> .
890 ex:TestConstraintComponent a sh:ConstraintComponent ;
891 sh:parameter [ sh:path ex:test1 ] , [ sh:path ex:test2 ] ;
892 sh:validator [ a sh:SPARQLAskValidator ;
893 sh:ask "ASK { FILTER (?value = CONCAT($test1, $test2)) }" ] .
894 ex:TestShape a sh:NodeShape ;
895 ex:test1 "Hello " ;
896 ex:test2 "World" ;
897 sh:targetNode "Hallo Welt", "Hello World" .
898 "#;
899 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
900 let report = validate_report(&loaded, &loaded.graph);
901 assert!(!report.conforms);
902 assert_eq!(report.results.len(), 1);
903 let r = &report.results[0];
904 assert_eq!(r.component.as_str(), "http://ex/TestConstraintComponent");
905 assert_eq!(r.focus.to_string(), "\"Hallo Welt\"");
906 assert_eq!(
907 r.value.as_ref().map(ToString::to_string),
908 Some("\"Hallo Welt\"".to_string())
909 );
910 assert_eq!(r.source_shape.to_string(), "<http://ex/TestShape>");
911 assert_eq!(r.path, None);
912
913 let parse_out = shifty_parse::parse_loaded(&loaded);
916 let schema = shifty_opt::normalize(&parse_out.schema);
917 let plan = shifty_opt::plan(&schema);
918 let outcome = validate_plan_graphs(&loaded.graph, &loaded.graph, &plan).unwrap();
919 assert!(!outcome.conforms, "algebra: Hallo Welt must still violate");
920 assert_eq!(
921 outcome.violations.len(),
922 1,
923 "algebra: exactly one violation: {:?}",
924 outcome.violations
925 );
926 assert_eq!(
927 outcome.violations[0].focus.to_string(),
928 "\"Hallo Welt\"",
929 "algebra: wrong focus node"
930 );
931 }
932
933 #[test]
934 fn custom_component_select_node_validator() {
935 let ttl = br#"
938 @prefix sh: <http://www.w3.org/ns/shacl#> .
939 @prefix ex: <http://ex/> .
940 ex:C a sh:ConstraintComponent ;
941 sh:parameter [ sh:path ex:requiredParam ] ;
942 sh:nodeValidator [ a sh:SPARQLSelectValidator ;
943 sh:select """SELECT $this WHERE {
944 $this ?p ?o .
945 FILTER NOT EXISTS { $this <http://ex/property> $requiredParam }
946 }""" ] .
947 ex:S a sh:NodeShape ;
948 ex:requiredParam "Value" ;
949 sh:targetNode ex:Good, ex:Bad .
950 ex:Good <http://ex/property> "Value" .
951 ex:Bad <http://ex/property> "Other" .
952 "#;
953 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
954 let report = validate_report(&loaded, &loaded.graph);
955 assert!(!report.conforms);
956 assert_eq!(report.results.len(), 1);
957 let r = &report.results[0];
958 assert_eq!(r.component.as_str(), "http://ex/C");
959 assert_eq!(r.focus.to_string(), "<http://ex/Bad>");
960 assert_eq!(
961 r.value.as_ref().map(ToString::to_string),
962 Some("<http://ex/Bad>".to_string())
963 );
964 }
965
966 #[test]
967 fn custom_component_not_activated_when_mandatory_param_absent() {
968 let ttl = br#"
971 @prefix sh: <http://www.w3.org/ns/shacl#> .
972 @prefix ex: <http://ex/> .
973 ex:C a sh:ConstraintComponent ;
974 sh:parameter [ sh:path ex:requiredParam ] ;
975 sh:validator [ a sh:SPARQLAskValidator ; sh:ask "ASK { FILTER (false) }" ] .
976 ex:S a sh:NodeShape ;
977 sh:targetNode ex:x .
978 ex:x ex:other "z" .
979 "#;
980 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
981 let report = validate_report(&loaded, &loaded.graph);
982 assert!(report.conforms, "results: {:?}", report.results);
983
984 let parsed = parse_turtle(ttl, None).unwrap();
985 assert!(
986 !parsed
987 .diagnostics
988 .iter()
989 .any(|d| d.message.contains("custom constraint component")),
990 "an inactive component must not be diagnosed: {:?}",
991 parsed.diagnostics
992 );
993 }
994
995 #[test]
996 fn custom_component_property_validator_complex_path() {
997 let ttl = br#"
1001 @prefix sh: <http://www.w3.org/ns/shacl#> .
1002 @prefix ex: <http://ex/> .
1003 ex:ForbidComponent a sh:ConstraintComponent ;
1004 sh:parameter [ sh:path ex:forbidden ] ;
1005 sh:propertyValidator [ a sh:SPARQLSelectValidator ;
1006 sh:select """SELECT $this ?value WHERE {
1007 $this $PATH ?value .
1008 FILTER (STR(?value) = STR($forbidden))
1009 }""" ] .
1010 ex:S a sh:NodeShape ;
1011 sh:targetNode ex:x ;
1012 sh:property [ sh:path ( ex:a ex:b ) ; ex:forbidden "bad" ] .
1013 ex:x ex:a ex:m .
1014 ex:m ex:b "bad", "ok" .
1015 "#;
1016 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
1017 let report = validate_report(&loaded, &loaded.graph);
1018 assert!(!report.conforms);
1019 assert_eq!(report.results.len(), 1, "results: {:?}", report.results);
1020 let r = &report.results[0];
1021 assert_eq!(r.component.as_str(), "http://ex/ForbidComponent");
1022 assert_eq!(r.focus.to_string(), "<http://ex/x>");
1023 assert_eq!(
1024 r.value.as_ref().map(ToString::to_string),
1025 Some("\"bad\"".to_string())
1026 );
1027 }
1028
1029 #[test]
1030 fn custom_component_property_validator_inverse_path() {
1031 let ttl = br#"
1035 @prefix sh: <http://www.w3.org/ns/shacl#> .
1036 @prefix ex: <http://ex/> .
1037 ex:OkComponent a sh:ConstraintComponent ;
1038 sh:parameter [ sh:path ex:want ] ;
1039 sh:validator [ a sh:SPARQLAskValidator ;
1040 sh:ask "ASK { $value <http://ex/label> $want }" ] .
1041 ex:S a sh:NodeShape ;
1042 sh:targetNode ex:p ;
1043 sh:property [ sh:path [ sh:inversePath ex:parent ] ; ex:want "ok" ] .
1044 ex:c1 ex:parent ex:p ; ex:label "ok" .
1045 ex:c2 ex:parent ex:p ; ex:label "no" .
1046 "#;
1047 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
1048 let report = validate_report(&loaded, &loaded.graph);
1049 assert!(!report.conforms);
1052 assert_eq!(report.results.len(), 1, "results: {:?}", report.results);
1053 assert_eq!(
1054 report.results[0].component.as_str(),
1055 "http://ex/OkComponent"
1056 );
1057 assert_eq!(
1058 report.results[0].value.as_ref().map(ToString::to_string),
1059 Some("<http://ex/c2>".to_string())
1060 );
1061 }
1062
1063 #[test]
1064 fn custom_component_ask_validator_subquery_count() {
1065 let shapes_ttl = br#"
1071 @prefix sh: <http://www.w3.org/ns/shacl#> .
1072 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1073 @prefix ex: <urn:ex/> .
1074
1075 ex:countComponent a sh:ConstraintComponent ;
1076 sh:parameter [ sh:path ex:exactCount ; sh:datatype xsd:integer ] ;
1077 sh:parameter [ sh:path ex:class ] ;
1078 sh:validator ex:hasExactCount .
1079
1080 ex:hasExactCount a sh:SPARQLAskValidator ;
1081 sh:message "Wrong count" ;
1082 sh:ask """
1083 ASK {
1084 {
1085 SELECT (COUNT(DISTINCT ?i) AS ?count)
1086 WHERE { ?i a $class . }
1087 }
1088 FILTER (?count = $exactCount)
1089 }
1090 """ .
1091
1092 ex:shape a sh:NodeShape ;
1093 sh:targetNode ex:sentinel ;
1094 ex:class ex:Thing ;
1095 ex:exactCount 1 .
1096 "#;
1097 let shapes = shifty_parse::load_turtle(shapes_ttl, None).unwrap();
1098
1099 let data_none =
1101 shifty_parse::load_turtle(b"@prefix ex: <urn:ex/> . ex:sentinel a ex:Sentinel .", None)
1102 .unwrap();
1103 let report = validate_report_graphs(&shapes, &data_none.graph);
1104 assert!(
1105 !report.conforms,
1106 "zero Things with exactCount=1 must not conform: {:?}",
1107 report.results
1108 );
1109
1110 let data_one = shifty_parse::load_turtle(
1112 b"@prefix ex: <urn:ex/> . ex:sentinel a ex:Sentinel . ex:t1 a ex:Thing .",
1113 None,
1114 )
1115 .unwrap();
1116 let report_ok = validate_report_graphs(&shapes, &data_one.graph);
1117 assert!(
1118 report_ok.conforms,
1119 "exactly one Thing with exactCount=1 must conform: {:?}",
1120 report_ok.results
1121 );
1122
1123 let data_two = shifty_parse::load_turtle(
1125 b"@prefix ex: <urn:ex/> . ex:sentinel a ex:Sentinel . ex:t1 a ex:Thing . ex:t2 a ex:Thing .",
1126 None,
1127 )
1128 .unwrap();
1129 let report_two = validate_report_graphs(&shapes, &data_two.graph);
1130 assert!(
1131 !report_two.conforms,
1132 "two Things with exactCount=1 must not conform: {:?}",
1133 report_two.results
1134 );
1135
1136 let parse_out = shifty_parse::parse_loaded(&shapes);
1138 let schema = shifty_opt::normalize(&parse_out.schema);
1139 let plan = shifty_opt::plan(&schema);
1140 let alg_none = validate_plan_graphs(&data_none.graph, &shapes.graph, &plan).unwrap();
1141 assert!(!alg_none.conforms, "algebra: zero Things must not conform");
1142 let alg_one = validate_plan_graphs(&data_one.graph, &shapes.graph, &plan).unwrap();
1143 assert!(alg_one.conforms, "algebra: one Thing must conform");
1144 let alg_two = validate_plan_graphs(&data_two.graph, &shapes.graph, &plan).unwrap();
1145 assert!(!alg_two.conforms, "algebra: two Things must not conform");
1146 }
1147
1148 #[test]
1149 fn custom_component_invalid_sparql_ignored_by_default() {
1150 let shapes_ttl = br#"
1156 @prefix sh: <http://www.w3.org/ns/shacl#> .
1157 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1158 @prefix ex: <urn:ex/> .
1159
1160 ex:badComponent a sh:ConstraintComponent ;
1161 sh:parameter [ sh:path ex:exactCount ; sh:datatype xsd:integer ] ;
1162 sh:parameter [ sh:path ex:class ] ;
1163 sh:validator [ a sh:SPARQLAskValidator ;
1164 sh:ask """
1165 ASK WHERE {
1166 {
1167 SELECT *
1168 WHERE { ?i a $class . }
1169 HAVING (COUNT(DISTINCT ?i) = $exactCount)
1170 }
1171 }
1172 """ ] .
1173
1174 ex:shape a sh:NodeShape ;
1175 sh:targetNode ex:sentinel ;
1176 ex:class ex:Thing ;
1177 ex:exactCount 1 .
1178 "#;
1179 let shapes = shifty_parse::load_turtle(shapes_ttl, None).unwrap();
1180 let data =
1181 shifty_parse::load_turtle(b"@prefix ex: <urn:ex/> . ex:sentinel a ex:Sentinel .", None)
1182 .unwrap();
1183
1184 let report = validate_report_graphs(&shapes, &data.graph);
1186 assert!(
1187 report.conforms,
1188 "bad validator query silently skipped under Ignore: {:?}",
1189 report.results
1190 );
1191 }
1192
1193 #[test]
1194 fn equals_on_node_shape_uses_the_focus_node_as_the_value() {
1195 let ttl = format!(
1196 "{PREFIXES}
1197 ex:S a sh:NodeShape ;
1198 sh:targetNode ex:valid, ex:extra, ex:missing ;
1199 sh:equals ex:p .
1200 ex:valid ex:p ex:valid .
1201 ex:extra ex:p ex:extra, ex:other .
1202 "
1203 );
1204 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1205 assert!(
1206 parsed.diagnostics.is_empty(),
1207 "diags: {:?}",
1208 parsed.diagnostics
1209 );
1210 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1211
1212 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
1213 assert!(!algebra.conforms);
1214 let mut foci: Vec<_> = algebra
1215 .violations
1216 .iter()
1217 .map(|violation| violation.focus.to_string())
1218 .collect();
1219 foci.sort();
1220 assert_eq!(
1221 foci,
1222 [
1223 "<http://ex/extra>".to_string(),
1224 "<http://ex/missing>".to_string()
1225 ]
1226 );
1227
1228 let normalized = shifty_opt::normalize(&parsed.schema);
1229 let plan = shifty_opt::plan(&normalized);
1230 let planned = validate_plan(&loaded.graph, &plan).unwrap();
1231 assert_eq!(planned.conforms, algebra.conforms);
1232 assert_eq!(planned.violations.len(), algebra.violations.len());
1233
1234 let report = validate_report(&loaded, &loaded.graph);
1235 assert!(!report.conforms);
1236 assert_eq!(report.results.len(), 2);
1237 assert!(report.results.iter().all(|result| result.component.as_str()
1238 == "http://www.w3.org/ns/shacl#EqualsConstraintComponent"));
1239 }
1240
1241 #[test]
1242 fn datatype_violation() {
1243 let ttl = format!(
1244 "{PREFIXES}
1245 ex:S a sh:NodeShape ;
1246 sh:targetNode ex:x ;
1247 sh:property [ sh:path ex:p ; sh:datatype xsd:integer ] .
1248 ex:x ex:p \"hello\" .
1249 "
1250 );
1251 assert!(!run(&ttl).conforms);
1252 }
1253
1254 #[test]
1255 fn nodekind_and_class_target() {
1256 let ttl = format!(
1257 "{PREFIXES}
1258 ex:S a sh:NodeShape ;
1259 sh:targetClass ex:Person ;
1260 sh:property [ sh:path ex:knows ; sh:nodeKind sh:IRI ] .
1261 ex:alice a ex:Person ; ex:knows ex:bob .
1262 ex:carol a ex:Person ; ex:knows \"notaniri\" .
1263 "
1264 );
1265 let outcome = run(&ttl);
1266 assert!(!outcome.conforms);
1267 let bad: Vec<_> = outcome
1268 .violations
1269 .iter()
1270 .map(|r| r.focus.to_string())
1271 .collect();
1272 assert_eq!(bad, vec!["<http://ex/carol>".to_string()]);
1273 }
1274
1275 #[test]
1276 fn recursion_over_cyclic_data_terminates() {
1277 let ttl = format!(
1279 "{PREFIXES}
1280 ex:S a sh:NodeShape ;
1281 sh:targetNode ex:a ;
1282 sh:property [ sh:path ex:knows ; sh:node ex:S ; sh:nodeKind sh:IRI ] .
1283 ex:a ex:knows ex:b .
1284 ex:b ex:knows ex:a .
1285 "
1286 );
1287 assert!(run(&ttl).conforms);
1290 }
1291
1292 #[test]
1293 fn empty_graph_conforms() {
1294 let outcome = validate(&Graph::new(), &shifty_algebra::Schema::new()).unwrap();
1295 assert!(outcome.conforms);
1296 }
1297
1298 #[test]
1299 fn non_stratifiable_schema_is_diagnosed() {
1300 let ttl = format!(
1302 "{PREFIXES}
1303 ex:S a sh:NodeShape ;
1304 sh:targetNode ex:x ;
1305 sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ; sh:qualifiedMinCount 1 ] .
1306 ex:x ex:p ex:y .
1307 "
1308 );
1309 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1310 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1311 assert!(validate(&loaded.graph, &out.schema).is_err());
1312 }
1313
1314 fn triple(s: &str, p: &str, o: &str) -> oxrdf::Triple {
1315 use oxrdf::NamedNode;
1316 oxrdf::Triple::new(
1317 NamedNode::new(s).unwrap(),
1318 NamedNode::new(p).unwrap(),
1319 NamedNode::new(o).unwrap(),
1320 )
1321 }
1322
1323 #[test]
1324 fn triple_rule_infers_from_path() {
1325 let ttl = format!(
1327 "{PREFIXES}
1328 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
1329 sh:rule [ a sh:TripleRule ;
1330 sh:subject sh:this ; sh:predicate ex:knows2 ;
1331 sh:object [ sh:path ex:knows ] ] .
1332 ex:a a ex:Person ; ex:knows ex:b .
1333 "
1334 );
1335 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1336 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1337 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1338 assert_eq!(outcome.inferred.len(), 1);
1339 assert!(
1340 outcome
1341 .graph
1342 .contains(&triple("http://ex/a", "http://ex/knows2", "http://ex/b"))
1343 );
1344 }
1345
1346 #[test]
1347 fn inference_reaches_a_fixpoint() {
1348 let ttl = format!(
1351 "{PREFIXES}
1352 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
1353 sh:rule [ a sh:TripleRule ;
1354 sh:subject sh:this ; sh:predicate ex:reaches ;
1355 sh:object [ sh:path [ sh:alternativePath ( ex:knows ( ex:knows ex:reaches ) ) ] ] ] .
1356 ex:a a ex:Person ; ex:knows ex:b .
1357 ex:b a ex:Person ; ex:knows ex:c .
1358 ex:c a ex:Person .
1359 "
1360 );
1361 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1362 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1363 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1364 assert!(
1365 outcome
1366 .graph
1367 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/b"))
1368 );
1369 assert!(
1370 outcome
1371 .graph
1372 .contains(&triple("http://ex/b", "http://ex/reaches", "http://ex/c"))
1373 );
1374 assert!(
1376 outcome
1377 .graph
1378 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/c"))
1379 );
1380 }
1381
1382 #[test]
1383 fn later_order_output_reactivates_an_earlier_rule() {
1384 let ttl = format!(
1385 "{PREFIXES}
1386 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1387 sh:rule [
1388 a sh:TripleRule ; sh:order 0 ;
1389 sh:subject sh:this ; sh:predicate ex:done ;
1390 sh:object [ sh:path ex:ready ]
1391 ] ;
1392 sh:rule [
1393 a sh:TripleRule ; sh:order 1 ;
1394 sh:subject sh:this ; sh:predicate ex:ready ;
1395 sh:object ex:y
1396 ] .
1397 "
1398 );
1399 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1400 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1401 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1402
1403 assert!(
1404 outcome
1405 .graph
1406 .contains(&triple("http://ex/x", "http://ex/ready", "http://ex/y"))
1407 );
1408 assert!(
1409 outcome
1410 .graph
1411 .contains(&triple("http://ex/x", "http://ex/done", "http://ex/y"))
1412 );
1413 }
1414
1415 #[test]
1416 fn inferred_triples_can_create_new_rule_targets() {
1417 let ttl = format!(
1418 "{PREFIXES}
1419 ex:Seed a sh:NodeShape ; sh:targetNode ex:x ;
1420 sh:rule [
1421 a sh:TripleRule ;
1422 sh:subject sh:this ; sh:predicate ex:eligible ;
1423 sh:object ex:y
1424 ] .
1425 ex:Eligible a sh:NodeShape ; sh:targetSubjectsOf ex:eligible ;
1426 sh:rule [
1427 a sh:TripleRule ;
1428 sh:subject sh:this ; sh:predicate ex:classified ;
1429 sh:object ex:yes
1430 ] .
1431 "
1432 );
1433 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1434 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1435 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1436
1437 assert!(outcome.graph.contains(&triple(
1438 "http://ex/x",
1439 "http://ex/classified",
1440 "http://ex/yes",
1441 )));
1442 }
1443
1444 #[test]
1445 fn split_inference_uses_shapes_graph_as_rule_context() {
1446 let shapes_ttl = format!(
1447 "{PREFIXES}
1448 ex:InverseShape a sh:NodeShape ;
1449 sh:targetClass ex:Thing ;
1450 sh:rule [
1451 a sh:SPARQLRule ;
1452 sh:construct \"\"\"
1453 CONSTRUCT {{ ?o ?inverse $this }}
1454 WHERE {{
1455 $this ?predicate ?o .
1456 ?predicate ex:inverseOf ?inverse .
1457 }}
1458 \"\"\"
1459 ] .
1460 ex:p ex:inverseOf ex:q .
1461 "
1462 );
1463 let data_ttl = format!(
1464 "{PREFIXES}
1465 ex:a a ex:Thing ; ex:p ex:b .
1466 "
1467 );
1468 let shapes = shifty_parse::load_turtle(shapes_ttl.as_bytes(), None).unwrap();
1469 let parsed = shifty_parse::parse_loaded(&shapes);
1470 let data = shifty_parse::load_turtle(data_ttl.as_bytes(), None).unwrap();
1471
1472 let outcome = infer_graphs(&data.graph, &shapes.graph, &parsed.schema).unwrap();
1473
1474 assert!(
1475 outcome
1476 .graph
1477 .contains(&triple("http://ex/b", "http://ex/q", "http://ex/a"))
1478 );
1479 assert_eq!(outcome.inferred.len(), 1);
1480 }
1481}