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 mut sparql_prebound = vec!["this", "shapesGraph", "currentShape"];
238 if path.is_some() {
239 sparql_prebound.push("PATH");
240 }
241 for constraint_term in self.g.objects(s, vocab::SH_SPARQL) {
242 let Some(constraint_node) = term_to_node(&constraint_term) else {
243 self.diag(DiagLevel::Error, "sh:sparql must reference a resource", s);
244 continue;
245 };
246 let parsed = if let Some(Term::Literal(query)) =
247 self.g.object(&constraint_node, vocab::SH_SELECT)
248 {
249 self.canonical_shacl_sparql(
250 &constraint_node,
251 query.value(),
252 ExpectedQuery::Select,
253 &sparql_prebound,
254 )
255 .map(|query| (SparqlQueryKind::Select, query))
256 } else if let Some(Term::Literal(query)) =
257 self.g.object(&constraint_node, vocab::SH_ASK)
258 {
259 self.canonical_shacl_sparql(
260 &constraint_node,
261 query.value(),
262 ExpectedQuery::Ask,
263 &sparql_prebound,
264 )
265 .map(|query| (SparqlQueryKind::Ask, query))
266 } else {
267 self.diag(
268 DiagLevel::Error,
269 "sh:sparql constraint requires sh:select or sh:ask",
270 &constraint_node,
271 );
272 None
273 };
274 if let Some((kind, query)) = parsed {
275 let shape = Some(match s {
276 NamedOrBlankNode::NamedNode(n) => Term::NamedNode(n.clone()),
277 NamedOrBlankNode::BlankNode(b) => Term::BlankNode(b.clone()),
278 });
279 let mut messages: Vec<Term> = self.g.objects(&constraint_node, vocab::SH_MESSAGE);
282 if messages.is_empty() {
283 messages = self.g.objects(s, vocab::SH_MESSAGE);
284 }
285 let constraint = SparqlConstraint {
286 kind,
287 query,
288 path: path.clone(),
289 shape,
290 messages,
291 extra_bindings: Vec::new(),
292 bind_value_to_this: false,
293 };
294 conjuncts.push(self.arena.insert(Shape::Sparql(constraint)));
295 }
296 }
297
298 for expr_term in self.g.objects(s, vocab::SH_EXPRESSION) {
302 if let Some(expr) = self.parse_node_expr(expr_term, s) {
303 if node_expr_has_function(&expr) {
304 self.diag(
308 DiagLevel::Unsupported,
309 "sh:expression with a function call is not yet evaluated",
310 s,
311 );
312 } else {
313 conjuncts.push(self.arena.insert(Shape::Expression(expr)));
314 }
315 }
316 }
317
318 let body = if conjuncts.is_empty() {
324 self.arena.top()
325 } else if conjuncts.len() == 1 {
326 if conjuncts[0] == id {
327 self.arena.top()
328 } else {
329 conjuncts[0]
330 }
331 } else {
332 self.arena.insert(Shape::And(conjuncts))
333 };
334 self.arena.set(
335 id,
336 Shape::Annotated {
337 severity: self.severity(s),
338 messages: self.messages(s),
339 shape: body,
340 },
341 );
342 id
343 }
344
345 fn severity(&self, shape: &NamedOrBlankNode) -> Severity {
346 match self.g.object(shape, vocab::SH_SEVERITY) {
347 Some(Term::NamedNode(value)) => Severity::from_named_node(value),
348 _ => Severity::Violation,
349 }
350 }
351
352 fn messages(&self, shape: &NamedOrBlankNode) -> std::sync::Arc<[Term]> {
354 self.g.objects(shape, vocab::SH_MESSAGE).into()
355 }
356
357 fn collect_value_constraints(&mut self, s: &NamedOrBlankNode) -> Vec<ShapeId> {
365 let mut value: Vec<ShapeId> = Vec::new();
366
367 for c in self.g.objects(s, vocab::SH_CLASS) {
369 let tn = self.arena.insert(Shape::TestConst(c));
370 let cc = self.arena.count(class_path(), Some(1), None, tn);
371 value.push(cc);
372 }
373
374 for d in self.g.objects(s, vocab::SH_DATATYPE) {
376 if let Term::NamedNode(n) = d {
377 let id = self.arena.insert(Shape::TestType(ValueType::Datatype(n)));
378 value.push(id);
379 }
380 }
381
382 for k in self.g.objects(s, vocab::SH_NODE_KIND) {
384 if let Some(set) = map_node_kind(&k) {
385 let id = self.arena.insert(Shape::TestKind(set));
386 value.push(id);
387 } else {
388 self.diag(DiagLevel::Warning, "unrecognized sh:nodeKind value", s);
389 }
390 }
391
392 let lo = self
396 .lit(s, vocab::SH_MIN_INCLUSIVE)
397 .map(|value| Bound {
398 value,
399 inclusive: true,
400 })
401 .or_else(|| {
402 self.lit(s, vocab::SH_MIN_EXCLUSIVE).map(|value| Bound {
403 value,
404 inclusive: false,
405 })
406 });
407 let hi = self
408 .lit(s, vocab::SH_MAX_INCLUSIVE)
409 .map(|value| Bound {
410 value,
411 inclusive: true,
412 })
413 .or_else(|| {
414 self.lit(s, vocab::SH_MAX_EXCLUSIVE).map(|value| Bound {
415 value,
416 inclusive: false,
417 })
418 });
419 if lo.is_some() || hi.is_some() {
420 let id = self
421 .arena
422 .insert(Shape::TestType(ValueType::NumericRange { lo, hi }));
423 value.push(id);
424 }
425
426 let min_len = self.int(s, vocab::SH_MIN_LENGTH);
428 let max_len = self.int(s, vocab::SH_MAX_LENGTH);
429 if min_len.is_some() || max_len.is_some() {
430 let id = self.arena.insert(Shape::TestType(ValueType::Length {
431 min: min_len,
432 max: max_len,
433 }));
434 value.push(id);
435 }
436
437 let flags = self
439 .lit(s, vocab::SH_FLAGS)
440 .map(|l| l.value().to_string())
441 .unwrap_or_default();
442 for pat in self.g.objects(s, vocab::SH_PATTERN) {
443 if let Term::Literal(l) = pat {
444 let id = self.arena.insert(Shape::TestType(ValueType::Pattern {
445 regex: l.value().to_string(),
446 flags: flags.clone(),
447 }));
448 value.push(id);
449 }
450 }
451
452 for li in self.g.objects(s, vocab::SH_LANGUAGE_IN) {
454 let langs: Vec<String> = self
455 .g
456 .read_list(&li)
457 .into_iter()
458 .filter_map(|m| match m {
459 Term::Literal(l) => Some(l.value().to_string()),
460 _ => None,
461 })
462 .collect();
463 let id = self.arena.insert(Shape::TestType(ValueType::LangIn(langs)));
464 value.push(id);
465 }
466
467 for inl in self.g.objects(s, vocab::SH_IN) {
471 let alts: Vec<ShapeId> = self
472 .g
473 .read_list(&inl)
474 .into_iter()
475 .map(|m| self.arena.insert(Shape::TestConst(m)))
476 .collect();
477 let or = self.arena.or(alts);
478 value.push(or);
479 }
480
481 for n in self.g.objects(s, vocab::SH_NODE) {
485 if let Some(nn) = term_to_node(&n) {
486 let id = self.lower_shape(&nn);
487 value.push(id);
488 }
489 }
490
491 for prop in self.g.objects(s, vocab::SH_PROPERTY) {
495 if let Some(pn) = term_to_node(&prop) {
496 let id = self.lower_shape(&pn);
497 value.push(id);
498 }
499 }
500
501 for n in self.g.objects(s, vocab::SH_NOT) {
503 if let Some(nn) = term_to_node(&n) {
504 let id = self.lower_shape(&nn);
505 let neg = self.arena.not(id);
506 value.push(neg);
507 }
508 }
509
510 for l in self.g.objects(s, vocab::SH_AND) {
512 let ids = self.lower_shape_list(&l);
513 let a = self.arena.and(ids);
514 value.push(a);
515 }
516 for l in self.g.objects(s, vocab::SH_OR) {
517 let ids = self.lower_shape_list(&l);
518 let o = self.arena.or(ids);
519 value.push(o);
520 }
521 for l in self.g.objects(s, vocab::SH_XONE) {
522 let ids = self.lower_shape_list(&l);
523 let x = self.arena.xone(ids);
524 value.push(x);
525 }
526
527 value
528 }
529
530 fn collect_path_constraints(
534 &mut self,
535 s: &NamedOrBlankNode,
536 path: Option<&Path>,
537 conjuncts: &mut Vec<ShapeId>,
538 ) {
539 let need_path = |me: &mut Self, what: &str| {
540 me.diag(DiagLevel::Warning, format!("{what} ignored: no sh:path"), s);
541 };
542
543 let min_count = self.int(s, vocab::SH_MIN_COUNT);
544 let max_count = self.int(s, vocab::SH_MAX_COUNT);
545 if min_count.is_some() || max_count.is_some() {
546 match path {
547 Some(p) => {
548 let top = self.arena.top();
549 let c = self.arena.count(p.clone(), min_count, max_count, top);
550 conjuncts.push(c);
551 }
552 None => need_path(self, "sh:minCount/sh:maxCount"),
553 }
554 }
555
556 for v in self.g.objects(s, vocab::SH_HAS_VALUE) {
558 match path {
559 Some(p) => {
560 let tc = self.arena.insert(Shape::TestConst(v));
561 let c = self.arena.count(p.clone(), Some(1), None, tc);
562 conjuncts.push(c);
563 }
564 None => {
565 let tc = self.arena.insert(Shape::TestConst(v));
566 conjuncts.push(tc);
567 }
568 }
569 }
570
571 for q in self.g.objects(s, vocab::SH_QUALIFIED_VALUE_SHAPE) {
573 if let Some(qn) = term_to_node(&q) {
574 let qmin = self.int(s, vocab::SH_QUALIFIED_MIN_COUNT);
575 let qmax = self.int(s, vocab::SH_QUALIFIED_MAX_COUNT);
576 match path {
577 Some(p) => {
578 let mut qualifiers = vec![self.lower_shape(&qn)];
579 if self.bool_prop(s, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
580 for sibling in self.sibling_qualified_shapes(s, &qn) {
581 let sibling = self.lower_shape(&sibling);
582 qualifiers.push(self.arena.not(sibling));
583 }
584 }
585 let qualifier = self.arena.and(qualifiers);
586 let c = self.arena.count(p.clone(), qmin, qmax, qualifier);
587 conjuncts.push(c);
588 }
589 None => need_path(self, "sh:qualifiedValueShape"),
590 }
591 }
592 }
593
594 let pairs = [
596 (vocab::SH_EQUALS, "equals"),
597 (vocab::SH_DISJOINT, "disjoint"),
598 (vocab::SH_LESS_THAN, "lessThan"),
599 (vocab::SH_LESS_THAN_OR_EQUALS, "lessThanOrEquals"),
600 ];
601 for (pred, name) in pairs {
602 for other in self.g.objects(s, pred) {
603 let Term::NamedNode(op) = other else { continue };
604 match path {
605 Some(p) => {
606 let shape = match name {
607 "equals" => Shape::Eq(p.clone(), op),
608 "disjoint" => Shape::Disj(p.clone(), op),
609 "lessThan" => Shape::Lt(p.clone(), op),
610 _ => Shape::Le(p.clone(), op),
611 };
612 let c = self.arena.insert(shape);
613 conjuncts.push(c);
614 }
615 None if matches!(name, "equals" | "disjoint") => {
616 let shape = if name == "equals" {
617 Shape::Eq(Path::Id, op)
618 } else {
619 Shape::Disj(Path::Id, op)
620 };
621 let c = self.arena.insert(shape);
622 conjuncts.push(c);
623 }
624 None => need_path(self, &format!("sh:{name}")),
625 }
626 }
627 }
628
629 if self.bool_prop(s, vocab::SH_UNIQUE_LANG) {
631 match path {
632 Some(p) => {
633 let c = self.arena.insert(Shape::UniqueLang(p.clone()));
634 conjuncts.push(c);
635 }
636 None => need_path(self, "sh:uniqueLang"),
637 }
638 }
639 }
640
641 fn target_selectors(&mut self, s: &NamedOrBlankNode) -> Vec<Selector> {
643 let mut sels = Vec::new();
644
645 for c in self.g.objects(s, vocab::SH_TARGET_NODE) {
646 sels.push(Selector::IsConst(c));
647 }
648 for c in self.g.objects(s, vocab::SH_TARGET_CLASS) {
649 sels.push(self.class_selector(c));
650 }
651 for p in self.g.objects(s, vocab::SH_TARGET_SUBJECTS_OF) {
652 if let Term::NamedNode(n) = p {
653 sels.push(Selector::HasOut(n));
654 }
655 }
656 for p in self.g.objects(s, vocab::SH_TARGET_OBJECTS_OF) {
657 if let Term::NamedNode(n) = p {
658 sels.push(Selector::HasIn(n));
659 }
660 }
661
662 if (self.g.is_instance_of(s, vocab::RDFS_CLASS)
664 || self.g.is_instance_of(s, vocab::OWL_CLASS))
665 && let NamedOrBlankNode::NamedNode(n) = s
666 {
667 sels.push(self.class_selector(Term::NamedNode(n.clone())));
668 }
669
670 for target_term in self.g.objects(s, vocab::SH_TARGET) {
671 let Some(target_node) = term_to_node(&target_term) else {
672 self.diag(DiagLevel::Error, "sh:target must reference a resource", s);
673 continue;
674 };
675 match self.g.object(&target_node, vocab::SH_SELECT) {
676 Some(Term::Literal(query)) => {
677 if let Some(query) =
678 self.canonical_sparql(&target_node, query.value(), ExpectedQuery::Select)
679 {
680 sels.push(Selector::Sparql(SparqlTarget { query }));
681 }
682 }
683 _ => self.diag(
684 DiagLevel::Unsupported,
685 "custom sh:target without sh:select is not yet lowered",
686 &target_node,
687 ),
688 }
689 }
690
691 sels
692 }
693
694 fn lower_custom_components(&mut self, shapes: &[NamedOrBlankNode]) {
704 let components = self.collect_component_defs();
705 if components.is_empty() {
706 return;
707 }
708 for s in shapes {
709 let Some(&shape_id) = self.cache.get(s) else {
710 continue;
711 };
712 let is_property_shape = self.g.object(s, vocab::SH_PATH).is_some();
713 let path = if is_property_shape {
714 self.parse_shape_path(s)
715 } else {
716 None
717 };
718 let shape_term = match s {
719 NamedOrBlankNode::NamedNode(n) => Some(Term::NamedNode(n.clone())),
720 NamedOrBlankNode::BlankNode(b) => Some(Term::BlankNode(b.clone())),
721 };
722
723 for component in &components {
724 let mut extra_bindings: Vec<(String, Term)> = Vec::new();
726 let mut activated = true;
727 for p in &component.params {
728 if let Some(value) = self.g.object(s, p.path.as_ref()) {
729 extra_bindings.push((p.var.clone(), value));
730 } else if !p.optional {
731 activated = false;
732 break;
733 }
734 }
735 if !activated {
736 continue;
737 }
738
739 let validator = if is_property_shape {
741 component
742 .property_validator
743 .as_ref()
744 .or(component.generic_validator.as_ref())
745 } else {
746 component
747 .node_validator
748 .as_ref()
749 .or(component.generic_validator.as_ref())
750 };
751 let Some(validator) = validator else { continue };
752
753 let constraint = SparqlConstraint {
754 kind: validator.kind,
755 query: validator.query.clone(),
756 path: path.clone(),
757 shape: shape_term.clone(),
758 messages: validator.messages.clone(),
759 extra_bindings,
760 bind_value_to_this: !is_property_shape
763 && validator.kind == SparqlQueryKind::Ask,
764 };
765 let raw_id = self.arena.insert(Shape::Sparql(constraint));
766 let sparql_id = if validator.kind == SparqlQueryKind::Ask {
772 self.arena.not(raw_id)
773 } else {
774 raw_id
775 };
776
777 let (severity, messages, inner) = match self.arena.get(shape_id) {
779 Shape::Annotated {
780 severity,
781 messages,
782 shape: inner,
783 } => (severity.clone(), messages.clone(), *inner),
784 _ => continue, };
786 let combined = self.arena.and(vec![inner, sparql_id]);
787 self.arena.set(
788 shape_id,
789 Shape::Annotated {
790 severity,
791 messages,
792 shape: combined,
793 },
794 );
795 }
796 }
797 }
798
799 fn collect_component_defs(&mut self) -> Vec<ComponentDef> {
803 let mut out: Vec<ComponentDef> = Vec::new();
804 let mut seen = HashSet::new();
805 for triple in self.g.graph.triples_for_predicate(vocab::SH_PARAMETER) {
806 let subject = triple.subject.into_owned();
807 if !seen.insert(subject.clone()) {
808 continue;
809 }
810 let NamedOrBlankNode::NamedNode(iri) = &subject else {
811 continue;
812 };
813 if vocab::NATIVE_CONSTRAINT_COMPONENTS.contains(&iri.as_ref()) {
814 continue; }
816 let mut params: Vec<ParamDef> = Vec::new();
817 for p in self.g.objects(&subject, vocab::SH_PARAMETER) {
818 let Some(pn) = term_to_node(&p) else { continue };
819 let Some(Term::NamedNode(path)) = self.g.object(&pn, vocab::SH_PATH) else {
820 continue;
821 };
822 let var = local_name(path.as_str()).to_string();
823 let optional = matches!(
824 self.g.object(&pn, vocab::SH_OPTIONAL),
825 Some(Term::Literal(ref l)) if l.value() == "true"
826 );
827 params.push(ParamDef {
828 path,
829 var,
830 optional,
831 });
832 }
833 if params.is_empty() {
834 continue;
835 }
836 let mut prebound = vec![
837 "this".to_string(),
838 "value".to_string(),
839 "shapesGraph".to_string(),
840 "currentShape".to_string(),
841 "PATH".to_string(),
842 ];
843 prebound.extend(params.iter().map(|p| p.var.clone()));
844 let node_validator = self
845 .g
846 .object(&subject, vocab::SH_NODE_VALIDATOR)
847 .and_then(|v| self.parse_validator_def(&v, &prebound));
848 let property_validator = self
849 .g
850 .object(&subject, vocab::SH_PROPERTY_VALIDATOR)
851 .and_then(|v| self.parse_validator_def(&v, &prebound));
852 let generic_validator = self
853 .g
854 .object(&subject, vocab::SH_VALIDATOR)
855 .and_then(|v| self.parse_validator_def(&v, &prebound));
856 if node_validator.is_none()
857 && property_validator.is_none()
858 && generic_validator.is_none()
859 {
860 continue; }
862 out.push(ComponentDef {
863 iri: iri.clone(),
864 params,
865 node_validator,
866 property_validator,
867 generic_validator,
868 });
869 }
870 out.sort_by(|a, b| a.iri.as_str().cmp(b.iri.as_str()));
871 out
872 }
873
874 fn parse_validator_def(&mut self, node: &Term, prebound: &[String]) -> Option<ValidatorDef> {
878 let node = term_to_node(node)?;
879 let (kind, raw) = if let Some(Term::Literal(q)) = self.g.object(&node, vocab::SH_ASK) {
880 (SparqlQueryKind::Ask, q.value().to_string())
881 } else if let Some(Term::Literal(q)) = self.g.object(&node, vocab::SH_SELECT) {
882 (SparqlQueryKind::Select, q.value().to_string())
883 } else {
884 return None; };
886 let canonical = match canonical_sparql_query(self.g, &node, &raw).and_then(|(query, c)| {
887 validate_shacl_prebinding(&query, prebound.iter().map(String::as_str))?;
888 Ok(c)
889 }) {
890 Ok(c) => c,
891 Err(msg) => {
892 self.diag(
893 DiagLevel::Error,
894 format!("invalid SPARQL in validator: {msg}"),
895 &node,
896 );
897 return None;
898 }
899 };
900 let messages = self.g.objects(&node, vocab::SH_MESSAGE);
901 Some(ValidatorDef {
902 kind,
903 query: canonical,
904 messages,
905 })
906 }
907
908 fn parse_rules(&mut self, s: &NamedOrBlankNode, selectors: &[Selector]) {
911 for rule_term in self.g.objects(s, vocab::SH_RULE) {
912 let Some(rn) = term_to_node(&rule_term) else {
913 continue;
914 };
915 let Some(head) = self.parse_rule_head(&rn) else {
916 continue;
917 };
918
919 let conditions: Vec<ShapeId> = self
920 .g
921 .objects(&rn, vocab::SH_CONDITION)
922 .iter()
923 .filter_map(term_to_node)
924 .map(|c| self.lower_shape(&c))
925 .collect();
926 let order = self.order(&rn);
927 let deactivated = self.bool_prop(&rn, vocab::SH_DEACTIVATED);
928
929 for sel in selectors {
930 self.rules.push(Rule {
931 selector: sel.clone(),
932 conditions: conditions.clone(),
933 head: head.clone(),
934 order,
935 deactivated,
936 });
937 }
938 }
939 }
940
941 fn sibling_qualified_shapes(
944 &self,
945 shape: &NamedOrBlankNode,
946 qualifier: &NamedOrBlankNode,
947 ) -> Vec<NamedOrBlankNode> {
948 let mut siblings = HashSet::new();
949 for triple in self.g.graph.triples_for_predicate(vocab::SH_PROPERTY) {
950 if term_to_node(&triple.object.into_owned()).as_ref() != Some(shape) {
951 continue;
952 }
953 let parent = triple.subject.into_owned();
954 for property in self.g.objects(&parent, vocab::SH_PROPERTY) {
955 let Some(property) = term_to_node(&property) else {
956 continue;
957 };
958 for sibling in self.g.objects(&property, vocab::SH_QUALIFIED_VALUE_SHAPE) {
959 if let Some(sibling) = term_to_node(&sibling) {
960 siblings.insert(sibling);
961 }
962 }
963 }
964 }
965 siblings.remove(qualifier);
966 let mut siblings: Vec<_> = siblings.into_iter().collect();
967 siblings.sort_by_key(|node| node.to_string());
968 siblings
969 }
970
971 fn parse_rule_head(&mut self, rn: &NamedOrBlankNode) -> Option<RuleHead> {
972 if let Some(Term::Literal(q)) = self.g.object(rn, vocab::SH_CONSTRUCT) {
975 let query = self.canonical_sparql(rn, q.value(), ExpectedQuery::Construct)?;
976 return Some(RuleHead::Sparql(SparqlConstruct { query }));
977 }
978 let (subj, pred, obj) = (
980 self.g.object(rn, vocab::SH_SUBJECT),
981 self.g.object(rn, vocab::SH_PREDICATE),
982 self.g.object(rn, vocab::SH_OBJECT),
983 );
984 if subj.is_none() && pred.is_none() && obj.is_none() {
985 self.diag(DiagLevel::Unsupported, "unrecognized sh:rule head", rn);
986 return None;
987 }
988 let (Some(subj), Some(pred), Some(obj)) = (subj, pred, obj) else {
989 self.diag(
990 DiagLevel::Error,
991 "sh:TripleRule missing subject/predicate/object",
992 rn,
993 );
994 return None;
995 };
996 Some(RuleHead::Triple {
997 subject: self.parse_node_expr(subj, rn)?,
998 predicate: self.parse_node_expr(pred, rn)?,
999 object: self.parse_node_expr(obj, rn)?,
1000 })
1001 }
1002
1003 fn parse_node_expr(&mut self, term: Term, owner: &NamedOrBlankNode) -> Option<NodeExpr> {
1007 match &term {
1008 Term::NamedNode(n) if n.as_ref() == vocab::SH_THIS => Some(NodeExpr::This),
1009 Term::NamedNode(_) | Term::Literal(_) => Some(NodeExpr::Constant(term)),
1010 Term::BlankNode(_) => {
1011 let node = term_to_node(&term).expect("blank node");
1012 if let Some(path_term) = self.g.object(&node, vocab::SH_PATH) {
1013 match parse_path(self.g, &path_term) {
1014 Ok(path) => Some(NodeExpr::Path(path)),
1015 Err(e) => {
1016 self.diag(
1017 DiagLevel::Error,
1018 format!("invalid node-expression path: {e}"),
1019 owner,
1020 );
1021 None
1022 }
1023 }
1024 } else if self.g.object(&node, vocab::SH_FILTER_SHAPE).is_some() {
1025 self.parse_filter_expr(&node, owner)
1026 } else if let Some(list) = self.g.object(&node, vocab::SH_INTERSECTION) {
1027 self.parse_set_expr(&list, owner)
1028 .map(NodeExpr::Intersection)
1029 } else if let Some(list) = self.g.object(&node, vocab::SH_UNION) {
1030 self.parse_set_expr(&list, owner).map(NodeExpr::Union)
1031 } else if let Some(expr) = self.try_function_call(&node, owner) {
1032 Some(expr)
1033 } else {
1034 self.diag(
1035 DiagLevel::Unsupported,
1036 "complex node expression not yet lowered",
1037 owner,
1038 );
1039 None
1040 }
1041 }
1042 }
1043 }
1044
1045 fn parse_filter_expr(
1048 &mut self,
1049 node: &NamedOrBlankNode,
1050 owner: &NamedOrBlankNode,
1051 ) -> Option<NodeExpr> {
1052 let Some(shape_term) = self.g.object(node, vocab::SH_FILTER_SHAPE) else {
1053 self.diag(DiagLevel::Error, "sh:filterShape missing a value", owner);
1054 return None;
1055 };
1056 let Some(shape_node) = term_to_node(&shape_term) else {
1057 self.diag(
1058 DiagLevel::Error,
1059 "sh:filterShape must reference a shape",
1060 owner,
1061 );
1062 return None;
1063 };
1064 let Some(nodes_term) = self.g.object(node, vocab::SH_NODES) else {
1065 self.diag(
1066 DiagLevel::Error,
1067 "filter expression missing sh:nodes",
1068 owner,
1069 );
1070 return None;
1071 };
1072 let input = self.parse_node_expr(nodes_term, owner)?;
1073 let shape = self.lower_shape(&shape_node);
1074 Some(NodeExpr::Filter {
1075 input: Box::new(input),
1076 shape,
1077 })
1078 }
1079
1080 fn parse_set_expr(
1084 &mut self,
1085 list_head: &Term,
1086 owner: &NamedOrBlankNode,
1087 ) -> Option<Vec<NodeExpr>> {
1088 let members = self.g.read_list(list_head);
1089 if members.is_empty() {
1090 self.diag(
1091 DiagLevel::Error,
1092 "sh:intersection/sh:union expects a non-empty list",
1093 owner,
1094 );
1095 return None;
1096 }
1097 let n = members.len();
1098 let exprs: Vec<NodeExpr> = members
1099 .into_iter()
1100 .filter_map(|t| self.parse_node_expr(t, owner))
1101 .collect();
1102 if exprs.len() != n {
1103 return None;
1104 }
1105 Some(exprs)
1106 }
1107
1108 fn try_function_call(
1113 &mut self,
1114 node: &NamedOrBlankNode,
1115 owner: &NamedOrBlankNode,
1116 ) -> Option<NodeExpr> {
1117 let func_preds: Vec<(NamedNode, Term)> = self
1118 .g
1119 .graph
1120 .triples_for_subject(node)
1121 .map(|t| (t.predicate.into_owned(), t.object.into_owned()))
1122 .filter(|(p, _)| {
1123 let s = p.as_str();
1124 !s.starts_with(vocab::SH)
1125 && !s.starts_with(vocab::RDF)
1126 && !s.starts_with(vocab::RDFS)
1127 && !s.starts_with(vocab::OWL)
1128 })
1129 .collect();
1130
1131 if func_preds.len() != 1 {
1132 return None;
1133 }
1134 let (func_iri, list_head) = func_preds.into_iter().next().unwrap();
1135 let arg_terms = self.g.read_list(&list_head);
1136 let n = arg_terms.len();
1137 let args: Vec<NodeExpr> = arg_terms
1138 .into_iter()
1139 .filter_map(|t| self.parse_node_expr(t, owner))
1140 .collect();
1141 if args.len() != n {
1142 return None;
1143 }
1144 Some(NodeExpr::Function {
1145 iri: func_iri,
1146 args,
1147 })
1148 }
1149
1150 fn order(&self, s: &NamedOrBlankNode) -> Option<i64> {
1151 match self.g.object(s, vocab::SH_ORDER) {
1152 Some(Term::Literal(l)) => l.value().parse().ok(),
1153 _ => None,
1154 }
1155 }
1156
1157 fn class_selector(&mut self, class: Term) -> Selector {
1159 let tn = self.arena.insert(Shape::TestConst(class));
1160 Selector::HasPath(class_path(), tn)
1161 }
1162
1163 fn lower_shape_list(&mut self, list_head: &Term) -> Vec<ShapeId> {
1164 self.g
1165 .read_list(list_head)
1166 .into_iter()
1167 .filter_map(|m| term_to_node(&m))
1168 .map(|n| self.lower_shape(&n))
1169 .collect()
1170 }
1171
1172 fn parse_shape_path(&mut self, s: &NamedOrBlankNode) -> Option<Path> {
1173 let term = self.g.object(s, vocab::SH_PATH)?;
1174 match parse_path(self.g, &term) {
1175 Ok(p) => Some(p),
1176 Err(e) => {
1177 self.diag(DiagLevel::Error, format!("invalid sh:path: {e}"), s);
1178 None
1179 }
1180 }
1181 }
1182
1183 fn closed_allowed(&self, s: &NamedOrBlankNode) -> BTreeSet<oxrdf::NamedNode> {
1184 let mut q = BTreeSet::new();
1185 for prop in self.g.objects(s, vocab::SH_PROPERTY) {
1186 if let Some(pn) = term_to_node(&prop)
1187 && let Some(Term::NamedNode(n)) = self.g.object(&pn, vocab::SH_PATH)
1188 {
1189 q.insert(n);
1190 }
1191 }
1192 for ip in self.g.objects(s, vocab::SH_IGNORED_PROPERTIES) {
1193 for m in self.g.read_list(&ip) {
1194 if let Term::NamedNode(n) = m {
1195 q.insert(n);
1196 }
1197 }
1198 }
1199 q
1200 }
1201
1202 fn bool_prop(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> bool {
1203 matches!(self.g.object(s, pred), Some(Term::Literal(l)) if l.value() == "true")
1204 }
1205
1206 fn int(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> Option<u64> {
1207 match self.g.object(s, pred) {
1208 Some(Term::Literal(l)) => l.value().parse().ok(),
1209 _ => None,
1210 }
1211 }
1212
1213 fn lit(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> Option<Literal> {
1214 match self.g.object(s, pred) {
1215 Some(Term::Literal(l)) => Some(l),
1216 _ => None,
1217 }
1218 }
1219
1220 fn canonical_sparql(
1224 &mut self,
1225 owner: &NamedOrBlankNode,
1226 raw: &str,
1227 expected: ExpectedQuery,
1228 ) -> Option<String> {
1229 let (query, canonical) = match canonical_sparql_query(self.g, owner, raw) {
1230 Ok(result) => result,
1231 Err(message) => {
1232 self.diag(DiagLevel::Error, message, owner);
1233 return None;
1234 }
1235 };
1236 let actual = match &query {
1237 Query::Select { .. } => ExpectedQuery::Select,
1238 Query::Ask { .. } => ExpectedQuery::Ask,
1239 Query::Construct { .. } => ExpectedQuery::Construct,
1240 Query::Describe { .. } => ExpectedQuery::Describe,
1241 };
1242 if actual != expected {
1243 self.diag(
1244 DiagLevel::Error,
1245 format!("expected SPARQL {expected}, found {actual}"),
1246 owner,
1247 );
1248 return None;
1249 }
1250 Some(canonical)
1251 }
1252
1253 fn canonical_shacl_sparql(
1257 &mut self,
1258 owner: &NamedOrBlankNode,
1259 raw: &str,
1260 expected: ExpectedQuery,
1261 prebound: &[&str],
1262 ) -> Option<String> {
1263 let (query, canonical) = match canonical_sparql_query(self.g, owner, raw) {
1264 Ok(result) => result,
1265 Err(message) => {
1266 self.diag(DiagLevel::Error, message, owner);
1267 return None;
1268 }
1269 };
1270 let actual = match &query {
1271 Query::Select { .. } => ExpectedQuery::Select,
1272 Query::Ask { .. } => ExpectedQuery::Ask,
1273 Query::Construct { .. } => ExpectedQuery::Construct,
1274 Query::Describe { .. } => ExpectedQuery::Describe,
1275 };
1276 if actual != expected {
1277 self.diag(
1278 DiagLevel::Error,
1279 format!("expected SPARQL {expected}, found {actual}"),
1280 owner,
1281 );
1282 return None;
1283 }
1284 if let Err(message) = validate_shacl_prebinding(&query, prebound.iter().copied()) {
1285 self.diag(DiagLevel::Error, message, owner);
1286 return None;
1287 }
1288 Some(canonical)
1289 }
1290}
1291
1292fn validate_shacl_prebinding<'a>(
1294 query: &Query,
1295 prebound: impl IntoIterator<Item = &'a str>,
1296) -> Result<(), String> {
1297 let prebound: HashSet<&str> = prebound.into_iter().collect();
1298 let pattern = match query {
1299 Query::Select { pattern, .. }
1300 | Query::Ask { pattern, .. }
1301 | Query::Construct { pattern, .. }
1302 | Query::Describe { pattern, .. } => pattern,
1303 };
1304 let mut in_scope = HashSet::new();
1305 pattern.on_in_scope_variable(|variable| {
1306 in_scope.insert(variable.as_str());
1307 });
1308 let projection_prebound: HashSet<&str> = prebound
1309 .iter()
1310 .copied()
1311 .filter(|name| matches!(*name, "this" | "value" | "PATH") && in_scope.contains(name))
1312 .collect();
1313
1314 fn visit(
1315 pattern: &GraphPattern,
1316 prebound: &HashSet<&str>,
1317 projection_prebound: &HashSet<&str>,
1318 seen_top_projection: bool,
1319 ) -> Result<(), String> {
1320 match pattern {
1321 GraphPattern::Minus { .. } => {
1322 return Err("SHACL-SPARQL queries must not contain MINUS".into());
1323 }
1324 GraphPattern::Service { .. } => {
1325 return Err("SHACL-SPARQL queries must not contain SERVICE".into());
1326 }
1327 GraphPattern::Values { .. } => {
1328 return Err("SHACL-SPARQL queries must not contain VALUES".into());
1329 }
1330 GraphPattern::Extend {
1331 inner, variable, ..
1332 } => {
1333 if prebound.contains(variable.as_str()) {
1334 return Err(format!(
1335 "SHACL-SPARQL must not assign potentially pre-bound variable ?{} with AS",
1336 variable.as_str()
1337 ));
1338 }
1339 visit(inner, prebound, projection_prebound, seen_top_projection)?;
1340 }
1341 GraphPattern::Project { inner, variables } => {
1342 if seen_top_projection {
1343 let projected: HashSet<&str> = variables.iter().map(|v| v.as_str()).collect();
1344 let mut missing: Vec<&str> = projection_prebound
1345 .iter()
1346 .copied()
1347 .filter(|name| !projected.contains(name))
1348 .collect();
1349 missing.sort_unstable();
1350 if !missing.is_empty() {
1351 return Err(format!(
1352 "SHACL-SPARQL subquery does not project potentially pre-bound variable(s): {}",
1353 missing
1354 .into_iter()
1355 .map(|name| format!("?{name}"))
1356 .collect::<Vec<_>>()
1357 .join(", ")
1358 ));
1359 }
1360 }
1361 visit(inner, prebound, projection_prebound, true)?;
1362 }
1363 GraphPattern::Join { left, right }
1364 | GraphPattern::LeftJoin { left, right, .. }
1365 | GraphPattern::Union { left, right }
1366 | GraphPattern::Lateral { left, right } => {
1367 visit(left, prebound, projection_prebound, seen_top_projection)?;
1368 visit(right, prebound, projection_prebound, seen_top_projection)?;
1369 }
1370 GraphPattern::Graph { inner, .. }
1371 | GraphPattern::Filter { inner, .. }
1372 | GraphPattern::OrderBy { inner, .. }
1373 | GraphPattern::Distinct { inner }
1374 | GraphPattern::Reduced { inner }
1375 | GraphPattern::Slice { inner, .. }
1376 | GraphPattern::Group { inner, .. } => {
1377 visit(inner, prebound, projection_prebound, seen_top_projection)?;
1378 }
1379 GraphPattern::Bgp { .. } | GraphPattern::Path { .. } => {}
1380 }
1381 Ok(())
1382 }
1383
1384 visit(pattern, &prebound, &projection_prebound, false)
1390}
1391
1392pub fn canonical_sparql_query(
1404 g: &Loaded,
1405 owner: &NamedOrBlankNode,
1406 raw: &str,
1407) -> Result<(Query, String), String> {
1408 let mut parser = SparqlParser::new();
1409 if let Some(base) = &g.base {
1410 parser = parser
1411 .with_base_iri(base)
1412 .map_err(|e| format!("invalid SPARQL base IRI: {e}"))?;
1413 }
1414 for (prefix, namespace) in &g.prefixes {
1415 parser = parser
1416 .with_prefix(prefix, namespace)
1417 .map_err(|e| format!("invalid SPARQL prefix declaration {prefix}: {e}"))?;
1418 }
1419 let mut prefix_sources: Vec<NamedOrBlankNode> = g
1420 .objects(owner, vocab::SH_PREFIXES)
1421 .iter()
1422 .filter_map(term_to_node)
1423 .collect();
1424 let mut seen_sources = HashSet::new();
1425 while let Some(source) = prefix_sources.pop() {
1426 if !seen_sources.insert(source.clone()) {
1427 continue;
1428 }
1429 prefix_sources.extend(
1430 g.objects(&source, vocab::OWL_IMPORTS)
1431 .iter()
1432 .filter_map(term_to_node),
1433 );
1434 for declaration_term in g.objects(&source, vocab::SH_DECLARE) {
1435 let Some(declaration) = term_to_node(&declaration_term) else {
1436 continue;
1437 };
1438 let (Some(Term::Literal(prefix)), Some(Term::Literal(namespace))) = (
1439 g.object(&declaration, vocab::SH_PREFIX),
1440 g.object(&declaration, vocab::SH_NAMESPACE),
1441 ) else {
1442 continue;
1443 };
1444 parser = parser
1445 .with_prefix(prefix.value(), namespace.value())
1446 .map_err(|e| format!("invalid SHACL SPARQL prefix declaration: {e}"))?;
1447 }
1448 }
1449 let query = parser
1450 .parse_query(raw)
1451 .map_err(|e| format!("invalid SPARQL query: {e}"))?;
1452 let canonical = query.to_string();
1453 Ok((query, canonical))
1454}
1455
1456#[derive(Clone, Copy, PartialEq, Eq)]
1457enum ExpectedQuery {
1458 Select,
1459 Ask,
1460 Construct,
1461 Describe,
1462}
1463
1464impl std::fmt::Display for ExpectedQuery {
1465 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1466 f.write_str(match self {
1467 Self::Select => "SELECT",
1468 Self::Ask => "ASK",
1469 Self::Construct => "CONSTRUCT",
1470 Self::Describe => "DESCRIBE",
1471 })
1472 }
1473}
1474
1475fn class_path() -> Path {
1476 Path::seq(vec![
1477 Path::Pred(vocab::rdf_type()),
1478 Path::star(Path::Pred(vocab::rdfs_subclassof())),
1479 ])
1480}
1481
1482fn node_expr_has_function(e: &NodeExpr) -> bool {
1487 match e {
1488 NodeExpr::Function { .. } => true,
1489 NodeExpr::Filter { input, .. } => node_expr_has_function(input),
1490 NodeExpr::Intersection(es) | NodeExpr::Union(es) => es.iter().any(node_expr_has_function),
1491 NodeExpr::This | NodeExpr::Constant(_) | NodeExpr::Path(_) => false,
1492 }
1493}
1494
1495fn map_node_kind(term: &Term) -> Option<NodeKindSet> {
1496 let Term::NamedNode(n) = term else {
1497 return None;
1498 };
1499 let r = n.as_ref();
1500 Some(if r == vocab::SH_IRI {
1501 NodeKindSet::IRI
1502 } else if r == vocab::SH_BLANK_NODE {
1503 NodeKindSet::BLANK_NODE
1504 } else if r == vocab::SH_LITERAL {
1505 NodeKindSet::LITERAL
1506 } else if r == vocab::SH_BLANK_NODE_OR_IRI {
1507 NodeKindSet::BLANK_NODE_OR_IRI
1508 } else if r == vocab::SH_BLANK_NODE_OR_LITERAL {
1509 NodeKindSet::BLANK_NODE_OR_LITERAL
1510 } else if r == vocab::SH_IRI_OR_LITERAL {
1511 NodeKindSet::IRI_OR_LITERAL
1512 } else {
1513 return None;
1514 })
1515}