1use crate::diagnostics::{DiagLevel, Diagnostic};
10use crate::graph::{Loaded, term_to_node};
11use crate::path::parse_path;
12use crate::vocab;
13use oxrdf::{Literal, NamedNode, NamedOrBlankNode, Term};
14use shifty_algebra::{
15 Bound, NodeExpr, NodeKindSet, Path, Rule, RuleHead, Schema, Selector, Severity, Shape,
16 ShapeArena, ShapeId, SparqlConstraint, SparqlConstruct, SparqlQueryKind, SparqlTarget,
17 Statement, ValueType,
18};
19use spargebra::{Query, SparqlParser, algebra::GraphPattern};
20use std::collections::{BTreeSet, HashMap, HashSet};
21
22pub struct Lowered {
23 pub schema: Schema,
24 pub diagnostics: Vec<Diagnostic>,
25}
26
27pub fn lower(g: &Loaded) -> Lowered {
29 let mut l = Lowerer {
30 g,
31 arena: ShapeArena::new(),
32 cache: HashMap::new(),
33 statements: Vec::new(),
34 rules: Vec::new(),
35 diags: Vec::new(),
36 };
37 let shapes = l.discover_shapes();
38 for s in &shapes {
39 l.lower_shape(s);
40 }
41 l.lower_custom_components(&shapes);
42 for s in &shapes {
43 let selectors = l.target_selectors(s);
45 if let Some(shape) = l.cache.get(s).copied() {
46 for sel in &selectors {
47 l.statements.push(Statement {
48 selector: sel.clone(),
49 shape,
50 });
51 }
52 }
53 l.parse_rules(s, &selectors);
54 }
55 let sources = l
60 .cache
61 .iter()
62 .map(|(node, id)| (*id, Term::from(node.clone())))
63 .collect();
64 let mut schema = Schema {
65 arena: l.arena,
66 statements: l.statements,
67 rules: l.rules,
68 names: Default::default(),
69 sources,
70 };
71 for (node, id) in &l.cache {
72 if let NamedOrBlankNode::NamedNode(n) = node {
73 schema.add_name(*id, n.as_str().to_string());
74 }
75 }
76 schema.arena.debug_assert_finalized();
77 Lowered {
78 schema,
79 diagnostics: l.diags,
80 }
81}
82
83struct Lowerer<'a> {
84 g: &'a Loaded,
85 arena: ShapeArena,
86 cache: HashMap<NamedOrBlankNode, ShapeId>,
87 statements: Vec<Statement>,
88 rules: Vec<Rule>,
89 diags: Vec<Diagnostic>,
90}
91
92struct ComponentDef {
93 iri: NamedNode,
94 params: Vec<ParamDef>,
95 node_validator: Option<ValidatorDef>,
96 property_validator: Option<ValidatorDef>,
97 generic_validator: Option<ValidatorDef>,
98}
99
100struct ParamDef {
101 path: NamedNode,
102 var: String,
103 optional: bool,
104}
105
106struct ValidatorDef {
107 kind: SparqlQueryKind,
108 query: String,
109 messages: Vec<Term>,
110}
111
112fn local_name(iri: &str) -> &str {
115 iri.rsplit(['#', '/']).next().unwrap_or(iri)
116}
117
118impl Lowerer<'_> {
119 fn diag(&mut self, level: DiagLevel, msg: impl Into<String>, subj: &NamedOrBlankNode) {
120 self.diags
121 .push(Diagnostic::new(level, msg, Some(subj.to_string())));
122 }
123
124 fn discover_shapes(&self) -> Vec<NamedOrBlankNode> {
128 let mut found: HashSet<NamedOrBlankNode> = HashSet::new();
129 for triple in self.g.graph.iter() {
130 let p = triple.predicate;
131 let is_target = p == vocab::SH_TARGET_NODE
132 || p == vocab::SH_TARGET_CLASS
133 || p == vocab::SH_TARGET_SUBJECTS_OF
134 || p == vocab::SH_TARGET_OBJECTS_OF
135 || p == vocab::SH_TARGET;
136 if p == vocab::SH_PATH || p == vocab::SH_SPARQL || p == vocab::SH_RULE || is_target {
137 found.insert(triple.subject.into_owned());
138 }
139 if p == vocab::RDF_TYPE
140 && let Term::NamedNode(ty) = triple.object.into_owned()
141 && (ty.as_ref() == vocab::SH_NODE_SHAPE || ty.as_ref() == vocab::SH_PROPERTY_SHAPE)
142 {
143 found.insert(triple.subject.into_owned());
144 }
145 }
146 let mut shapes: Vec<NamedOrBlankNode> = found.into_iter().collect();
147 shapes.sort_by_key(|n| n.to_string());
148 shapes
149 }
150
151 fn lower_shape(&mut self, s: &NamedOrBlankNode) -> ShapeId {
152 if let Some(id) = self.cache.get(s) {
153 return *id;
154 }
155 let id = self.arena.reserve();
156 self.cache.insert(s.clone(), id);
157
158 if self.bool_prop(s, vocab::SH_DEACTIVATED) {
159 self.arena.set(id, Shape::Top);
160 return id;
161 }
162
163 let path = self.parse_shape_path(s);
164 let mut conjuncts: Vec<ShapeId> = Vec::new();
165
166 let value = self.collect_value_constraints(s);
169 if !value.is_empty() {
170 let value_phi = self.arena.and(value);
171 match &path {
172 Some(p) => {
173 let neg = self.arena.not(value_phi);
175 let c = self.arena.count(p.clone(), None, Some(0), neg);
176 conjuncts.push(c);
177 }
178 None => conjuncts.push(value_phi),
179 }
180 }
181
182 self.collect_path_constraints(s, path.as_ref(), &mut conjuncts);
183
184 if self.bool_prop(s, vocab::SH_CLOSED) {
185 let q = self.closed_allowed(s);
186 let c = self.arena.insert(Shape::Closed(q));
187 conjuncts.push(c);
188 }
189
190 let mut sparql_prebound = vec!["this", "shapesGraph", "currentShape"];
191 if path.is_some() {
192 sparql_prebound.push("PATH");
193 }
194 for constraint_term in self.g.objects(s, vocab::SH_SPARQL) {
195 let Some(constraint_node) = term_to_node(&constraint_term) else {
196 self.diag(DiagLevel::Error, "sh:sparql must reference a resource", s);
197 continue;
198 };
199 let parsed = if let Some(Term::Literal(query)) =
200 self.g.object(&constraint_node, vocab::SH_SELECT)
201 {
202 self.canonical_shacl_sparql(
203 &constraint_node,
204 query.value(),
205 ExpectedQuery::Select,
206 &sparql_prebound,
207 )
208 .map(|query| (SparqlQueryKind::Select, query))
209 } else if let Some(Term::Literal(query)) =
210 self.g.object(&constraint_node, vocab::SH_ASK)
211 {
212 self.canonical_shacl_sparql(
213 &constraint_node,
214 query.value(),
215 ExpectedQuery::Ask,
216 &sparql_prebound,
217 )
218 .map(|query| (SparqlQueryKind::Ask, query))
219 } else {
220 self.diag(
221 DiagLevel::Error,
222 "sh:sparql constraint requires sh:select or sh:ask",
223 &constraint_node,
224 );
225 None
226 };
227 if let Some((kind, query)) = parsed {
228 let shape = Some(match s {
229 NamedOrBlankNode::NamedNode(n) => Term::NamedNode(n.clone()),
230 NamedOrBlankNode::BlankNode(b) => Term::BlankNode(b.clone()),
231 });
232 let mut messages: Vec<Term> = self.g.objects(&constraint_node, vocab::SH_MESSAGE);
235 if messages.is_empty() {
236 messages = self.g.objects(s, vocab::SH_MESSAGE);
237 }
238 let constraint = SparqlConstraint {
239 kind,
240 query,
241 path: path.clone(),
242 shape,
243 messages,
244 extra_bindings: Vec::new(),
245 bind_value_to_this: false,
246 };
247 conjuncts.push(self.arena.insert(Shape::Sparql(constraint)));
248 }
249 }
250
251 for expr_term in self.g.objects(s, vocab::SH_EXPRESSION) {
255 if let Some(expr) = self.parse_node_expr(expr_term, s) {
256 if node_expr_has_function(&expr) {
257 self.diag(
261 DiagLevel::Unsupported,
262 "sh:expression with a function call is not yet evaluated",
263 s,
264 );
265 } else {
266 conjuncts.push(self.arena.insert(Shape::Expression(expr)));
267 }
268 }
269 }
270
271 let body = if conjuncts.is_empty() {
272 self.arena.top()
273 } else if conjuncts.len() == 1 {
274 if conjuncts[0] == id {
275 self.arena.top()
276 } else {
277 conjuncts[0]
278 }
279 } else {
280 self.arena.insert(Shape::And(conjuncts))
281 };
282 self.arena.set(
283 id,
284 Shape::Annotated {
285 severity: self.severity(s),
286 messages: self.messages(s),
287 shape: body,
288 },
289 );
290 id
291 }
292
293 fn severity(&self, shape: &NamedOrBlankNode) -> Severity {
294 match self.g.object(shape, vocab::SH_SEVERITY) {
295 Some(Term::NamedNode(value)) => Severity::from_named_node(value),
296 _ => Severity::Violation,
297 }
298 }
299
300 fn messages(&self, shape: &NamedOrBlankNode) -> std::sync::Arc<[Term]> {
302 self.g.objects(shape, vocab::SH_MESSAGE).into()
303 }
304
305 fn collect_value_constraints(&mut self, s: &NamedOrBlankNode) -> Vec<ShapeId> {
306 let mut value: Vec<ShapeId> = Vec::new();
307
308 for c in self.g.objects(s, vocab::SH_CLASS) {
310 let tn = self.arena.insert(Shape::TestConst(c));
311 let cc = self.arena.count(class_path(), Some(1), None, tn);
312 value.push(cc);
313 }
314
315 for d in self.g.objects(s, vocab::SH_DATATYPE) {
317 if let Term::NamedNode(n) = d {
318 let id = self.arena.insert(Shape::TestType(ValueType::Datatype(n)));
319 value.push(id);
320 }
321 }
322
323 for k in self.g.objects(s, vocab::SH_NODE_KIND) {
325 if let Some(set) = map_node_kind(&k) {
326 let id = self.arena.insert(Shape::TestKind(set));
327 value.push(id);
328 } else {
329 self.diag(DiagLevel::Warning, "unrecognized sh:nodeKind value", s);
330 }
331 }
332
333 let lo = self
335 .lit(s, vocab::SH_MIN_INCLUSIVE)
336 .map(|value| Bound {
337 value,
338 inclusive: true,
339 })
340 .or_else(|| {
341 self.lit(s, vocab::SH_MIN_EXCLUSIVE).map(|value| Bound {
342 value,
343 inclusive: false,
344 })
345 });
346 let hi = self
347 .lit(s, vocab::SH_MAX_INCLUSIVE)
348 .map(|value| Bound {
349 value,
350 inclusive: true,
351 })
352 .or_else(|| {
353 self.lit(s, vocab::SH_MAX_EXCLUSIVE).map(|value| Bound {
354 value,
355 inclusive: false,
356 })
357 });
358 if lo.is_some() || hi.is_some() {
359 let id = self
360 .arena
361 .insert(Shape::TestType(ValueType::NumericRange { lo, hi }));
362 value.push(id);
363 }
364
365 let min_len = self.int(s, vocab::SH_MIN_LENGTH);
367 let max_len = self.int(s, vocab::SH_MAX_LENGTH);
368 if min_len.is_some() || max_len.is_some() {
369 let id = self.arena.insert(Shape::TestType(ValueType::Length {
370 min: min_len,
371 max: max_len,
372 }));
373 value.push(id);
374 }
375
376 let flags = self
378 .lit(s, vocab::SH_FLAGS)
379 .map(|l| l.value().to_string())
380 .unwrap_or_default();
381 for pat in self.g.objects(s, vocab::SH_PATTERN) {
382 if let Term::Literal(l) = pat {
383 let id = self.arena.insert(Shape::TestType(ValueType::Pattern {
384 regex: l.value().to_string(),
385 flags: flags.clone(),
386 }));
387 value.push(id);
388 }
389 }
390
391 for li in self.g.objects(s, vocab::SH_LANGUAGE_IN) {
393 let langs: Vec<String> = self
394 .g
395 .read_list(&li)
396 .into_iter()
397 .filter_map(|m| match m {
398 Term::Literal(l) => Some(l.value().to_string()),
399 _ => None,
400 })
401 .collect();
402 let id = self.arena.insert(Shape::TestType(ValueType::LangIn(langs)));
403 value.push(id);
404 }
405
406 for inl in self.g.objects(s, vocab::SH_IN) {
408 let alts: Vec<ShapeId> = self
409 .g
410 .read_list(&inl)
411 .into_iter()
412 .map(|m| self.arena.insert(Shape::TestConst(m)))
413 .collect();
414 let or = self.arena.or(alts);
415 value.push(or);
416 }
417
418 for n in self.g.objects(s, vocab::SH_NODE) {
420 if let Some(nn) = term_to_node(&n) {
421 let id = self.lower_shape(&nn);
422 value.push(id);
423 }
424 }
425
426 for prop in self.g.objects(s, vocab::SH_PROPERTY) {
430 if let Some(pn) = term_to_node(&prop) {
431 let id = self.lower_shape(&pn);
432 value.push(id);
433 }
434 }
435
436 for n in self.g.objects(s, vocab::SH_NOT) {
438 if let Some(nn) = term_to_node(&n) {
439 let id = self.lower_shape(&nn);
440 let neg = self.arena.not(id);
441 value.push(neg);
442 }
443 }
444
445 for l in self.g.objects(s, vocab::SH_AND) {
447 let ids = self.lower_shape_list(&l);
448 let a = self.arena.and(ids);
449 value.push(a);
450 }
451 for l in self.g.objects(s, vocab::SH_OR) {
452 let ids = self.lower_shape_list(&l);
453 let o = self.arena.or(ids);
454 value.push(o);
455 }
456 for l in self.g.objects(s, vocab::SH_XONE) {
457 let ids = self.lower_shape_list(&l);
458 let x = self.arena.xone(ids);
459 value.push(x);
460 }
461
462 value
463 }
464
465 fn collect_path_constraints(
469 &mut self,
470 s: &NamedOrBlankNode,
471 path: Option<&Path>,
472 conjuncts: &mut Vec<ShapeId>,
473 ) {
474 let need_path = |me: &mut Self, what: &str| {
475 me.diag(DiagLevel::Warning, format!("{what} ignored: no sh:path"), s);
476 };
477
478 let min_count = self.int(s, vocab::SH_MIN_COUNT);
479 let max_count = self.int(s, vocab::SH_MAX_COUNT);
480 if min_count.is_some() || max_count.is_some() {
481 match path {
482 Some(p) => {
483 let top = self.arena.top();
484 let c = self.arena.count(p.clone(), min_count, max_count, top);
485 conjuncts.push(c);
486 }
487 None => need_path(self, "sh:minCount/sh:maxCount"),
488 }
489 }
490
491 for v in self.g.objects(s, vocab::SH_HAS_VALUE) {
493 match path {
494 Some(p) => {
495 let tc = self.arena.insert(Shape::TestConst(v));
496 let c = self.arena.count(p.clone(), Some(1), None, tc);
497 conjuncts.push(c);
498 }
499 None => {
500 let tc = self.arena.insert(Shape::TestConst(v));
501 conjuncts.push(tc);
502 }
503 }
504 }
505
506 for q in self.g.objects(s, vocab::SH_QUALIFIED_VALUE_SHAPE) {
508 if let Some(qn) = term_to_node(&q) {
509 let qmin = self.int(s, vocab::SH_QUALIFIED_MIN_COUNT);
510 let qmax = self.int(s, vocab::SH_QUALIFIED_MAX_COUNT);
511 match path {
512 Some(p) => {
513 let mut qualifiers = vec![self.lower_shape(&qn)];
514 if self.bool_prop(s, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
515 for sibling in self.sibling_qualified_shapes(s, &qn) {
516 let sibling = self.lower_shape(&sibling);
517 qualifiers.push(self.arena.not(sibling));
518 }
519 }
520 let qualifier = self.arena.and(qualifiers);
521 let c = self.arena.count(p.clone(), qmin, qmax, qualifier);
522 conjuncts.push(c);
523 }
524 None => need_path(self, "sh:qualifiedValueShape"),
525 }
526 }
527 }
528
529 let pairs = [
531 (vocab::SH_EQUALS, "equals"),
532 (vocab::SH_DISJOINT, "disjoint"),
533 (vocab::SH_LESS_THAN, "lessThan"),
534 (vocab::SH_LESS_THAN_OR_EQUALS, "lessThanOrEquals"),
535 ];
536 for (pred, name) in pairs {
537 for other in self.g.objects(s, pred) {
538 let Term::NamedNode(op) = other else { continue };
539 match path {
540 Some(p) => {
541 let shape = match name {
542 "equals" => Shape::Eq(p.clone(), op),
543 "disjoint" => Shape::Disj(p.clone(), op),
544 "lessThan" => Shape::Lt(p.clone(), op),
545 _ => Shape::Le(p.clone(), op),
546 };
547 let c = self.arena.insert(shape);
548 conjuncts.push(c);
549 }
550 None if matches!(name, "equals" | "disjoint") => {
551 let shape = if name == "equals" {
552 Shape::Eq(Path::Id, op)
553 } else {
554 Shape::Disj(Path::Id, op)
555 };
556 let c = self.arena.insert(shape);
557 conjuncts.push(c);
558 }
559 None => need_path(self, &format!("sh:{name}")),
560 }
561 }
562 }
563
564 if self.bool_prop(s, vocab::SH_UNIQUE_LANG) {
566 match path {
567 Some(p) => {
568 let c = self.arena.insert(Shape::UniqueLang(p.clone()));
569 conjuncts.push(c);
570 }
571 None => need_path(self, "sh:uniqueLang"),
572 }
573 }
574 }
575
576 fn target_selectors(&mut self, s: &NamedOrBlankNode) -> Vec<Selector> {
578 let mut sels = Vec::new();
579
580 for c in self.g.objects(s, vocab::SH_TARGET_NODE) {
581 sels.push(Selector::IsConst(c));
582 }
583 for c in self.g.objects(s, vocab::SH_TARGET_CLASS) {
584 sels.push(self.class_selector(c));
585 }
586 for p in self.g.objects(s, vocab::SH_TARGET_SUBJECTS_OF) {
587 if let Term::NamedNode(n) = p {
588 sels.push(Selector::HasOut(n));
589 }
590 }
591 for p in self.g.objects(s, vocab::SH_TARGET_OBJECTS_OF) {
592 if let Term::NamedNode(n) = p {
593 sels.push(Selector::HasIn(n));
594 }
595 }
596
597 if (self.g.is_instance_of(s, vocab::RDFS_CLASS)
599 || self.g.is_instance_of(s, vocab::OWL_CLASS))
600 && let NamedOrBlankNode::NamedNode(n) = s
601 {
602 sels.push(self.class_selector(Term::NamedNode(n.clone())));
603 }
604
605 for target_term in self.g.objects(s, vocab::SH_TARGET) {
606 let Some(target_node) = term_to_node(&target_term) else {
607 self.diag(DiagLevel::Error, "sh:target must reference a resource", s);
608 continue;
609 };
610 match self.g.object(&target_node, vocab::SH_SELECT) {
611 Some(Term::Literal(query)) => {
612 if let Some(query) =
613 self.canonical_sparql(&target_node, query.value(), ExpectedQuery::Select)
614 {
615 sels.push(Selector::Sparql(SparqlTarget { query }));
616 }
617 }
618 _ => self.diag(
619 DiagLevel::Unsupported,
620 "custom sh:target without sh:select is not yet lowered",
621 &target_node,
622 ),
623 }
624 }
625
626 sels
627 }
628
629 fn lower_custom_components(&mut self, shapes: &[NamedOrBlankNode]) {
639 let components = self.collect_component_defs();
640 if components.is_empty() {
641 return;
642 }
643 for s in shapes {
644 let Some(&shape_id) = self.cache.get(s) else {
645 continue;
646 };
647 let is_property_shape = self.g.object(s, vocab::SH_PATH).is_some();
648 let path = if is_property_shape {
649 self.parse_shape_path(s)
650 } else {
651 None
652 };
653 let shape_term = match s {
654 NamedOrBlankNode::NamedNode(n) => Some(Term::NamedNode(n.clone())),
655 NamedOrBlankNode::BlankNode(b) => Some(Term::BlankNode(b.clone())),
656 };
657
658 for component in &components {
659 let mut extra_bindings: Vec<(String, Term)> = Vec::new();
661 let mut activated = true;
662 for p in &component.params {
663 if let Some(value) = self.g.object(s, p.path.as_ref()) {
664 extra_bindings.push((p.var.clone(), value));
665 } else if !p.optional {
666 activated = false;
667 break;
668 }
669 }
670 if !activated {
671 continue;
672 }
673
674 let validator = if is_property_shape {
676 component
677 .property_validator
678 .as_ref()
679 .or(component.generic_validator.as_ref())
680 } else {
681 component
682 .node_validator
683 .as_ref()
684 .or(component.generic_validator.as_ref())
685 };
686 let Some(validator) = validator else { continue };
687
688 let constraint = SparqlConstraint {
689 kind: validator.kind,
690 query: validator.query.clone(),
691 path: path.clone(),
692 shape: shape_term.clone(),
693 messages: validator.messages.clone(),
694 extra_bindings,
695 bind_value_to_this: !is_property_shape
698 && validator.kind == SparqlQueryKind::Ask,
699 };
700 let raw_id = self.arena.insert(Shape::Sparql(constraint));
701 let sparql_id = if validator.kind == SparqlQueryKind::Ask {
707 self.arena.not(raw_id)
708 } else {
709 raw_id
710 };
711
712 let (severity, messages, inner) = match self.arena.get(shape_id) {
714 Shape::Annotated {
715 severity,
716 messages,
717 shape: inner,
718 } => (severity.clone(), messages.clone(), *inner),
719 _ => continue, };
721 let combined = self.arena.and(vec![inner, sparql_id]);
722 self.arena.set(
723 shape_id,
724 Shape::Annotated {
725 severity,
726 messages,
727 shape: combined,
728 },
729 );
730 }
731 }
732 }
733
734 fn collect_component_defs(&mut self) -> Vec<ComponentDef> {
738 let mut out: Vec<ComponentDef> = Vec::new();
739 let mut seen = HashSet::new();
740 for triple in self.g.graph.triples_for_predicate(vocab::SH_PARAMETER) {
741 let subject = triple.subject.into_owned();
742 if !seen.insert(subject.clone()) {
743 continue;
744 }
745 let NamedOrBlankNode::NamedNode(iri) = &subject else {
746 continue;
747 };
748 if vocab::NATIVE_CONSTRAINT_COMPONENTS.contains(&iri.as_ref()) {
749 continue; }
751 let mut params: Vec<ParamDef> = Vec::new();
752 for p in self.g.objects(&subject, vocab::SH_PARAMETER) {
753 let Some(pn) = term_to_node(&p) else { continue };
754 let Some(Term::NamedNode(path)) = self.g.object(&pn, vocab::SH_PATH) else {
755 continue;
756 };
757 let var = local_name(path.as_str()).to_string();
758 let optional = matches!(
759 self.g.object(&pn, vocab::SH_OPTIONAL),
760 Some(Term::Literal(ref l)) if l.value() == "true"
761 );
762 params.push(ParamDef {
763 path,
764 var,
765 optional,
766 });
767 }
768 if params.is_empty() {
769 continue;
770 }
771 let mut prebound = vec![
772 "this".to_string(),
773 "value".to_string(),
774 "shapesGraph".to_string(),
775 "currentShape".to_string(),
776 "PATH".to_string(),
777 ];
778 prebound.extend(params.iter().map(|p| p.var.clone()));
779 let node_validator = self
780 .g
781 .object(&subject, vocab::SH_NODE_VALIDATOR)
782 .and_then(|v| self.parse_validator_def(&v, &prebound));
783 let property_validator = self
784 .g
785 .object(&subject, vocab::SH_PROPERTY_VALIDATOR)
786 .and_then(|v| self.parse_validator_def(&v, &prebound));
787 let generic_validator = self
788 .g
789 .object(&subject, vocab::SH_VALIDATOR)
790 .and_then(|v| self.parse_validator_def(&v, &prebound));
791 if node_validator.is_none()
792 && property_validator.is_none()
793 && generic_validator.is_none()
794 {
795 continue; }
797 out.push(ComponentDef {
798 iri: iri.clone(),
799 params,
800 node_validator,
801 property_validator,
802 generic_validator,
803 });
804 }
805 out.sort_by(|a, b| a.iri.as_str().cmp(b.iri.as_str()));
806 out
807 }
808
809 fn parse_validator_def(&mut self, node: &Term, prebound: &[String]) -> Option<ValidatorDef> {
813 let node = term_to_node(node)?;
814 let (kind, raw) = if let Some(Term::Literal(q)) = self.g.object(&node, vocab::SH_ASK) {
815 (SparqlQueryKind::Ask, q.value().to_string())
816 } else if let Some(Term::Literal(q)) = self.g.object(&node, vocab::SH_SELECT) {
817 (SparqlQueryKind::Select, q.value().to_string())
818 } else {
819 return None; };
821 let canonical = match canonical_sparql_query(self.g, &node, &raw).and_then(|(query, c)| {
822 validate_shacl_prebinding(&query, prebound.iter().map(String::as_str))?;
823 Ok(c)
824 }) {
825 Ok(c) => c,
826 Err(msg) => {
827 self.diag(
828 DiagLevel::Error,
829 format!("invalid SPARQL in validator: {msg}"),
830 &node,
831 );
832 return None;
833 }
834 };
835 let messages = self.g.objects(&node, vocab::SH_MESSAGE);
836 Some(ValidatorDef {
837 kind,
838 query: canonical,
839 messages,
840 })
841 }
842
843 fn parse_rules(&mut self, s: &NamedOrBlankNode, selectors: &[Selector]) {
846 for rule_term in self.g.objects(s, vocab::SH_RULE) {
847 let Some(rn) = term_to_node(&rule_term) else {
848 continue;
849 };
850 let Some(head) = self.parse_rule_head(&rn) else {
851 continue;
852 };
853
854 let conditions: Vec<ShapeId> = self
855 .g
856 .objects(&rn, vocab::SH_CONDITION)
857 .iter()
858 .filter_map(term_to_node)
859 .map(|c| self.lower_shape(&c))
860 .collect();
861 let order = self.order(&rn);
862 let deactivated = self.bool_prop(&rn, vocab::SH_DEACTIVATED);
863
864 for sel in selectors {
865 self.rules.push(Rule {
866 selector: sel.clone(),
867 conditions: conditions.clone(),
868 head: head.clone(),
869 order,
870 deactivated,
871 });
872 }
873 }
874 }
875
876 fn sibling_qualified_shapes(
879 &self,
880 shape: &NamedOrBlankNode,
881 qualifier: &NamedOrBlankNode,
882 ) -> Vec<NamedOrBlankNode> {
883 let mut siblings = HashSet::new();
884 for triple in self.g.graph.triples_for_predicate(vocab::SH_PROPERTY) {
885 if term_to_node(&triple.object.into_owned()).as_ref() != Some(shape) {
886 continue;
887 }
888 let parent = triple.subject.into_owned();
889 for property in self.g.objects(&parent, vocab::SH_PROPERTY) {
890 let Some(property) = term_to_node(&property) else {
891 continue;
892 };
893 for sibling in self.g.objects(&property, vocab::SH_QUALIFIED_VALUE_SHAPE) {
894 if let Some(sibling) = term_to_node(&sibling) {
895 siblings.insert(sibling);
896 }
897 }
898 }
899 }
900 siblings.remove(qualifier);
901 let mut siblings: Vec<_> = siblings.into_iter().collect();
902 siblings.sort_by_key(|node| node.to_string());
903 siblings
904 }
905
906 fn parse_rule_head(&mut self, rn: &NamedOrBlankNode) -> Option<RuleHead> {
907 if let Some(Term::Literal(q)) = self.g.object(rn, vocab::SH_CONSTRUCT) {
910 let query = self.canonical_sparql(rn, q.value(), ExpectedQuery::Construct)?;
911 return Some(RuleHead::Sparql(SparqlConstruct { query }));
912 }
913 let (subj, pred, obj) = (
915 self.g.object(rn, vocab::SH_SUBJECT),
916 self.g.object(rn, vocab::SH_PREDICATE),
917 self.g.object(rn, vocab::SH_OBJECT),
918 );
919 if subj.is_none() && pred.is_none() && obj.is_none() {
920 self.diag(DiagLevel::Unsupported, "unrecognized sh:rule head", rn);
921 return None;
922 }
923 let (Some(subj), Some(pred), Some(obj)) = (subj, pred, obj) else {
924 self.diag(
925 DiagLevel::Error,
926 "sh:TripleRule missing subject/predicate/object",
927 rn,
928 );
929 return None;
930 };
931 Some(RuleHead::Triple {
932 subject: self.parse_node_expr(subj, rn)?,
933 predicate: self.parse_node_expr(pred, rn)?,
934 object: self.parse_node_expr(obj, rn)?,
935 })
936 }
937
938 fn parse_node_expr(&mut self, term: Term, owner: &NamedOrBlankNode) -> Option<NodeExpr> {
942 match &term {
943 Term::NamedNode(n) if n.as_ref() == vocab::SH_THIS => Some(NodeExpr::This),
944 Term::NamedNode(_) | Term::Literal(_) => Some(NodeExpr::Constant(term)),
945 Term::BlankNode(_) => {
946 let node = term_to_node(&term).expect("blank node");
947 if let Some(path_term) = self.g.object(&node, vocab::SH_PATH) {
948 match parse_path(self.g, &path_term) {
949 Ok(path) => Some(NodeExpr::Path(path)),
950 Err(e) => {
951 self.diag(
952 DiagLevel::Error,
953 format!("invalid node-expression path: {e}"),
954 owner,
955 );
956 None
957 }
958 }
959 } else if self.g.object(&node, vocab::SH_FILTER_SHAPE).is_some() {
960 self.parse_filter_expr(&node, owner)
961 } else if let Some(list) = self.g.object(&node, vocab::SH_INTERSECTION) {
962 self.parse_set_expr(&list, owner)
963 .map(NodeExpr::Intersection)
964 } else if let Some(list) = self.g.object(&node, vocab::SH_UNION) {
965 self.parse_set_expr(&list, owner).map(NodeExpr::Union)
966 } else if let Some(expr) = self.try_function_call(&node, owner) {
967 Some(expr)
968 } else {
969 self.diag(
970 DiagLevel::Unsupported,
971 "complex node expression not yet lowered",
972 owner,
973 );
974 None
975 }
976 }
977 }
978 }
979
980 fn parse_filter_expr(
983 &mut self,
984 node: &NamedOrBlankNode,
985 owner: &NamedOrBlankNode,
986 ) -> Option<NodeExpr> {
987 let Some(shape_term) = self.g.object(node, vocab::SH_FILTER_SHAPE) else {
988 self.diag(DiagLevel::Error, "sh:filterShape missing a value", owner);
989 return None;
990 };
991 let Some(shape_node) = term_to_node(&shape_term) else {
992 self.diag(
993 DiagLevel::Error,
994 "sh:filterShape must reference a shape",
995 owner,
996 );
997 return None;
998 };
999 let Some(nodes_term) = self.g.object(node, vocab::SH_NODES) else {
1000 self.diag(
1001 DiagLevel::Error,
1002 "filter expression missing sh:nodes",
1003 owner,
1004 );
1005 return None;
1006 };
1007 let input = self.parse_node_expr(nodes_term, owner)?;
1008 let shape = self.lower_shape(&shape_node);
1009 Some(NodeExpr::Filter {
1010 input: Box::new(input),
1011 shape,
1012 })
1013 }
1014
1015 fn parse_set_expr(
1019 &mut self,
1020 list_head: &Term,
1021 owner: &NamedOrBlankNode,
1022 ) -> Option<Vec<NodeExpr>> {
1023 let members = self.g.read_list(list_head);
1024 if members.is_empty() {
1025 self.diag(
1026 DiagLevel::Error,
1027 "sh:intersection/sh:union expects a non-empty list",
1028 owner,
1029 );
1030 return None;
1031 }
1032 let n = members.len();
1033 let exprs: Vec<NodeExpr> = members
1034 .into_iter()
1035 .filter_map(|t| self.parse_node_expr(t, owner))
1036 .collect();
1037 if exprs.len() != n {
1038 return None;
1039 }
1040 Some(exprs)
1041 }
1042
1043 fn try_function_call(
1048 &mut self,
1049 node: &NamedOrBlankNode,
1050 owner: &NamedOrBlankNode,
1051 ) -> Option<NodeExpr> {
1052 let func_preds: Vec<(NamedNode, Term)> = self
1053 .g
1054 .graph
1055 .triples_for_subject(node)
1056 .map(|t| (t.predicate.into_owned(), t.object.into_owned()))
1057 .filter(|(p, _)| {
1058 let s = p.as_str();
1059 !s.starts_with(vocab::SH)
1060 && !s.starts_with(vocab::RDF)
1061 && !s.starts_with(vocab::RDFS)
1062 && !s.starts_with(vocab::OWL)
1063 })
1064 .collect();
1065
1066 if func_preds.len() != 1 {
1067 return None;
1068 }
1069 let (func_iri, list_head) = func_preds.into_iter().next().unwrap();
1070 let arg_terms = self.g.read_list(&list_head);
1071 let n = arg_terms.len();
1072 let args: Vec<NodeExpr> = arg_terms
1073 .into_iter()
1074 .filter_map(|t| self.parse_node_expr(t, owner))
1075 .collect();
1076 if args.len() != n {
1077 return None;
1078 }
1079 Some(NodeExpr::Function {
1080 iri: func_iri,
1081 args,
1082 })
1083 }
1084
1085 fn order(&self, s: &NamedOrBlankNode) -> Option<i64> {
1086 match self.g.object(s, vocab::SH_ORDER) {
1087 Some(Term::Literal(l)) => l.value().parse().ok(),
1088 _ => None,
1089 }
1090 }
1091
1092 fn class_selector(&mut self, class: Term) -> Selector {
1094 let tn = self.arena.insert(Shape::TestConst(class));
1095 Selector::HasPath(class_path(), tn)
1096 }
1097
1098 fn lower_shape_list(&mut self, list_head: &Term) -> Vec<ShapeId> {
1099 self.g
1100 .read_list(list_head)
1101 .into_iter()
1102 .filter_map(|m| term_to_node(&m))
1103 .map(|n| self.lower_shape(&n))
1104 .collect()
1105 }
1106
1107 fn parse_shape_path(&mut self, s: &NamedOrBlankNode) -> Option<Path> {
1108 let term = self.g.object(s, vocab::SH_PATH)?;
1109 match parse_path(self.g, &term) {
1110 Ok(p) => Some(p),
1111 Err(e) => {
1112 self.diag(DiagLevel::Error, format!("invalid sh:path: {e}"), s);
1113 None
1114 }
1115 }
1116 }
1117
1118 fn closed_allowed(&self, s: &NamedOrBlankNode) -> BTreeSet<oxrdf::NamedNode> {
1119 let mut q = BTreeSet::new();
1120 for prop in self.g.objects(s, vocab::SH_PROPERTY) {
1121 if let Some(pn) = term_to_node(&prop)
1122 && let Some(Term::NamedNode(n)) = self.g.object(&pn, vocab::SH_PATH)
1123 {
1124 q.insert(n);
1125 }
1126 }
1127 for ip in self.g.objects(s, vocab::SH_IGNORED_PROPERTIES) {
1128 for m in self.g.read_list(&ip) {
1129 if let Term::NamedNode(n) = m {
1130 q.insert(n);
1131 }
1132 }
1133 }
1134 q
1135 }
1136
1137 fn bool_prop(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> bool {
1138 matches!(self.g.object(s, pred), Some(Term::Literal(l)) if l.value() == "true")
1139 }
1140
1141 fn int(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> Option<u64> {
1142 match self.g.object(s, pred) {
1143 Some(Term::Literal(l)) => l.value().parse().ok(),
1144 _ => None,
1145 }
1146 }
1147
1148 fn lit(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> Option<Literal> {
1149 match self.g.object(s, pred) {
1150 Some(Term::Literal(l)) => Some(l),
1151 _ => None,
1152 }
1153 }
1154
1155 fn canonical_sparql(
1159 &mut self,
1160 owner: &NamedOrBlankNode,
1161 raw: &str,
1162 expected: ExpectedQuery,
1163 ) -> Option<String> {
1164 let (query, canonical) = match canonical_sparql_query(self.g, owner, raw) {
1165 Ok(result) => result,
1166 Err(message) => {
1167 self.diag(DiagLevel::Error, message, owner);
1168 return None;
1169 }
1170 };
1171 let actual = match &query {
1172 Query::Select { .. } => ExpectedQuery::Select,
1173 Query::Ask { .. } => ExpectedQuery::Ask,
1174 Query::Construct { .. } => ExpectedQuery::Construct,
1175 Query::Describe { .. } => ExpectedQuery::Describe,
1176 };
1177 if actual != expected {
1178 self.diag(
1179 DiagLevel::Error,
1180 format!("expected SPARQL {expected}, found {actual}"),
1181 owner,
1182 );
1183 return None;
1184 }
1185 Some(canonical)
1186 }
1187
1188 fn canonical_shacl_sparql(
1192 &mut self,
1193 owner: &NamedOrBlankNode,
1194 raw: &str,
1195 expected: ExpectedQuery,
1196 prebound: &[&str],
1197 ) -> Option<String> {
1198 let (query, canonical) = match canonical_sparql_query(self.g, owner, raw) {
1199 Ok(result) => result,
1200 Err(message) => {
1201 self.diag(DiagLevel::Error, message, owner);
1202 return None;
1203 }
1204 };
1205 let actual = match &query {
1206 Query::Select { .. } => ExpectedQuery::Select,
1207 Query::Ask { .. } => ExpectedQuery::Ask,
1208 Query::Construct { .. } => ExpectedQuery::Construct,
1209 Query::Describe { .. } => ExpectedQuery::Describe,
1210 };
1211 if actual != expected {
1212 self.diag(
1213 DiagLevel::Error,
1214 format!("expected SPARQL {expected}, found {actual}"),
1215 owner,
1216 );
1217 return None;
1218 }
1219 if let Err(message) = validate_shacl_prebinding(&query, prebound.iter().copied()) {
1220 self.diag(DiagLevel::Error, message, owner);
1221 return None;
1222 }
1223 Some(canonical)
1224 }
1225}
1226
1227fn validate_shacl_prebinding<'a>(
1229 query: &Query,
1230 prebound: impl IntoIterator<Item = &'a str>,
1231) -> Result<(), String> {
1232 let prebound: HashSet<&str> = prebound.into_iter().collect();
1233 let pattern = match query {
1234 Query::Select { pattern, .. }
1235 | Query::Ask { pattern, .. }
1236 | Query::Construct { pattern, .. }
1237 | Query::Describe { pattern, .. } => pattern,
1238 };
1239 let mut in_scope = HashSet::new();
1240 pattern.on_in_scope_variable(|variable| {
1241 in_scope.insert(variable.as_str());
1242 });
1243 let projection_prebound: HashSet<&str> = prebound
1244 .iter()
1245 .copied()
1246 .filter(|name| matches!(*name, "this" | "value" | "PATH") && in_scope.contains(name))
1247 .collect();
1248
1249 fn visit(
1250 pattern: &GraphPattern,
1251 prebound: &HashSet<&str>,
1252 projection_prebound: &HashSet<&str>,
1253 seen_top_projection: bool,
1254 ) -> Result<(), String> {
1255 match pattern {
1256 GraphPattern::Minus { .. } => {
1257 return Err("SHACL-SPARQL queries must not contain MINUS".into());
1258 }
1259 GraphPattern::Service { .. } => {
1260 return Err("SHACL-SPARQL queries must not contain SERVICE".into());
1261 }
1262 GraphPattern::Values { .. } => {
1263 return Err("SHACL-SPARQL queries must not contain VALUES".into());
1264 }
1265 GraphPattern::Extend {
1266 inner, variable, ..
1267 } => {
1268 if prebound.contains(variable.as_str()) {
1269 return Err(format!(
1270 "SHACL-SPARQL must not assign potentially pre-bound variable ?{} with AS",
1271 variable.as_str()
1272 ));
1273 }
1274 visit(inner, prebound, projection_prebound, seen_top_projection)?;
1275 }
1276 GraphPattern::Project { inner, variables } => {
1277 if seen_top_projection {
1278 let projected: HashSet<&str> = variables.iter().map(|v| v.as_str()).collect();
1279 let mut missing: Vec<&str> = projection_prebound
1280 .iter()
1281 .copied()
1282 .filter(|name| !projected.contains(name))
1283 .collect();
1284 missing.sort_unstable();
1285 if !missing.is_empty() {
1286 return Err(format!(
1287 "SHACL-SPARQL subquery does not project potentially pre-bound variable(s): {}",
1288 missing
1289 .into_iter()
1290 .map(|name| format!("?{name}"))
1291 .collect::<Vec<_>>()
1292 .join(", ")
1293 ));
1294 }
1295 }
1296 visit(inner, prebound, projection_prebound, true)?;
1297 }
1298 GraphPattern::Join { left, right }
1299 | GraphPattern::LeftJoin { left, right, .. }
1300 | GraphPattern::Union { left, right }
1301 | GraphPattern::Lateral { left, right } => {
1302 visit(left, prebound, projection_prebound, seen_top_projection)?;
1303 visit(right, prebound, projection_prebound, seen_top_projection)?;
1304 }
1305 GraphPattern::Graph { inner, .. }
1306 | GraphPattern::Filter { inner, .. }
1307 | GraphPattern::OrderBy { inner, .. }
1308 | GraphPattern::Distinct { inner }
1309 | GraphPattern::Reduced { inner }
1310 | GraphPattern::Slice { inner, .. }
1311 | GraphPattern::Group { inner, .. } => {
1312 visit(inner, prebound, projection_prebound, seen_top_projection)?;
1313 }
1314 GraphPattern::Bgp { .. } | GraphPattern::Path { .. } => {}
1315 }
1316 Ok(())
1317 }
1318
1319 visit(pattern, &prebound, &projection_prebound, false)
1325}
1326
1327pub fn canonical_sparql_query(
1339 g: &Loaded,
1340 owner: &NamedOrBlankNode,
1341 raw: &str,
1342) -> Result<(Query, String), String> {
1343 let mut parser = SparqlParser::new();
1344 if let Some(base) = &g.base {
1345 parser = parser
1346 .with_base_iri(base)
1347 .map_err(|e| format!("invalid SPARQL base IRI: {e}"))?;
1348 }
1349 for (prefix, namespace) in &g.prefixes {
1350 parser = parser
1351 .with_prefix(prefix, namespace)
1352 .map_err(|e| format!("invalid SPARQL prefix declaration {prefix}: {e}"))?;
1353 }
1354 let mut prefix_sources: Vec<NamedOrBlankNode> = g
1355 .objects(owner, vocab::SH_PREFIXES)
1356 .iter()
1357 .filter_map(term_to_node)
1358 .collect();
1359 let mut seen_sources = HashSet::new();
1360 while let Some(source) = prefix_sources.pop() {
1361 if !seen_sources.insert(source.clone()) {
1362 continue;
1363 }
1364 prefix_sources.extend(
1365 g.objects(&source, vocab::OWL_IMPORTS)
1366 .iter()
1367 .filter_map(term_to_node),
1368 );
1369 for declaration_term in g.objects(&source, vocab::SH_DECLARE) {
1370 let Some(declaration) = term_to_node(&declaration_term) else {
1371 continue;
1372 };
1373 let (Some(Term::Literal(prefix)), Some(Term::Literal(namespace))) = (
1374 g.object(&declaration, vocab::SH_PREFIX),
1375 g.object(&declaration, vocab::SH_NAMESPACE),
1376 ) else {
1377 continue;
1378 };
1379 parser = parser
1380 .with_prefix(prefix.value(), namespace.value())
1381 .map_err(|e| format!("invalid SHACL SPARQL prefix declaration: {e}"))?;
1382 }
1383 }
1384 let query = parser
1385 .parse_query(raw)
1386 .map_err(|e| format!("invalid SPARQL query: {e}"))?;
1387 let canonical = query.to_string();
1388 Ok((query, canonical))
1389}
1390
1391#[derive(Clone, Copy, PartialEq, Eq)]
1392enum ExpectedQuery {
1393 Select,
1394 Ask,
1395 Construct,
1396 Describe,
1397}
1398
1399impl std::fmt::Display for ExpectedQuery {
1400 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1401 f.write_str(match self {
1402 Self::Select => "SELECT",
1403 Self::Ask => "ASK",
1404 Self::Construct => "CONSTRUCT",
1405 Self::Describe => "DESCRIBE",
1406 })
1407 }
1408}
1409
1410fn class_path() -> Path {
1411 Path::seq(vec![
1412 Path::Pred(vocab::rdf_type()),
1413 Path::star(Path::Pred(vocab::rdfs_subclassof())),
1414 ])
1415}
1416
1417fn node_expr_has_function(e: &NodeExpr) -> bool {
1422 match e {
1423 NodeExpr::Function { .. } => true,
1424 NodeExpr::Filter { input, .. } => node_expr_has_function(input),
1425 NodeExpr::Intersection(es) | NodeExpr::Union(es) => es.iter().any(node_expr_has_function),
1426 NodeExpr::This | NodeExpr::Constant(_) | NodeExpr::Path(_) => false,
1427 }
1428}
1429
1430fn map_node_kind(term: &Term) -> Option<NodeKindSet> {
1431 let Term::NamedNode(n) = term else {
1432 return None;
1433 };
1434 let r = n.as_ref();
1435 Some(if r == vocab::SH_IRI {
1436 NodeKindSet::IRI
1437 } else if r == vocab::SH_BLANK_NODE {
1438 NodeKindSet::BLANK_NODE
1439 } else if r == vocab::SH_LITERAL {
1440 NodeKindSet::LITERAL
1441 } else if r == vocab::SH_BLANK_NODE_OR_IRI {
1442 NodeKindSet::BLANK_NODE_OR_IRI
1443 } else if r == vocab::SH_BLANK_NODE_OR_LITERAL {
1444 NodeKindSet::BLANK_NODE_OR_LITERAL
1445 } else if r == vocab::SH_IRI_OR_LITERAL {
1446 NodeKindSet::IRI_OR_LITERAL
1447 } else {
1448 return None;
1449 })
1450}