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 parsed = parse_turtle(ttl, None).unwrap();
763 assert!(
764 parsed
765 .diagnostics
766 .iter()
767 .any(|d| d.message.contains("custom constraint component")),
768 "expected a custom-component diagnostic, got: {:?}",
769 parsed.diagnostics
770 );
771 }
772
773 #[test]
774 fn custom_component_select_node_validator() {
775 let ttl = br#"
778 @prefix sh: <http://www.w3.org/ns/shacl#> .
779 @prefix ex: <http://ex/> .
780 ex:C a sh:ConstraintComponent ;
781 sh:parameter [ sh:path ex:requiredParam ] ;
782 sh:nodeValidator [ a sh:SPARQLSelectValidator ;
783 sh:select """SELECT $this WHERE {
784 $this ?p ?o .
785 FILTER NOT EXISTS { $this <http://ex/property> $requiredParam }
786 }""" ] .
787 ex:S a sh:NodeShape ;
788 ex:requiredParam "Value" ;
789 sh:targetNode ex:Good, ex:Bad .
790 ex:Good <http://ex/property> "Value" .
791 ex:Bad <http://ex/property> "Other" .
792 "#;
793 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
794 let report = validate_report(&loaded, &loaded.graph);
795 assert!(!report.conforms);
796 assert_eq!(report.results.len(), 1);
797 let r = &report.results[0];
798 assert_eq!(r.component.as_str(), "http://ex/C");
799 assert_eq!(r.focus.to_string(), "<http://ex/Bad>");
800 assert_eq!(
801 r.value.as_ref().map(ToString::to_string),
802 Some("<http://ex/Bad>".to_string())
803 );
804 }
805
806 #[test]
807 fn custom_component_not_activated_when_mandatory_param_absent() {
808 let ttl = br#"
811 @prefix sh: <http://www.w3.org/ns/shacl#> .
812 @prefix ex: <http://ex/> .
813 ex:C a sh:ConstraintComponent ;
814 sh:parameter [ sh:path ex:requiredParam ] ;
815 sh:validator [ a sh:SPARQLAskValidator ; sh:ask "ASK { FILTER (false) }" ] .
816 ex:S a sh:NodeShape ;
817 sh:targetNode ex:x .
818 ex:x ex:other "z" .
819 "#;
820 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
821 let report = validate_report(&loaded, &loaded.graph);
822 assert!(report.conforms, "results: {:?}", report.results);
823
824 let parsed = parse_turtle(ttl, None).unwrap();
825 assert!(
826 !parsed
827 .diagnostics
828 .iter()
829 .any(|d| d.message.contains("custom constraint component")),
830 "an inactive component must not be diagnosed: {:?}",
831 parsed.diagnostics
832 );
833 }
834
835 #[test]
836 fn custom_component_property_validator_complex_path() {
837 let ttl = br#"
841 @prefix sh: <http://www.w3.org/ns/shacl#> .
842 @prefix ex: <http://ex/> .
843 ex:ForbidComponent a sh:ConstraintComponent ;
844 sh:parameter [ sh:path ex:forbidden ] ;
845 sh:propertyValidator [ a sh:SPARQLSelectValidator ;
846 sh:select """SELECT $this ?value WHERE {
847 $this $PATH ?value .
848 FILTER (STR(?value) = STR($forbidden))
849 }""" ] .
850 ex:S a sh:NodeShape ;
851 sh:targetNode ex:x ;
852 sh:property [ sh:path ( ex:a ex:b ) ; ex:forbidden "bad" ] .
853 ex:x ex:a ex:m .
854 ex:m ex:b "bad", "ok" .
855 "#;
856 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
857 let report = validate_report(&loaded, &loaded.graph);
858 assert!(!report.conforms);
859 assert_eq!(report.results.len(), 1, "results: {:?}", report.results);
860 let r = &report.results[0];
861 assert_eq!(r.component.as_str(), "http://ex/ForbidComponent");
862 assert_eq!(r.focus.to_string(), "<http://ex/x>");
863 assert_eq!(
864 r.value.as_ref().map(ToString::to_string),
865 Some("\"bad\"".to_string())
866 );
867 }
868
869 #[test]
870 fn custom_component_property_validator_inverse_path() {
871 let ttl = br#"
875 @prefix sh: <http://www.w3.org/ns/shacl#> .
876 @prefix ex: <http://ex/> .
877 ex:OkComponent a sh:ConstraintComponent ;
878 sh:parameter [ sh:path ex:want ] ;
879 sh:validator [ a sh:SPARQLAskValidator ;
880 sh:ask "ASK { $value <http://ex/label> $want }" ] .
881 ex:S a sh:NodeShape ;
882 sh:targetNode ex:p ;
883 sh:property [ sh:path [ sh:inversePath ex:parent ] ; ex:want "ok" ] .
884 ex:c1 ex:parent ex:p ; ex:label "ok" .
885 ex:c2 ex:parent ex:p ; ex:label "no" .
886 "#;
887 let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
888 let report = validate_report(&loaded, &loaded.graph);
889 assert!(!report.conforms);
892 assert_eq!(report.results.len(), 1, "results: {:?}", report.results);
893 assert_eq!(
894 report.results[0].component.as_str(),
895 "http://ex/OkComponent"
896 );
897 assert_eq!(
898 report.results[0].value.as_ref().map(ToString::to_string),
899 Some("<http://ex/c2>".to_string())
900 );
901 }
902
903 #[test]
904 fn equals_on_node_shape_uses_the_focus_node_as_the_value() {
905 let ttl = format!(
906 "{PREFIXES}
907 ex:S a sh:NodeShape ;
908 sh:targetNode ex:valid, ex:extra, ex:missing ;
909 sh:equals ex:p .
910 ex:valid ex:p ex:valid .
911 ex:extra ex:p ex:extra, ex:other .
912 "
913 );
914 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
915 assert!(
916 parsed.diagnostics.is_empty(),
917 "diags: {:?}",
918 parsed.diagnostics
919 );
920 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
921
922 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
923 assert!(!algebra.conforms);
924 let mut foci: Vec<_> = algebra
925 .violations
926 .iter()
927 .map(|violation| violation.focus.to_string())
928 .collect();
929 foci.sort();
930 assert_eq!(
931 foci,
932 [
933 "<http://ex/extra>".to_string(),
934 "<http://ex/missing>".to_string()
935 ]
936 );
937
938 let normalized = shifty_opt::normalize(&parsed.schema);
939 let plan = shifty_opt::plan(&normalized);
940 let planned = validate_plan(&loaded.graph, &plan).unwrap();
941 assert_eq!(planned.conforms, algebra.conforms);
942 assert_eq!(planned.violations.len(), algebra.violations.len());
943
944 let report = validate_report(&loaded, &loaded.graph);
945 assert!(!report.conforms);
946 assert_eq!(report.results.len(), 2);
947 assert!(report.results.iter().all(|result| result.component.as_str()
948 == "http://www.w3.org/ns/shacl#EqualsConstraintComponent"));
949 }
950
951 #[test]
952 fn datatype_violation() {
953 let ttl = format!(
954 "{PREFIXES}
955 ex:S a sh:NodeShape ;
956 sh:targetNode ex:x ;
957 sh:property [ sh:path ex:p ; sh:datatype xsd:integer ] .
958 ex:x ex:p \"hello\" .
959 "
960 );
961 assert!(!run(&ttl).conforms);
962 }
963
964 #[test]
965 fn nodekind_and_class_target() {
966 let ttl = format!(
967 "{PREFIXES}
968 ex:S a sh:NodeShape ;
969 sh:targetClass ex:Person ;
970 sh:property [ sh:path ex:knows ; sh:nodeKind sh:IRI ] .
971 ex:alice a ex:Person ; ex:knows ex:bob .
972 ex:carol a ex:Person ; ex:knows \"notaniri\" .
973 "
974 );
975 let outcome = run(&ttl);
976 assert!(!outcome.conforms);
977 let bad: Vec<_> = outcome
978 .violations
979 .iter()
980 .map(|r| r.focus.to_string())
981 .collect();
982 assert_eq!(bad, vec!["<http://ex/carol>".to_string()]);
983 }
984
985 #[test]
986 fn recursion_over_cyclic_data_terminates() {
987 let ttl = format!(
989 "{PREFIXES}
990 ex:S a sh:NodeShape ;
991 sh:targetNode ex:a ;
992 sh:property [ sh:path ex:knows ; sh:node ex:S ; sh:nodeKind sh:IRI ] .
993 ex:a ex:knows ex:b .
994 ex:b ex:knows ex:a .
995 "
996 );
997 assert!(run(&ttl).conforms);
1000 }
1001
1002 #[test]
1003 fn empty_graph_conforms() {
1004 let outcome = validate(&Graph::new(), &shifty_algebra::Schema::new()).unwrap();
1005 assert!(outcome.conforms);
1006 }
1007
1008 #[test]
1009 fn non_stratifiable_schema_is_diagnosed() {
1010 let ttl = format!(
1012 "{PREFIXES}
1013 ex:S a sh:NodeShape ;
1014 sh:targetNode ex:x ;
1015 sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ; sh:qualifiedMinCount 1 ] .
1016 ex:x ex:p ex:y .
1017 "
1018 );
1019 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1020 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1021 assert!(validate(&loaded.graph, &out.schema).is_err());
1022 }
1023
1024 fn triple(s: &str, p: &str, o: &str) -> oxrdf::Triple {
1025 use oxrdf::NamedNode;
1026 oxrdf::Triple::new(
1027 NamedNode::new(s).unwrap(),
1028 NamedNode::new(p).unwrap(),
1029 NamedNode::new(o).unwrap(),
1030 )
1031 }
1032
1033 #[test]
1034 fn triple_rule_infers_from_path() {
1035 let ttl = format!(
1037 "{PREFIXES}
1038 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
1039 sh:rule [ a sh:TripleRule ;
1040 sh:subject sh:this ; sh:predicate ex:knows2 ;
1041 sh:object [ sh:path ex:knows ] ] .
1042 ex:a a ex:Person ; ex:knows ex:b .
1043 "
1044 );
1045 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1046 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1047 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1048 assert_eq!(outcome.inferred.len(), 1);
1049 assert!(
1050 outcome
1051 .graph
1052 .contains(&triple("http://ex/a", "http://ex/knows2", "http://ex/b"))
1053 );
1054 }
1055
1056 #[test]
1057 fn inference_reaches_a_fixpoint() {
1058 let ttl = format!(
1061 "{PREFIXES}
1062 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
1063 sh:rule [ a sh:TripleRule ;
1064 sh:subject sh:this ; sh:predicate ex:reaches ;
1065 sh:object [ sh:path [ sh:alternativePath ( ex:knows ( ex:knows ex:reaches ) ) ] ] ] .
1066 ex:a a ex:Person ; ex:knows ex:b .
1067 ex:b a ex:Person ; ex:knows ex:c .
1068 ex:c a ex:Person .
1069 "
1070 );
1071 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1072 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1073 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1074 assert!(
1075 outcome
1076 .graph
1077 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/b"))
1078 );
1079 assert!(
1080 outcome
1081 .graph
1082 .contains(&triple("http://ex/b", "http://ex/reaches", "http://ex/c"))
1083 );
1084 assert!(
1086 outcome
1087 .graph
1088 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/c"))
1089 );
1090 }
1091
1092 #[test]
1093 fn later_order_output_reactivates_an_earlier_rule() {
1094 let ttl = format!(
1095 "{PREFIXES}
1096 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1097 sh:rule [
1098 a sh:TripleRule ; sh:order 0 ;
1099 sh:subject sh:this ; sh:predicate ex:done ;
1100 sh:object [ sh:path ex:ready ]
1101 ] ;
1102 sh:rule [
1103 a sh:TripleRule ; sh:order 1 ;
1104 sh:subject sh:this ; sh:predicate ex:ready ;
1105 sh:object ex:y
1106 ] .
1107 "
1108 );
1109 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1110 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1111 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1112
1113 assert!(
1114 outcome
1115 .graph
1116 .contains(&triple("http://ex/x", "http://ex/ready", "http://ex/y"))
1117 );
1118 assert!(
1119 outcome
1120 .graph
1121 .contains(&triple("http://ex/x", "http://ex/done", "http://ex/y"))
1122 );
1123 }
1124
1125 #[test]
1126 fn inferred_triples_can_create_new_rule_targets() {
1127 let ttl = format!(
1128 "{PREFIXES}
1129 ex:Seed a sh:NodeShape ; sh:targetNode ex:x ;
1130 sh:rule [
1131 a sh:TripleRule ;
1132 sh:subject sh:this ; sh:predicate ex:eligible ;
1133 sh:object ex:y
1134 ] .
1135 ex:Eligible a sh:NodeShape ; sh:targetSubjectsOf ex:eligible ;
1136 sh:rule [
1137 a sh:TripleRule ;
1138 sh:subject sh:this ; sh:predicate ex:classified ;
1139 sh:object ex:yes
1140 ] .
1141 "
1142 );
1143 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
1144 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
1145 let outcome = infer(&loaded.graph, &out.schema).unwrap();
1146
1147 assert!(outcome.graph.contains(&triple(
1148 "http://ex/x",
1149 "http://ex/classified",
1150 "http://ex/yes",
1151 )));
1152 }
1153
1154 #[test]
1155 fn split_inference_uses_shapes_graph_as_rule_context() {
1156 let shapes_ttl = format!(
1157 "{PREFIXES}
1158 ex:InverseShape a sh:NodeShape ;
1159 sh:targetClass ex:Thing ;
1160 sh:rule [
1161 a sh:SPARQLRule ;
1162 sh:construct \"\"\"
1163 CONSTRUCT {{ ?o ?inverse $this }}
1164 WHERE {{
1165 $this ?predicate ?o .
1166 ?predicate ex:inverseOf ?inverse .
1167 }}
1168 \"\"\"
1169 ] .
1170 ex:p ex:inverseOf ex:q .
1171 "
1172 );
1173 let data_ttl = format!(
1174 "{PREFIXES}
1175 ex:a a ex:Thing ; ex:p ex:b .
1176 "
1177 );
1178 let shapes = shifty_parse::load_turtle(shapes_ttl.as_bytes(), None).unwrap();
1179 let parsed = shifty_parse::parse_loaded(&shapes);
1180 let data = shifty_parse::load_turtle(data_ttl.as_bytes(), None).unwrap();
1181
1182 let outcome = infer_graphs(&data.graph, &shapes.graph, &parsed.schema).unwrap();
1183
1184 assert!(
1185 outcome
1186 .graph
1187 .contains(&triple("http://ex/b", "http://ex/q", "http://ex/a"))
1188 );
1189 assert_eq!(outcome.inferred.len(), 1);
1190 }
1191}