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