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