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 entry_shape_names_limit_top_level_validation_but_keep_dependencies() {
227 let ttl = format!(
228 "{PREFIXES}
229 ex:Selected a sh:NodeShape ;
230 sh:targetNode ex:a ;
231 sh:node ex:Helper .
232
233 ex:Skipped a sh:NodeShape ;
234 sh:targetNode ex:b ;
235 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
236
237 ex:Helper a sh:NodeShape ;
238 sh:property [ sh:path ex:required ; sh:minCount 1 ] .
239
240 ex:a ex:p \"present\" .
241 ex:b a ex:Thing .
242 "
243 );
244 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
245 let parsed = shifty_parse::parse_loaded(&loaded);
246 let normalized = shifty_opt::normalize(&parsed.schema);
247 let plan = shifty_opt::plan(&normalized);
248 let options = ValidationOptions {
249 entry_shape_names: vec!["http://ex/Selected".to_string()],
250 ..Default::default()
251 };
252
253 let outcome = validate_plan_with_options(&loaded.graph, &plan, &options).unwrap();
254 assert!(!outcome.conforms);
255 assert_eq!(outcome.violations.len(), 1);
256 assert_eq!(outcome.violations[0].focus.to_string(), "<http://ex/a>");
257
258 let report = validate_report_with_options(&loaded, &loaded.graph, &options);
259 assert!(!report.conforms);
260 assert_eq!(report.results.len(), 1);
261 assert_eq!(report.results[0].focus.to_string(), "<http://ex/a>");
262 }
263
264 #[test]
265 fn inference_rules_fire_for_implicit_class_targets() {
266 let ttl = br#"
267 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
268 @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
269 @prefix owl: <http://www.w3.org/2002/07/owl#> .
270 @prefix sh: <http://www.w3.org/ns/shacl#> .
271 @prefix ex: <http://ex/> .
272
273 ex:Parent a owl:Class, sh:NodeShape ;
274 sh:rule [
275 a sh:TripleRule ;
276 sh:subject sh:this ;
277 sh:predicate ex:hasTag ;
278 sh:object ex:Tag
279 ] .
280
281 ex:Child rdfs:subClassOf ex:Parent .
282 ex:item a ex:Child .
283 "#;
284 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
285 let parsed = shifty_parse::parse_loaded(&loaded);
286 let normalized = shifty_opt::normalize(&parsed.schema);
287
288 let outcome = infer(&loaded.graph, &normalized).expect("stratifiable schema");
289
290 assert!(outcome.graph.contains(&oxrdf::Triple::new(
291 oxrdf::NamedNode::new_unchecked("http://ex/item"),
292 oxrdf::NamedNode::new_unchecked("http://ex/hasTag"),
293 oxrdf::NamedNode::new_unchecked("http://ex/Tag"),
294 )));
295 }
296
297 fn infer_ttl(ttl: &[u8]) -> InferenceOutcome {
300 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
301 let parsed = shifty_parse::parse_loaded(&loaded);
302 assert!(
303 parsed.diagnostics.is_empty(),
304 "parse diagnostics: {:?}",
305 parsed.diagnostics
306 );
307 let normalized = shifty_opt::normalize(&parsed.schema);
308 infer(&loaded.graph, &normalized).expect("stratifiable schema")
309 }
310
311 fn triple_term(s: &str, p: &str, o: impl Into<oxrdf::Term>) -> oxrdf::Triple {
312 oxrdf::Triple::new(
313 oxrdf::NamedNode::new_unchecked(s),
314 oxrdf::NamedNode::new_unchecked(p),
315 o,
316 )
317 }
318
319 #[test]
320 fn rule_object_union_node_expression() {
321 let outcome = infer_ttl(
323 br#"
324 @prefix sh: <http://www.w3.org/ns/shacl#> .
325 @prefix ex: <http://ex/> .
326 ex:PersonShape a sh:NodeShape ;
327 sh:targetClass ex:Person ;
328 sh:rule [
329 a sh:TripleRule ;
330 sh:subject sh:this ;
331 sh:predicate ex:contact ;
332 sh:object [ sh:union ( [ sh:path ex:email ] [ sh:path ex:phone ] ) ]
333 ] .
334 ex:alice a ex:Person ; ex:email "a@x.org" ; ex:phone "555-1234" .
335 "#,
336 );
337 let email = oxrdf::Literal::new_simple_literal("a@x.org");
338 let phone = oxrdf::Literal::new_simple_literal("555-1234");
339 assert!(outcome.graph.contains(&triple_term(
340 "http://ex/alice",
341 "http://ex/contact",
342 email
343 )));
344 assert!(outcome.graph.contains(&triple_term(
345 "http://ex/alice",
346 "http://ex/contact",
347 phone
348 )));
349 }
350
351 #[test]
352 fn rule_object_intersection_node_expression() {
353 let outcome = infer_ttl(
355 br#"
356 @prefix sh: <http://www.w3.org/ns/shacl#> .
357 @prefix ex: <http://ex/> .
358 ex:S a sh:NodeShape ;
359 sh:targetClass ex:T ;
360 sh:rule [
361 a sh:TripleRule ;
362 sh:subject sh:this ;
363 sh:predicate ex:both ;
364 sh:object [ sh:intersection ( [ sh:path ex:a ] [ sh:path ex:b ] ) ]
365 ] .
366 ex:x a ex:T ; ex:a ex:shared, ex:onlyA ; ex:b ex:shared, ex:onlyB .
367 "#,
368 );
369 let both = "http://ex/both";
370 assert!(outcome.graph.contains(&triple_term(
371 "http://ex/x",
372 both,
373 oxrdf::NamedNode::new_unchecked("http://ex/shared")
374 )));
375 assert!(!outcome.graph.contains(&triple_term(
376 "http://ex/x",
377 both,
378 oxrdf::NamedNode::new_unchecked("http://ex/onlyA")
379 )));
380 assert!(!outcome.graph.contains(&triple_term(
381 "http://ex/x",
382 both,
383 oxrdf::NamedNode::new_unchecked("http://ex/onlyB")
384 )));
385 }
386
387 #[test]
388 fn rule_object_filter_node_expression() {
389 let outcome = infer_ttl(
391 br#"
392 @prefix sh: <http://www.w3.org/ns/shacl#> .
393 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
394 @prefix ex: <http://ex/> .
395 ex:IntShape a sh:NodeShape ; sh:datatype xsd:integer .
396 ex:S a sh:NodeShape ;
397 sh:targetClass ex:T ;
398 sh:rule [
399 a sh:TripleRule ;
400 sh:subject sh:this ;
401 sh:predicate ex:intValue ;
402 sh:object [ sh:filterShape ex:IntShape ; sh:nodes [ sh:path ex:value ] ]
403 ] .
404 ex:x a ex:T ; ex:value 42, "hello" .
405 "#,
406 );
407 let int_value = "http://ex/intValue";
408 assert!(outcome.graph.contains(&triple_term(
409 "http://ex/x",
410 int_value,
411 oxrdf::Literal::new_typed_literal(
412 "42",
413 oxrdf::NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#integer")
414 )
415 )));
416 assert!(!outcome.graph.contains(&triple_term(
417 "http://ex/x",
418 int_value,
419 oxrdf::Literal::new_simple_literal("hello")
420 )));
421 }
422
423 #[test]
424 fn planned_validation_preserves_severity_and_applies_threshold() {
425 let ttl = format!(
426 "{PREFIXES}
427 ex:S a sh:NodeShape ;
428 sh:targetNode ex:x ;
429 sh:property ex:InfoShape, ex:WarningShape .
430 ex:InfoShape a sh:PropertyShape ;
431 sh:path ex:required ;
432 sh:minCount 1 ;
433 sh:severity sh:Info .
434 ex:WarningShape a sh:PropertyShape ;
435 sh:path ex:required ;
436 sh:minCount 1 ;
437 sh:severity sh:Warning .
438 "
439 );
440 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
441 let parsed = shifty_parse::parse_loaded(&loaded);
442 let normalized = shifty_opt::normalize(&parsed.schema);
443 let plan = shifty_opt::plan(&normalized);
444
445 let info = validate_plan_with_options(
446 &loaded.graph,
447 &plan,
448 &ValidationOptions {
449 minimum_severity: shifty_algebra::Severity::Info,
450 sort_results: true,
451 ..Default::default()
452 },
453 )
454 .unwrap();
455 assert!(!info.conforms);
456 assert_eq!(info.violations.len(), 1);
457 assert_eq!(
458 info.violations[0].severity,
459 shifty_algebra::Severity::Warning
460 );
461 let mut severities: Vec<_> = info.violations[0]
462 .reasons
463 .iter()
464 .map(|reason| reason.severity.clone())
465 .collect();
466 severities.sort_by_key(shifty_algebra::Severity::rank);
467 assert_eq!(
468 severities,
469 vec![
470 shifty_algebra::Severity::Info,
471 shifty_algebra::Severity::Warning
472 ]
473 );
474
475 let warning = validate_plan_with_options(
476 &loaded.graph,
477 &plan,
478 &ValidationOptions {
479 minimum_severity: shifty_algebra::Severity::Warning,
480 sort_results: true,
481 ..Default::default()
482 },
483 )
484 .unwrap();
485 assert!(!warning.conforms);
486
487 let violation = validate_plan_with_options(
488 &loaded.graph,
489 &plan,
490 &ValidationOptions {
491 minimum_severity: shifty_algebra::Severity::Violation,
492 sort_results: true,
493 ..Default::default()
494 },
495 )
496 .unwrap();
497 assert!(violation.conforms);
498 assert_eq!(violation.violations.len(), 1);
499
500 let report = validate_report_with_options(
501 &loaded,
502 &loaded.graph,
503 &ValidationOptions {
504 minimum_severity: shifty_algebra::Severity::Violation,
505 sort_results: true,
506 ..Default::default()
507 },
508 );
509 assert!(report.conforms);
510 assert_eq!(report.results.len(), 2);
511 }
512
513 #[test]
514 fn validation_findings_sort_by_severity_then_focus_node() {
515 let ttl = format!(
516 "{PREFIXES}
517 ex:InfoShape a sh:NodeShape ;
518 sh:targetNode ex:a ;
519 sh:nodeKind sh:Literal ;
520 sh:severity sh:Info .
521 ex:WarningShape a sh:NodeShape ;
522 sh:targetNode ex:z ;
523 sh:nodeKind sh:Literal ;
524 sh:severity sh:Warning .
525 ex:ViolationShape a sh:NodeShape ;
526 sh:targetNode ex:m ;
527 sh:nodeKind sh:Literal .
528 "
529 );
530 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
531 let parsed = shifty_parse::parse_loaded(&loaded);
532 let plan = shifty_opt::plan(&shifty_opt::normalize(&parsed.schema));
533 let outcome = validate_plan(&loaded.graph, &plan).unwrap();
534
535 let ordered: Vec<_> = outcome
536 .violations
537 .iter()
538 .map(|finding| (finding.severity.clone(), finding.focus.to_string()))
539 .collect();
540 assert_eq!(
541 ordered,
542 vec![
543 (
544 shifty_algebra::Severity::Violation,
545 "<http://ex/m>".to_string()
546 ),
547 (
548 shifty_algebra::Severity::Warning,
549 "<http://ex/z>".to_string()
550 ),
551 (shifty_algebra::Severity::Info, "<http://ex/a>".to_string()),
552 ]
553 );
554 }
555
556 #[test]
557 fn reports_specific_failing_constraints() {
558 let ttl = format!(
559 "{PREFIXES}
560 ex:S a sh:NodeShape ;
561 sh:targetNode ex:x ;
562 sh:closed true ;
563 sh:ignoredProperties ( rdf:type ) ;
564 sh:property [ sh:path ex:age ; sh:datatype xsd:integer ; sh:maxCount 1 ] .
565 ex:x ex:age \"foo\" , 5 ; ex:extra 1 .
566 "
567 );
568 let outcome = run(&ttl);
569 assert!(!outcome.conforms);
570 assert_eq!(outcome.violations.len(), 1);
571 let msgs: Vec<&str> = outcome.violations[0]
572 .reasons
573 .iter()
574 .map(|r| r.message.as_str())
575 .collect();
576 assert!(
578 msgs.iter().any(|m| m.contains("datatype(xsd:integer)")),
579 "missing datatype reason: {msgs:?}"
580 );
581 assert!(
582 msgs.iter().any(|m| m.contains("at most 1")),
583 "missing maxCount reason: {msgs:?}"
584 );
585 assert!(
586 msgs.iter()
587 .any(|m| m.contains("closed") && m.contains("extra")),
588 "missing closed reason: {msgs:?}"
589 );
590 }
591
592 #[test]
593 fn cardinality_and_datatype() {
594 let ttl = format!(
595 "{PREFIXES}
596 ex:S a sh:NodeShape ;
597 sh:targetNode ex:alice, ex:bob ;
598 sh:property [ sh:path ex:age ; sh:maxCount 1 ; sh:datatype xsd:integer ] .
599 ex:alice ex:age 30 .
600 ex:bob ex:age 30 ; ex:age 40 .
601 "
602 );
603 let outcome = run(&ttl);
604 assert!(!outcome.conforms);
605 let bad: Vec<_> = outcome
607 .violations
608 .iter()
609 .map(|r| r.focus.to_string())
610 .collect();
611 assert_eq!(bad, vec!["<http://ex/bob>".to_string()]);
612 }
613
614 #[test]
615 fn qualified_value_shape_disjoint_uses_all_sibling_property_shapes() {
616 let ttl = format!(
617 "{PREFIXES}
618 ex:S a sh:NodeShape ;
619 sh:targetNode ex:x ;
620 sh:property ex:A, ex:B .
621 ex:A a sh:PropertyShape ;
622 sh:path ex:p ;
623 sh:qualifiedValueShape [ sh:class ex:TypeA ] ;
624 sh:qualifiedValueShapesDisjoint true ;
625 sh:qualifiedMinCount 1 .
626 ex:B a sh:PropertyShape ;
627 sh:path ex:q ;
628 sh:qualifiedValueShape [ sh:class ex:TypeB ] ;
629 sh:qualifiedValueShapesDisjoint true ;
630 sh:qualifiedMaxCount 10 .
631 ex:x ex:p ex:value .
632 ex:value a ex:TypeA, ex:TypeB .
633 "
634 );
635 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
636 assert!(
637 parsed.diagnostics.is_empty(),
638 "diags: {:?}",
639 parsed.diagnostics
640 );
641 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
642
643 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
644 assert!(!algebra.conforms);
645
646 let report = validate_report(&loaded, &loaded.graph);
647 assert!(!report.conforms);
648 assert_eq!(report.results.len(), 1);
649 assert_eq!(
650 report.results[0].component.as_str(),
651 "http://www.w3.org/ns/shacl#QualifiedMinCountConstraintComponent"
652 );
653 }
654
655 #[test]
656 fn disjoint_on_node_shape_uses_the_focus_node_as_the_value() {
657 let ttl = format!(
658 "{PREFIXES}
659 ex:S a sh:NodeShape ;
660 sh:targetNode ex:valid, ex:invalid ;
661 sh:disjoint ex:p .
662 ex:valid ex:p ex:other .
663 ex:invalid ex:p ex:invalid .
664 "
665 );
666 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
667 assert!(
668 parsed.diagnostics.is_empty(),
669 "diags: {:?}",
670 parsed.diagnostics
671 );
672 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
673
674 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
675 assert!(!algebra.conforms);
676 assert_eq!(algebra.violations.len(), 1);
677 assert_eq!(
678 algebra.violations[0].focus.to_string(),
679 "<http://ex/invalid>"
680 );
681
682 let normalized = shifty_opt::normalize(&parsed.schema);
683 let plan = shifty_opt::plan(&normalized);
684 let planned = validate_plan(&loaded.graph, &plan).unwrap();
685 assert_eq!(planned.conforms, algebra.conforms);
686 assert_eq!(planned.violations.len(), algebra.violations.len());
687
688 let report = validate_report(&loaded, &loaded.graph);
689 assert!(!report.conforms);
690 assert_eq!(report.results.len(), 1);
691 assert_eq!(
692 report.results[0].component.as_str(),
693 "http://www.w3.org/ns/shacl#DisjointConstraintComponent"
694 );
695 assert_eq!(
696 report.results[0].value.as_ref().map(ToString::to_string),
697 Some("<http://ex/invalid>".to_string())
698 );
699 }
700
701 #[test]
702 fn expression_constraint_reports_non_true_values() {
703 let ttl = format!(
705 "{PREFIXES}
706 ex:S a sh:NodeShape ;
707 sh:targetNode true, false ;
708 sh:expression sh:this .
709 "
710 );
711 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
712 assert!(
713 parsed.diagnostics.is_empty(),
714 "diags: {:?}",
715 parsed.diagnostics
716 );
717 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
718
719 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
721 assert!(!algebra.conforms);
722 assert_eq!(algebra.violations.len(), 1);
723 assert_eq!(
724 algebra.violations[0].focus,
725 oxrdf::Term::Literal(oxrdf::Literal::new_typed_literal(
726 "false",
727 oxrdf::vocab::xsd::BOOLEAN
728 ))
729 );
730
731 let normalized = shifty_opt::normalize(&parsed.schema);
733 let plan = shifty_opt::plan(&normalized);
734 let planned = validate_plan(&loaded.graph, &plan).unwrap();
735 assert_eq!(planned.conforms, algebra.conforms);
736 assert_eq!(planned.violations.len(), algebra.violations.len());
737
738 let report = validate_report(&loaded, &loaded.graph);
740 assert!(!report.conforms);
741 assert_eq!(report.results.len(), 1);
742 let result = &report.results[0];
743 assert_eq!(
744 result.component.as_str(),
745 "http://www.w3.org/ns/shacl#ExpressionConstraintComponent"
746 );
747 assert_eq!(result.path, None);
748 assert_eq!(
749 result.value.as_ref().map(ToString::to_string),
750 Some("\"false\"^^<http://www.w3.org/2001/XMLSchema#boolean>".to_string())
751 );
752 }
753
754 #[test]
755 fn expression_constraint_with_path_and_filter() {
756 let ttl = format!(
760 "{PREFIXES}
761 ex:S a sh:NodeShape ;
762 sh:targetNode ex:x ;
763 sh:expression [
764 sh:filterShape [ sh:datatype xsd:boolean ] ;
765 sh:nodes [ sh:path ex:flag ] ;
766 ] .
767 ex:x ex:flag true, false, \"not-a-bool\" .
768 "
769 );
770 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
771 assert!(
772 parsed.diagnostics.is_empty(),
773 "diags: {:?}",
774 parsed.diagnostics
775 );
776 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
777
778 let report = validate_report(&loaded, &loaded.graph);
781 assert!(!report.conforms);
782 assert_eq!(report.results.len(), 1);
783 assert_eq!(
784 report.results[0].component.as_str(),
785 "http://www.w3.org/ns/shacl#ExpressionConstraintComponent"
786 );
787 assert_eq!(
788 report.results[0].value.as_ref().map(ToString::to_string),
789 Some("\"false\"^^<http://www.w3.org/2001/XMLSchema#boolean>".to_string())
790 );
791
792 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
794 assert!(!algebra.conforms);
795 assert_eq!(algebra.violations.len(), 1);
796 }
797
798 #[test]
799 fn expression_constraint_with_function_is_diagnosed() {
800 let ttl = format!(
803 "{PREFIXES}
804 ex:S a sh:NodeShape ;
805 sh:targetNode ex:x ;
806 sh:expression [ ex:fn ( sh:this ) ] .
807 "
808 );
809 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
810 assert!(
811 parsed
812 .diagnostics
813 .iter()
814 .any(|d| d.message.contains("sh:expression")),
815 "expected an unsupported-expression diagnostic, got: {:?}",
816 parsed.diagnostics
817 );
818 }
819
820 #[test]
821 fn sparql_function_expression_evaluates() {
822 let ttl = br#"
825 @prefix sh: <http://www.w3.org/ns/shacl#> .
826 @prefix ex: <http://ex/> .
827 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
828 ex:booleanFunction a sh:SPARQLFunction ;
829 sh:returnType xsd:boolean ;
830 sh:ask "ASK { FILTER (true) }" .
831 ex:withArguments a sh:SPARQLFunction ;
832 sh:parameter [ sh:name "arg1" ; sh:path ex:arg1 ] ,
833 [ sh:name "arg2" ; sh:path ex:arg2 ] ;
834 sh:returnType xsd:string ;
835 sh:select "SELECT ?result WHERE { BIND (CONCAT($arg1, \"-\", $arg2) AS ?result) }" .
836 "#;
837 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
838
839 let b = evaluate_function_expression(&loaded, "ex:booleanFunction()").unwrap();
840 assert_eq!(b, Some(oxrdf::Term::Literal(oxrdf::Literal::from(true))));
841
842 let s = evaluate_function_expression(&loaded, "ex:withArguments(\"A\", \"B\")").unwrap();
843 assert_eq!(
844 s,
845 Some(oxrdf::Term::Literal(oxrdf::Literal::new_simple_literal(
846 "A-B"
847 )))
848 );
849 }
850
851 #[test]
852 fn sparql_function_called_from_sparql_constraint() {
853 let ttl = br#"
856 @prefix sh: <http://www.w3.org/ns/shacl#> .
857 @prefix ex: <http://ex/> .
858 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
859 ex:isOk a sh:SPARQLFunction ;
860 sh:parameter [ sh:path ex:arg ] ;
861 sh:returnType xsd:boolean ;
862 sh:ask "ASK { FILTER (STR($arg) = \"ok\") }" .
863 ex:S a sh:NodeShape ;
864 sh:targetNode ex:x, ex:y ;
865 sh:sparql [ sh:select """SELECT $this ?value WHERE {
866 $this <http://ex/val> ?value .
867 FILTER (! <http://ex/isOk>(?value))
868 }""" ] .
869 ex:x <http://ex/val> "ok" .
870 ex:y <http://ex/val> "bad" .
871 "#;
872 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
873 let report = validate_report(&loaded, &loaded.graph);
874 assert!(!report.conforms);
875 assert_eq!(report.results.len(), 1, "results: {:?}", report.results);
876 assert_eq!(report.results[0].focus.to_string(), "<http://ex/y>");
877 assert_eq!(
878 report.results[0].value.as_ref().map(ToString::to_string),
879 Some("\"bad\"".to_string())
880 );
881 }
882
883 #[test]
884 fn graph_reading_function_policy_gates_loud_failure() {
885 let ttl = br#"
889 @prefix sh: <http://www.w3.org/ns/shacl#> .
890 @prefix ex: <http://ex/> .
891 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
892 ex:exists a sh:SPARQLFunction ;
893 sh:returnType xsd:boolean ;
894 sh:ask "ASK { ?s <http://ex/marker> ?o }" .
895 ex:S a sh:NodeShape ;
896 sh:targetNode ex:x ;
897 sh:sparql [ sh:select
898 "SELECT $this WHERE { $this a <http://ex/T> . FILTER (<http://ex/exists>()) }" ] .
899 ex:x a <http://ex/T> .
900 ex:y <http://ex/marker> "m" .
901 "#;
902 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
903
904 let lenient = validate_report(&loaded, &loaded.graph);
907 assert!(lenient.conforms, "results: {:?}", lenient.results);
908
909 let strict_opts = ValidationOptions {
912 engine: EngineOptions {
913 unsupported: UnsupportedPolicy::Error,
914 },
915 ..Default::default()
916 };
917 let strict = validate_report_with_options(&loaded, &loaded.graph, &strict_opts);
918 assert!(!strict.conforms);
919 assert_eq!(strict.results.len(), 1, "results: {:?}", strict.results);
920 }
921
922 #[test]
923 fn custom_component_ask_validator() {
924 let ttl = br#"
927 @prefix sh: <http://www.w3.org/ns/shacl#> .
928 @prefix ex: <http://ex/> .
929 ex:TestConstraintComponent a sh:ConstraintComponent ;
930 sh:parameter [ sh:path ex:test1 ] , [ sh:path ex:test2 ] ;
931 sh:validator [ a sh:SPARQLAskValidator ;
932 sh:ask "ASK { FILTER (?value = CONCAT($test1, $test2)) }" ] .
933 ex:TestShape a sh:NodeShape ;
934 ex:test1 "Hello " ;
935 ex:test2 "World" ;
936 sh:targetNode "Hallo Welt", "Hello World" .
937 "#;
938 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
939 let report = validate_report(&loaded, &loaded.graph);
940 assert!(!report.conforms);
941 assert_eq!(report.results.len(), 1);
942 let r = &report.results[0];
943 assert_eq!(r.component.as_str(), "http://ex/TestConstraintComponent");
944 assert_eq!(r.focus.to_string(), "\"Hallo Welt\"");
945 assert_eq!(
946 r.value.as_ref().map(ToString::to_string),
947 Some("\"Hallo Welt\"".to_string())
948 );
949 assert_eq!(r.source_shape.to_string(), "<http://ex/TestShape>");
950 assert_eq!(r.path, None);
951
952 let parse_out = shifty_parse::parse_loaded(&loaded);
955 let schema = shifty_opt::normalize(&parse_out.schema);
956 let plan = shifty_opt::plan(&schema);
957 let outcome = validate_plan_graphs(&loaded.graph, &loaded.graph, &plan).unwrap();
958 assert!(!outcome.conforms, "algebra: Hallo Welt must still violate");
959 assert_eq!(
960 outcome.violations.len(),
961 1,
962 "algebra: exactly one violation: {:?}",
963 outcome.violations
964 );
965 assert_eq!(
966 outcome.violations[0].focus.to_string(),
967 "\"Hallo Welt\"",
968 "algebra: wrong focus node"
969 );
970 }
971
972 #[test]
973 fn custom_component_select_node_validator() {
974 let ttl = br#"
977 @prefix sh: <http://www.w3.org/ns/shacl#> .
978 @prefix ex: <http://ex/> .
979 ex:C a sh:ConstraintComponent ;
980 sh:parameter [ sh:path ex:requiredParam ] ;
981 sh:nodeValidator [ a sh:SPARQLSelectValidator ;
982 sh:select """SELECT $this WHERE {
983 $this ?p ?o .
984 FILTER NOT EXISTS { $this <http://ex/property> $requiredParam }
985 }""" ] .
986 ex:S a sh:NodeShape ;
987 ex:requiredParam "Value" ;
988 sh:targetNode ex:Good, ex:Bad .
989 ex:Good <http://ex/property> "Value" .
990 ex:Bad <http://ex/property> "Other" .
991 "#;
992 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
993 let report = validate_report(&loaded, &loaded.graph);
994 assert!(!report.conforms);
995 assert_eq!(report.results.len(), 1);
996 let r = &report.results[0];
997 assert_eq!(r.component.as_str(), "http://ex/C");
998 assert_eq!(r.focus.to_string(), "<http://ex/Bad>");
999 assert_eq!(
1000 r.value.as_ref().map(ToString::to_string),
1001 Some("<http://ex/Bad>".to_string())
1002 );
1003 }
1004
1005 #[test]
1006 fn custom_component_not_activated_when_mandatory_param_absent() {
1007 let ttl = br#"
1010 @prefix sh: <http://www.w3.org/ns/shacl#> .
1011 @prefix ex: <http://ex/> .
1012 ex:C a sh:ConstraintComponent ;
1013 sh:parameter [ sh:path ex:requiredParam ] ;
1014 sh:validator [ a sh:SPARQLAskValidator ; sh:ask "ASK { FILTER (false) }" ] .
1015 ex:S a sh:NodeShape ;
1016 sh:targetNode ex:x .
1017 ex:x ex:other "z" .
1018 "#;
1019 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
1020 let report = validate_report(&loaded, &loaded.graph);
1021 assert!(report.conforms, "results: {:?}", report.results);
1022
1023 let parsed = parse_turtle(ttl, None).unwrap();
1024 assert!(
1025 !parsed
1026 .diagnostics
1027 .iter()
1028 .any(|d| d.message.contains("custom constraint component")),
1029 "an inactive component must not be diagnosed: {:?}",
1030 parsed.diagnostics
1031 );
1032 }
1033
1034 #[test]
1035 fn custom_component_property_validator_complex_path() {
1036 let ttl = br#"
1040 @prefix sh: <http://www.w3.org/ns/shacl#> .
1041 @prefix ex: <http://ex/> .
1042 ex:ForbidComponent a sh:ConstraintComponent ;
1043 sh:parameter [ sh:path ex:forbidden ] ;
1044 sh:propertyValidator [ a sh:SPARQLSelectValidator ;
1045 sh:select """SELECT $this ?value WHERE {
1046 $this $PATH ?value .
1047 FILTER (STR(?value) = STR($forbidden))
1048 }""" ] .
1049 ex:S a sh:NodeShape ;
1050 sh:targetNode ex:x ;
1051 sh:property [ sh:path ( ex:a ex:b ) ; ex:forbidden "bad" ] .
1052 ex:x ex:a ex:m .
1053 ex:m ex:b "bad", "ok" .
1054 "#;
1055 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
1056 let report = validate_report(&loaded, &loaded.graph);
1057 assert!(!report.conforms);
1058 assert_eq!(report.results.len(), 1, "results: {:?}", report.results);
1059 let r = &report.results[0];
1060 assert_eq!(r.component.as_str(), "http://ex/ForbidComponent");
1061 assert_eq!(r.focus.to_string(), "<http://ex/x>");
1062 assert_eq!(
1063 r.value.as_ref().map(ToString::to_string),
1064 Some("\"bad\"".to_string())
1065 );
1066 }
1067
1068 #[test]
1069 fn custom_component_property_validator_inverse_path() {
1070 let ttl = br#"
1074 @prefix sh: <http://www.w3.org/ns/shacl#> .
1075 @prefix ex: <http://ex/> .
1076 ex:OkComponent a sh:ConstraintComponent ;
1077 sh:parameter [ sh:path ex:want ] ;
1078 sh:validator [ a sh:SPARQLAskValidator ;
1079 sh:ask "ASK { $value <http://ex/label> $want }" ] .
1080 ex:S a sh:NodeShape ;
1081 sh:targetNode ex:p ;
1082 sh:property [ sh:path [ sh:inversePath ex:parent ] ; ex:want "ok" ] .
1083 ex:c1 ex:parent ex:p ; ex:label "ok" .
1084 ex:c2 ex:parent ex:p ; ex:label "no" .
1085 "#;
1086 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
1087 let report = validate_report(&loaded, &loaded.graph);
1088 assert!(!report.conforms);
1091 assert_eq!(report.results.len(), 1, "results: {:?}", report.results);
1092 assert_eq!(
1093 report.results[0].component.as_str(),
1094 "http://ex/OkComponent"
1095 );
1096 assert_eq!(
1097 report.results[0].value.as_ref().map(ToString::to_string),
1098 Some("<http://ex/c2>".to_string())
1099 );
1100 }
1101
1102 #[test]
1103 fn custom_component_ask_validator_subquery_count() {
1104 let shapes_ttl = br#"
1110 @prefix sh: <http://www.w3.org/ns/shacl#> .
1111 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1112 @prefix ex: <urn:ex/> .
1113
1114 ex:countComponent a sh:ConstraintComponent ;
1115 sh:parameter [ sh:path ex:exactCount ; sh:datatype xsd:integer ] ;
1116 sh:parameter [ sh:path ex:class ] ;
1117 sh:validator ex:hasExactCount .
1118
1119 ex:hasExactCount a sh:SPARQLAskValidator ;
1120 sh:message "Wrong count" ;
1121 sh:ask """
1122 ASK {
1123 {
1124 SELECT (COUNT(DISTINCT ?i) AS ?count)
1125 WHERE { ?i a $class . }
1126 }
1127 FILTER (?count = $exactCount)
1128 }
1129 """ .
1130
1131 ex:shape a sh:NodeShape ;
1132 sh:targetNode ex:sentinel ;
1133 ex:class ex:Thing ;
1134 ex:exactCount 1 .
1135 "#;
1136 let shapes = shifty_parse::load_turtle(shapes_ttl, None).unwrap();
1137
1138 let data_none =
1140 shifty_parse::load_turtle(b"@prefix ex: <urn:ex/> . ex:sentinel a ex:Sentinel .", None)
1141 .unwrap();
1142 let report = validate_report_graphs(&shapes, &data_none.graph);
1143 assert!(
1144 !report.conforms,
1145 "zero Things with exactCount=1 must not conform: {:?}",
1146 report.results
1147 );
1148
1149 let data_one = shifty_parse::load_turtle(
1151 b"@prefix ex: <urn:ex/> . ex:sentinel a ex:Sentinel . ex:t1 a ex:Thing .",
1152 None,
1153 )
1154 .unwrap();
1155 let report_ok = validate_report_graphs(&shapes, &data_one.graph);
1156 assert!(
1157 report_ok.conforms,
1158 "exactly one Thing with exactCount=1 must conform: {:?}",
1159 report_ok.results
1160 );
1161
1162 let data_two = shifty_parse::load_turtle(
1164 b"@prefix ex: <urn:ex/> . ex:sentinel a ex:Sentinel . ex:t1 a ex:Thing . ex:t2 a ex:Thing .",
1165 None,
1166 )
1167 .unwrap();
1168 let report_two = validate_report_graphs(&shapes, &data_two.graph);
1169 assert!(
1170 !report_two.conforms,
1171 "two Things with exactCount=1 must not conform: {:?}",
1172 report_two.results
1173 );
1174
1175 let parse_out = shifty_parse::parse_loaded(&shapes);
1177 let schema = shifty_opt::normalize(&parse_out.schema);
1178 let plan = shifty_opt::plan(&schema);
1179 let alg_none = validate_plan_graphs(&data_none.graph, &shapes.graph, &plan).unwrap();
1180 assert!(!alg_none.conforms, "algebra: zero Things must not conform");
1181 let alg_one = validate_plan_graphs(&data_one.graph, &shapes.graph, &plan).unwrap();
1182 assert!(alg_one.conforms, "algebra: one Thing must conform");
1183 let alg_two = validate_plan_graphs(&data_two.graph, &shapes.graph, &plan).unwrap();
1184 assert!(!alg_two.conforms, "algebra: two Things must not conform");
1185 }
1186
1187 #[test]
1188 fn custom_component_invalid_sparql_ignored_by_default() {
1189 let shapes_ttl = br#"
1195 @prefix sh: <http://www.w3.org/ns/shacl#> .
1196 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1197 @prefix ex: <urn:ex/> .
1198
1199 ex:badComponent a sh:ConstraintComponent ;
1200 sh:parameter [ sh:path ex:exactCount ; sh:datatype xsd:integer ] ;
1201 sh:parameter [ sh:path ex:class ] ;
1202 sh:validator [ a sh:SPARQLAskValidator ;
1203 sh:ask """
1204 ASK WHERE {
1205 {
1206 SELECT *
1207 WHERE { ?i a $class . }
1208 HAVING (COUNT(DISTINCT ?i) = $exactCount)
1209 }
1210 }
1211 """ ] .
1212
1213 ex:shape a sh:NodeShape ;
1214 sh:targetNode ex:sentinel ;
1215 ex:class ex:Thing ;
1216 ex:exactCount 1 .
1217 "#;
1218 let shapes = shifty_parse::load_turtle(shapes_ttl, None).unwrap();
1219 let data =
1220 shifty_parse::load_turtle(b"@prefix ex: <urn:ex/> . ex:sentinel a ex:Sentinel .", None)
1221 .unwrap();
1222
1223 let report = validate_report_graphs(&shapes, &data.graph);
1225 assert!(
1226 report.conforms,
1227 "bad validator query silently skipped under Ignore: {:?}",
1228 report.results
1229 );
1230 }
1231
1232 #[test]
1233 fn equals_on_node_shape_uses_the_focus_node_as_the_value() {
1234 let ttl = format!(
1235 "{PREFIXES}
1236 ex:S a sh:NodeShape ;
1237 sh:targetNode ex:valid, ex:extra, ex:missing ;
1238 sh:equals ex:p .
1239 ex:valid ex:p ex:valid .
1240 ex:extra ex:p ex:extra, ex:other .
1241 "
1242 );
1243 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1244 assert!(
1245 parsed.diagnostics.is_empty(),
1246 "diags: {:?}",
1247 parsed.diagnostics
1248 );
1249 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1250
1251 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
1252 assert!(!algebra.conforms);
1253 let mut foci: Vec<_> = algebra
1254 .violations
1255 .iter()
1256 .map(|violation| violation.focus.to_string())
1257 .collect();
1258 foci.sort();
1259 assert_eq!(
1260 foci,
1261 [
1262 "<http://ex/extra>".to_string(),
1263 "<http://ex/missing>".to_string()
1264 ]
1265 );
1266
1267 let normalized = shifty_opt::normalize(&parsed.schema);
1268 let plan = shifty_opt::plan(&normalized);
1269 let planned = validate_plan(&loaded.graph, &plan).unwrap();
1270 assert_eq!(planned.conforms, algebra.conforms);
1271 assert_eq!(planned.violations.len(), algebra.violations.len());
1272
1273 let report = validate_report(&loaded, &loaded.graph);
1274 assert!(!report.conforms);
1275 assert_eq!(report.results.len(), 2);
1276 assert!(report.results.iter().all(|result| result.component.as_str()
1277 == "http://www.w3.org/ns/shacl#EqualsConstraintComponent"));
1278 }
1279
1280 #[test]
1281 fn datatype_violation() {
1282 let ttl = format!(
1283 "{PREFIXES}
1284 ex:S a sh:NodeShape ;
1285 sh:targetNode ex:x ;
1286 sh:property [ sh:path ex:p ; sh:datatype xsd:integer ] .
1287 ex:x ex:p \"hello\" .
1288 "
1289 );
1290 assert!(!run(&ttl).conforms);
1291 }
1292
1293 #[test]
1294 fn nodekind_and_class_target() {
1295 let ttl = format!(
1296 "{PREFIXES}
1297 ex:S a sh:NodeShape ;
1298 sh:targetClass ex:Person ;
1299 sh:property [ sh:path ex:knows ; sh:nodeKind sh:IRI ] .
1300 ex:alice a ex:Person ; ex:knows ex:bob .
1301 ex:carol a ex:Person ; ex:knows \"notaniri\" .
1302 "
1303 );
1304 let outcome = run(&ttl);
1305 assert!(!outcome.conforms);
1306 let bad: Vec<_> = outcome
1307 .violations
1308 .iter()
1309 .map(|r| r.focus.to_string())
1310 .collect();
1311 assert_eq!(bad, vec!["<http://ex/carol>".to_string()]);
1312 }
1313
1314 #[test]
1315 fn recursion_over_cyclic_data_terminates() {
1316 let ttl = format!(
1318 "{PREFIXES}
1319 ex:S a sh:NodeShape ;
1320 sh:targetNode ex:a ;
1321 sh:property [ sh:path ex:knows ; sh:node ex:S ; sh:nodeKind sh:IRI ] .
1322 ex:a ex:knows ex:b .
1323 ex:b ex:knows ex:a .
1324 "
1325 );
1326 assert!(run(&ttl).conforms);
1329 }
1330
1331 #[test]
1332 fn empty_graph_conforms() {
1333 let outcome = validate(&Graph::new(), &shifty_algebra::Schema::new()).unwrap();
1334 assert!(outcome.conforms);
1335 }
1336
1337 #[test]
1338 fn non_stratifiable_schema_is_diagnosed() {
1339 let ttl = format!(
1341 "{PREFIXES}
1342 ex:S a sh:NodeShape ;
1343 sh:targetNode ex:x ;
1344 sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ; sh:qualifiedMinCount 1 ] .
1345 ex:x ex:p ex:y .
1346 "
1347 );
1348 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1349 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1350 assert!(validate(&loaded.graph, &out.schema).is_err());
1351 }
1352
1353 fn triple(s: &str, p: &str, o: &str) -> oxrdf::Triple {
1354 use oxrdf::NamedNode;
1355 oxrdf::Triple::new(
1356 NamedNode::new(s).unwrap(),
1357 NamedNode::new(p).unwrap(),
1358 NamedNode::new(o).unwrap(),
1359 )
1360 }
1361
1362 #[test]
1363 fn triple_rule_infers_from_path() {
1364 let ttl = format!(
1366 "{PREFIXES}
1367 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
1368 sh:rule [ a sh:TripleRule ;
1369 sh:subject sh:this ; sh:predicate ex:knows2 ;
1370 sh:object [ sh:path ex:knows ] ] .
1371 ex:a a ex:Person ; ex:knows ex:b .
1372 "
1373 );
1374 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1375 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1376 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1377 assert_eq!(outcome.inferred.len(), 1);
1378 assert!(
1379 outcome
1380 .graph
1381 .contains(&triple("http://ex/a", "http://ex/knows2", "http://ex/b"))
1382 );
1383 }
1384
1385 #[test]
1386 fn inference_reaches_a_fixpoint() {
1387 let ttl = format!(
1390 "{PREFIXES}
1391 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
1392 sh:rule [ a sh:TripleRule ;
1393 sh:subject sh:this ; sh:predicate ex:reaches ;
1394 sh:object [ sh:path [ sh:alternativePath ( ex:knows ( ex:knows ex:reaches ) ) ] ] ] .
1395 ex:a a ex:Person ; ex:knows ex:b .
1396 ex:b a ex:Person ; ex:knows ex:c .
1397 ex:c a ex:Person .
1398 "
1399 );
1400 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1401 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1402 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1403 assert!(
1404 outcome
1405 .graph
1406 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/b"))
1407 );
1408 assert!(
1409 outcome
1410 .graph
1411 .contains(&triple("http://ex/b", "http://ex/reaches", "http://ex/c"))
1412 );
1413 assert!(
1415 outcome
1416 .graph
1417 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/c"))
1418 );
1419 }
1420
1421 #[test]
1422 fn later_order_output_reactivates_an_earlier_rule() {
1423 let ttl = format!(
1424 "{PREFIXES}
1425 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1426 sh:rule [
1427 a sh:TripleRule ; sh:order 0 ;
1428 sh:subject sh:this ; sh:predicate ex:done ;
1429 sh:object [ sh:path ex:ready ]
1430 ] ;
1431 sh:rule [
1432 a sh:TripleRule ; sh:order 1 ;
1433 sh:subject sh:this ; sh:predicate ex:ready ;
1434 sh:object ex:y
1435 ] .
1436 "
1437 );
1438 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1439 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1440 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1441
1442 assert!(
1443 outcome
1444 .graph
1445 .contains(&triple("http://ex/x", "http://ex/ready", "http://ex/y"))
1446 );
1447 assert!(
1448 outcome
1449 .graph
1450 .contains(&triple("http://ex/x", "http://ex/done", "http://ex/y"))
1451 );
1452 }
1453
1454 #[test]
1455 fn inferred_triples_can_create_new_rule_targets() {
1456 let ttl = format!(
1457 "{PREFIXES}
1458 ex:Seed a sh:NodeShape ; sh:targetNode ex:x ;
1459 sh:rule [
1460 a sh:TripleRule ;
1461 sh:subject sh:this ; sh:predicate ex:eligible ;
1462 sh:object ex:y
1463 ] .
1464 ex:Eligible a sh:NodeShape ; sh:targetSubjectsOf ex:eligible ;
1465 sh:rule [
1466 a sh:TripleRule ;
1467 sh:subject sh:this ; sh:predicate ex:classified ;
1468 sh:object ex:yes
1469 ] .
1470 "
1471 );
1472 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1473 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1474 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1475
1476 assert!(outcome.graph.contains(&triple(
1477 "http://ex/x",
1478 "http://ex/classified",
1479 "http://ex/yes",
1480 )));
1481 }
1482
1483 #[test]
1484 fn split_inference_uses_shapes_graph_as_rule_context() {
1485 let shapes_ttl = format!(
1486 "{PREFIXES}
1487 ex:InverseShape a sh:NodeShape ;
1488 sh:targetClass ex:Thing ;
1489 sh:rule [
1490 a sh:SPARQLRule ;
1491 sh:construct \"\"\"
1492 CONSTRUCT {{ ?o ?inverse $this }}
1493 WHERE {{
1494 $this ?predicate ?o .
1495 ?predicate ex:inverseOf ?inverse .
1496 }}
1497 \"\"\"
1498 ] .
1499 ex:p ex:inverseOf ex:q .
1500 "
1501 );
1502 let data_ttl = format!(
1503 "{PREFIXES}
1504 ex:a a ex:Thing ; ex:p ex:b .
1505 "
1506 );
1507 let shapes = shifty_parse::load_turtle(shapes_ttl.as_bytes(), None).unwrap();
1508 let parsed = shifty_parse::parse_loaded(&shapes);
1509 let data = shifty_parse::load_turtle(data_ttl.as_bytes(), None).unwrap();
1510
1511 let outcome = infer_graphs(&data.graph, &shapes.graph, &parsed.schema).unwrap();
1512
1513 assert!(
1514 outcome
1515 .graph
1516 .contains(&triple("http://ex/b", "http://ex/q", "http://ex/a"))
1517 );
1518 assert_eq!(outcome.inferred.len(), 1);
1519 }
1520}