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