1pub mod frozen;
10pub mod infer;
11mod native_exec;
12pub mod path;
13mod path_plan;
14pub mod profile;
15pub mod report;
16mod sparql;
17pub mod validate;
18pub mod value;
19
20pub use infer::{InferenceOutcome, infer, infer_graphs, infer_with_context};
21pub use report::{
22 ValidationReport, ValidationResult, report_to_graph, validate_report, validate_report_graphs,
23 validate_report_graphs_with_mode,
24};
25pub use validate::{
26 NonStratifiable, Reason, ValidationGraphMode, ValidationOutcome, Violation, focus_nodes,
27 validate, validate_graphs, validate_graphs_with_mode, validate_plan, validate_plan_graphs,
28 validate_plan_graphs_with_mode, validate_plan_with_context, validate_with_context,
29};
30
31#[cfg(test)]
32mod tests {
33 use super::*;
34 use oxrdf::Graph;
35 use shifty_parse::parse_turtle;
36
37 fn run(shapes_and_data: &str) -> ValidationOutcome {
38 let out = parse_turtle(shapes_and_data.as_bytes(), None).unwrap();
39 let loaded = shifty_parse::load_turtle(shapes_and_data.as_bytes(), None).unwrap();
41 validate(&loaded.graph, &out.schema).expect("stratifiable schema")
42 }
43
44 const PREFIXES: &str = r#"
45 @prefix sh: <http://www.w3.org/ns/shacl#> .
46 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
47 @prefix ex: <http://ex/> .
48 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
49 "#;
50
51 #[test]
52 fn reports_specific_failing_constraints() {
53 let ttl = format!(
54 "{PREFIXES}
55 ex:S a sh:NodeShape ;
56 sh:targetNode ex:x ;
57 sh:closed true ;
58 sh:ignoredProperties ( rdf:type ) ;
59 sh:property [ sh:path ex:age ; sh:datatype xsd:integer ; sh:maxCount 1 ] .
60 ex:x ex:age \"foo\" , 5 ; ex:extra 1 .
61 "
62 );
63 let outcome = run(&ttl);
64 assert!(!outcome.conforms);
65 assert_eq!(outcome.violations.len(), 1);
66 let msgs: Vec<&str> = outcome.violations[0]
67 .reasons
68 .iter()
69 .map(|r| r.message.as_str())
70 .collect();
71 assert!(
73 msgs.iter().any(|m| m.contains("datatype(xsd:integer)")),
74 "missing datatype reason: {msgs:?}"
75 );
76 assert!(
77 msgs.iter().any(|m| m.contains("at most 1")),
78 "missing maxCount reason: {msgs:?}"
79 );
80 assert!(
81 msgs.iter()
82 .any(|m| m.contains("closed") && m.contains("extra")),
83 "missing closed reason: {msgs:?}"
84 );
85 }
86
87 #[test]
88 fn cardinality_and_datatype() {
89 let ttl = format!(
90 "{PREFIXES}
91 ex:S a sh:NodeShape ;
92 sh:targetNode ex:alice, ex:bob ;
93 sh:property [ sh:path ex:age ; sh:maxCount 1 ; sh:datatype xsd:integer ] .
94 ex:alice ex:age 30 .
95 ex:bob ex:age 30 ; ex:age 40 .
96 "
97 );
98 let outcome = run(&ttl);
99 assert!(!outcome.conforms);
100 let bad: Vec<_> = outcome
102 .violations
103 .iter()
104 .map(|r| r.focus.to_string())
105 .collect();
106 assert_eq!(bad, vec!["<http://ex/bob>".to_string()]);
107 }
108
109 #[test]
110 fn qualified_value_shape_disjoint_uses_all_sibling_property_shapes() {
111 let ttl = format!(
112 "{PREFIXES}
113 ex:S a sh:NodeShape ;
114 sh:targetNode ex:x ;
115 sh:property ex:A, ex:B .
116 ex:A a sh:PropertyShape ;
117 sh:path ex:p ;
118 sh:qualifiedValueShape [ sh:class ex:TypeA ] ;
119 sh:qualifiedValueShapesDisjoint true ;
120 sh:qualifiedMinCount 1 .
121 ex:B a sh:PropertyShape ;
122 sh:path ex:q ;
123 sh:qualifiedValueShape [ sh:class ex:TypeB ] ;
124 sh:qualifiedValueShapesDisjoint true ;
125 sh:qualifiedMaxCount 10 .
126 ex:x ex:p ex:value .
127 ex:value a ex:TypeA, ex:TypeB .
128 "
129 );
130 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
131 assert!(
132 parsed.diagnostics.is_empty(),
133 "diags: {:?}",
134 parsed.diagnostics
135 );
136 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
137
138 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
139 assert!(!algebra.conforms);
140
141 let report = validate_report(&loaded, &loaded.graph);
142 assert!(!report.conforms);
143 assert_eq!(report.results.len(), 1);
144 assert_eq!(
145 report.results[0].component.as_str(),
146 "http://www.w3.org/ns/shacl#QualifiedMinCountConstraintComponent"
147 );
148 }
149
150 #[test]
151 fn disjoint_on_node_shape_uses_the_focus_node_as_the_value() {
152 let ttl = format!(
153 "{PREFIXES}
154 ex:S a sh:NodeShape ;
155 sh:targetNode ex:valid, ex:invalid ;
156 sh:disjoint ex:p .
157 ex:valid ex:p ex:other .
158 ex:invalid ex:p ex:invalid .
159 "
160 );
161 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
162 assert!(
163 parsed.diagnostics.is_empty(),
164 "diags: {:?}",
165 parsed.diagnostics
166 );
167 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
168
169 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
170 assert!(!algebra.conforms);
171 assert_eq!(algebra.violations.len(), 1);
172 assert_eq!(
173 algebra.violations[0].focus.to_string(),
174 "<http://ex/invalid>"
175 );
176
177 let normalized = shifty_opt::normalize(&parsed.schema);
178 let plan = shifty_opt::plan(&normalized);
179 let planned = validate_plan(&loaded.graph, &plan).unwrap();
180 assert_eq!(planned.conforms, algebra.conforms);
181 assert_eq!(planned.violations.len(), algebra.violations.len());
182
183 let report = validate_report(&loaded, &loaded.graph);
184 assert!(!report.conforms);
185 assert_eq!(report.results.len(), 1);
186 assert_eq!(
187 report.results[0].component.as_str(),
188 "http://www.w3.org/ns/shacl#DisjointConstraintComponent"
189 );
190 assert_eq!(
191 report.results[0].value.as_ref().map(ToString::to_string),
192 Some("<http://ex/invalid>".to_string())
193 );
194 }
195
196 #[test]
197 fn equals_on_node_shape_uses_the_focus_node_as_the_value() {
198 let ttl = format!(
199 "{PREFIXES}
200 ex:S a sh:NodeShape ;
201 sh:targetNode ex:valid, ex:extra, ex:missing ;
202 sh:equals ex:p .
203 ex:valid ex:p ex:valid .
204 ex:extra ex:p ex:extra, ex:other .
205 "
206 );
207 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
208 assert!(
209 parsed.diagnostics.is_empty(),
210 "diags: {:?}",
211 parsed.diagnostics
212 );
213 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
214
215 let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
216 assert!(!algebra.conforms);
217 let mut foci: Vec<_> = algebra
218 .violations
219 .iter()
220 .map(|violation| violation.focus.to_string())
221 .collect();
222 foci.sort();
223 assert_eq!(
224 foci,
225 [
226 "<http://ex/extra>".to_string(),
227 "<http://ex/missing>".to_string()
228 ]
229 );
230
231 let normalized = shifty_opt::normalize(&parsed.schema);
232 let plan = shifty_opt::plan(&normalized);
233 let planned = validate_plan(&loaded.graph, &plan).unwrap();
234 assert_eq!(planned.conforms, algebra.conforms);
235 assert_eq!(planned.violations.len(), algebra.violations.len());
236
237 let report = validate_report(&loaded, &loaded.graph);
238 assert!(!report.conforms);
239 assert_eq!(report.results.len(), 2);
240 assert!(report.results.iter().all(|result| result.component.as_str()
241 == "http://www.w3.org/ns/shacl#EqualsConstraintComponent"));
242 }
243
244 #[test]
245 fn datatype_violation() {
246 let ttl = format!(
247 "{PREFIXES}
248 ex:S a sh:NodeShape ;
249 sh:targetNode ex:x ;
250 sh:property [ sh:path ex:p ; sh:datatype xsd:integer ] .
251 ex:x ex:p \"hello\" .
252 "
253 );
254 assert!(!run(&ttl).conforms);
255 }
256
257 #[test]
258 fn nodekind_and_class_target() {
259 let ttl = format!(
260 "{PREFIXES}
261 ex:S a sh:NodeShape ;
262 sh:targetClass ex:Person ;
263 sh:property [ sh:path ex:knows ; sh:nodeKind sh:IRI ] .
264 ex:alice a ex:Person ; ex:knows ex:bob .
265 ex:carol a ex:Person ; ex:knows \"notaniri\" .
266 "
267 );
268 let outcome = run(&ttl);
269 assert!(!outcome.conforms);
270 let bad: Vec<_> = outcome
271 .violations
272 .iter()
273 .map(|r| r.focus.to_string())
274 .collect();
275 assert_eq!(bad, vec!["<http://ex/carol>".to_string()]);
276 }
277
278 #[test]
279 fn recursion_over_cyclic_data_terminates() {
280 let ttl = format!(
282 "{PREFIXES}
283 ex:S a sh:NodeShape ;
284 sh:targetNode ex:a ;
285 sh:property [ sh:path ex:knows ; sh:node ex:S ; sh:nodeKind sh:IRI ] .
286 ex:a ex:knows ex:b .
287 ex:b ex:knows ex:a .
288 "
289 );
290 assert!(run(&ttl).conforms);
293 }
294
295 #[test]
296 fn empty_graph_conforms() {
297 let outcome = validate(&Graph::new(), &shifty_algebra::Schema::new()).unwrap();
298 assert!(outcome.conforms);
299 }
300
301 #[test]
302 fn non_stratifiable_schema_is_diagnosed() {
303 let ttl = format!(
305 "{PREFIXES}
306 ex:S a sh:NodeShape ;
307 sh:targetNode ex:x ;
308 sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ; sh:qualifiedMinCount 1 ] .
309 ex:x ex:p ex:y .
310 "
311 );
312 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
313 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
314 assert!(validate(&loaded.graph, &out.schema).is_err());
315 }
316
317 fn triple(s: &str, p: &str, o: &str) -> oxrdf::Triple {
318 use oxrdf::NamedNode;
319 oxrdf::Triple::new(
320 NamedNode::new(s).unwrap(),
321 NamedNode::new(p).unwrap(),
322 NamedNode::new(o).unwrap(),
323 )
324 }
325
326 #[test]
327 fn triple_rule_infers_from_path() {
328 let ttl = format!(
330 "{PREFIXES}
331 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
332 sh:rule [ a sh:TripleRule ;
333 sh:subject sh:this ; sh:predicate ex:knows2 ;
334 sh:object [ sh:path ex:knows ] ] .
335 ex:a a ex:Person ; ex:knows ex:b .
336 "
337 );
338 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
339 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
340 let outcome = infer(&loaded.graph, &out.schema).unwrap();
341 assert_eq!(outcome.inferred.len(), 1);
342 assert!(
343 outcome
344 .graph
345 .contains(&triple("http://ex/a", "http://ex/knows2", "http://ex/b"))
346 );
347 }
348
349 #[test]
350 fn inference_reaches_a_fixpoint() {
351 let ttl = format!(
354 "{PREFIXES}
355 ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
356 sh:rule [ a sh:TripleRule ;
357 sh:subject sh:this ; sh:predicate ex:reaches ;
358 sh:object [ sh:path [ sh:alternativePath ( ex:knows ( ex:knows ex:reaches ) ) ] ] ] .
359 ex:a a ex:Person ; ex:knows ex:b .
360 ex:b a ex:Person ; ex:knows ex:c .
361 ex:c a ex:Person .
362 "
363 );
364 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
365 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
366 let outcome = infer(&loaded.graph, &out.schema).unwrap();
367 assert!(
368 outcome
369 .graph
370 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/b"))
371 );
372 assert!(
373 outcome
374 .graph
375 .contains(&triple("http://ex/b", "http://ex/reaches", "http://ex/c"))
376 );
377 assert!(
379 outcome
380 .graph
381 .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/c"))
382 );
383 }
384
385 #[test]
386 fn later_order_output_reactivates_an_earlier_rule() {
387 let ttl = format!(
388 "{PREFIXES}
389 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
390 sh:rule [
391 a sh:TripleRule ; sh:order 0 ;
392 sh:subject sh:this ; sh:predicate ex:done ;
393 sh:object [ sh:path ex:ready ]
394 ] ;
395 sh:rule [
396 a sh:TripleRule ; sh:order 1 ;
397 sh:subject sh:this ; sh:predicate ex:ready ;
398 sh:object ex:y
399 ] .
400 "
401 );
402 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
403 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
404 let outcome = infer(&loaded.graph, &out.schema).unwrap();
405
406 assert!(
407 outcome
408 .graph
409 .contains(&triple("http://ex/x", "http://ex/ready", "http://ex/y"))
410 );
411 assert!(
412 outcome
413 .graph
414 .contains(&triple("http://ex/x", "http://ex/done", "http://ex/y"))
415 );
416 }
417
418 #[test]
419 fn inferred_triples_can_create_new_rule_targets() {
420 let ttl = format!(
421 "{PREFIXES}
422 ex:Seed a sh:NodeShape ; sh:targetNode ex:x ;
423 sh:rule [
424 a sh:TripleRule ;
425 sh:subject sh:this ; sh:predicate ex:eligible ;
426 sh:object ex:y
427 ] .
428 ex:Eligible a sh:NodeShape ; sh:targetSubjectsOf ex:eligible ;
429 sh:rule [
430 a sh:TripleRule ;
431 sh:subject sh:this ; sh:predicate ex:classified ;
432 sh:object ex:yes
433 ] .
434 "
435 );
436 let out = parse_turtle(ttl.as_bytes(), None).unwrap();
437 let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
438 let outcome = infer(&loaded.graph, &out.schema).unwrap();
439
440 assert!(outcome.graph.contains(&triple(
441 "http://ex/x",
442 "http://ex/classified",
443 "http://ex/yes",
444 )));
445 }
446
447 #[test]
448 fn split_inference_uses_shapes_graph_as_rule_context() {
449 let shapes_ttl = format!(
450 "{PREFIXES}
451 ex:InverseShape a sh:NodeShape ;
452 sh:targetClass ex:Thing ;
453 sh:rule [
454 a sh:SPARQLRule ;
455 sh:construct \"\"\"
456 CONSTRUCT {{ ?o ?inverse $this }}
457 WHERE {{
458 $this ?predicate ?o .
459 ?predicate ex:inverseOf ?inverse .
460 }}
461 \"\"\"
462 ] .
463 ex:p ex:inverseOf ex:q .
464 "
465 );
466 let data_ttl = format!(
467 "{PREFIXES}
468 ex:a a ex:Thing ; ex:p ex:b .
469 "
470 );
471 let shapes = shifty_parse::load_turtle(shapes_ttl.as_bytes(), None).unwrap();
472 let parsed = shifty_parse::parse_loaded(&shapes);
473 let data = shifty_parse::load_turtle(data_ttl.as_bytes(), None).unwrap();
474
475 let outcome = infer_graphs(&data.graph, &shapes.graph, &parsed.schema).unwrap();
476
477 assert!(
478 outcome
479 .graph
480 .contains(&triple("http://ex/b", "http://ex/q", "http://ex/a"))
481 );
482 assert_eq!(outcome.inferred.len(), 1);
483 }
484}