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