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