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