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, Prefixes, Rule, RuleHead, Schema, Selector, Severity,
35 Shape, 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 prefixes: Prefixes::new(g.prefixes.iter().cloned()),
103 };
104 for (node, id) in &l.cache {
105 if let NamedOrBlankNode::NamedNode(n) = node {
106 schema.add_name(*id, n.as_str().to_string());
107 }
108 }
109 schema.arena.debug_assert_finalized();
110 Lowered {
111 schema,
112 diagnostics: l.diags,
113 }
114}
115
116struct Lowerer<'a> {
117 g: &'a Loaded,
118 arena: ShapeArena,
119 cache: HashMap<NamedOrBlankNode, ShapeId>,
125 statements: Vec<Statement>,
126 rules: Vec<Rule>,
127 diags: Vec<Diagnostic>,
128}
129
130struct ComponentDef {
131 iri: NamedNode,
132 params: Vec<ParamDef>,
133 node_validator: Option<ValidatorDef>,
134 property_validator: Option<ValidatorDef>,
135 generic_validator: Option<ValidatorDef>,
136}
137
138struct ParamDef {
139 path: NamedNode,
140 var: String,
141 optional: bool,
142}
143
144struct ValidatorDef {
145 kind: SparqlQueryKind,
146 query: String,
147 messages: Vec<Term>,
148}
149
150fn local_name(iri: &str) -> &str {
153 iri.rsplit(['#', '/']).next().unwrap_or(iri)
154}
155
156impl Lowerer<'_> {
157 fn diag(&mut self, level: DiagLevel, msg: impl Into<String>, subj: &NamedOrBlankNode) {
158 self.diags
159 .push(Diagnostic::new(level, msg, Some(subj.to_string())));
160 }
161
162 fn discover_shapes(&self) -> Vec<NamedOrBlankNode> {
166 let mut found: HashSet<NamedOrBlankNode> = HashSet::new();
167 for triple in self.g.graph.iter() {
168 let p = triple.predicate;
169 let is_target = p == vocab::SH_TARGET_NODE
170 || p == vocab::SH_TARGET_CLASS
171 || p == vocab::SH_TARGET_SUBJECTS_OF
172 || p == vocab::SH_TARGET_OBJECTS_OF
173 || p == vocab::SH_TARGET;
174 if p == vocab::SH_PATH || p == vocab::SH_SPARQL || p == vocab::SH_RULE || is_target {
175 found.insert(triple.subject.into_owned());
176 }
177 if p == vocab::RDF_TYPE
178 && let Term::NamedNode(ty) = triple.object.into_owned()
179 && (ty.as_ref() == vocab::SH_NODE_SHAPE || ty.as_ref() == vocab::SH_PROPERTY_SHAPE)
180 {
181 found.insert(triple.subject.into_owned());
182 }
183 }
184 let mut shapes: Vec<NamedOrBlankNode> = found.into_iter().collect();
185 shapes.sort_by_key(|n| n.to_string());
186 shapes
187 }
188
189 fn lower_shape(&mut self, s: &NamedOrBlankNode) -> ShapeId {
198 if let Some(id) = self.cache.get(s) {
199 return *id;
200 }
201 let id = self.arena.reserve();
202 self.cache.insert(s.clone(), id);
203
204 if self.bool_prop(s, vocab::SH_DEACTIVATED) {
205 self.arena.set(id, Shape::Top);
206 return id;
207 }
208
209 let path = self.parse_shape_path(s);
210 let mut conjuncts: Vec<ShapeId> = Vec::new();
211
212 let value = self.collect_value_constraints(s);
219 if !value.is_empty() {
220 let value_phi = self.arena.and(value);
221 match &path {
222 Some(p) => {
223 let neg = self.arena.not(value_phi);
225 let c = self.arena.count(p.clone(), None, Some(0), neg);
226 conjuncts.push(c);
227 }
228 None => conjuncts.push(value_phi),
229 }
230 }
231
232 self.collect_path_constraints(s, path.as_ref(), &mut conjuncts);
233
234 if self.bool_prop(s, vocab::SH_CLOSED) {
235 let q = self.closed_allowed(s);
236 let c = self.arena.insert(Shape::Closed(q));
237 conjuncts.push(c);
238 }
239
240 let sparql_prebound = vec!["this", "shapesGraph", "currentShape"];
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: if validator.kind == SparqlQueryKind::Select {
760 path.clone()
761 } else {
762 None
763 },
764 shape: shape_term.clone(),
765 messages: validator.messages.clone(),
766 extra_bindings,
767 bind_value_to_this: !is_property_shape
770 && validator.kind == SparqlQueryKind::Ask,
771 };
772 let raw_id = self.arena.insert(Shape::Sparql(constraint));
773 let sparql_id = if validator.kind == SparqlQueryKind::Ask {
779 self.arena.not(raw_id)
780 } else {
781 raw_id
782 };
783
784 let (severity, messages, inner) = match self.arena.get(shape_id) {
786 Shape::Annotated {
787 severity,
788 messages,
789 shape: inner,
790 } => (severity.clone(), messages.clone(), *inner),
791 _ => continue, };
793 let combined = self.arena.and(vec![inner, sparql_id]);
794 self.arena.set(
795 shape_id,
796 Shape::Annotated {
797 severity,
798 messages,
799 shape: combined,
800 },
801 );
802 }
803 }
804 }
805
806 fn collect_component_defs(&mut self) -> Vec<ComponentDef> {
810 let mut out: Vec<ComponentDef> = Vec::new();
811 let mut seen = HashSet::new();
812 for triple in self.g.graph.triples_for_predicate(vocab::SH_PARAMETER) {
813 let subject = triple.subject.into_owned();
814 if !seen.insert(subject.clone()) {
815 continue;
816 }
817 let NamedOrBlankNode::NamedNode(iri) = &subject else {
818 continue;
819 };
820 if vocab::NATIVE_CONSTRAINT_COMPONENTS.contains(&iri.as_ref()) {
821 continue; }
823 let mut params: Vec<ParamDef> = Vec::new();
824 for p in self.g.objects(&subject, vocab::SH_PARAMETER) {
825 let Some(pn) = term_to_node(&p) else { continue };
826 let Some(Term::NamedNode(path)) = self.g.object(&pn, vocab::SH_PATH) else {
827 continue;
828 };
829 let var = local_name(path.as_str()).to_string();
830 let optional = matches!(
831 self.g.object(&pn, vocab::SH_OPTIONAL),
832 Some(Term::Literal(ref l)) if l.value() == "true"
833 );
834 params.push(ParamDef {
835 path,
836 var,
837 optional,
838 });
839 }
840 if params.is_empty() {
841 continue;
842 }
843 let mut prebound = vec![
844 "this".to_string(),
845 "shapesGraph".to_string(),
846 "currentShape".to_string(),
847 ];
848 prebound.extend(params.iter().map(|p| p.var.clone()));
849 let node_validator = self
850 .g
851 .object(&subject, vocab::SH_NODE_VALIDATOR)
852 .and_then(|v| self.parse_validator_def(&v, &prebound));
853 let property_validator = self
854 .g
855 .object(&subject, vocab::SH_PROPERTY_VALIDATOR)
856 .and_then(|v| self.parse_validator_def(&v, &prebound));
857 let generic_validator = self
858 .g
859 .object(&subject, vocab::SH_VALIDATOR)
860 .and_then(|v| self.parse_validator_def(&v, &prebound));
861 if node_validator.is_none()
862 && property_validator.is_none()
863 && generic_validator.is_none()
864 {
865 continue; }
867 out.push(ComponentDef {
868 iri: iri.clone(),
869 params,
870 node_validator,
871 property_validator,
872 generic_validator,
873 });
874 }
875 out.sort_by(|a, b| a.iri.as_str().cmp(b.iri.as_str()));
876 out
877 }
878
879 fn parse_validator_def(&mut self, node: &Term, prebound: &[String]) -> Option<ValidatorDef> {
883 let node = term_to_node(node)?;
884 let (kind, raw) = if let Some(Term::Literal(q)) = self.g.object(&node, vocab::SH_ASK) {
885 (SparqlQueryKind::Ask, q.value().to_string())
886 } else if let Some(Term::Literal(q)) = self.g.object(&node, vocab::SH_SELECT) {
887 (SparqlQueryKind::Select, q.value().to_string())
888 } else {
889 return None; };
891 let canonical = match canonical_sparql_query(self.g, &node, &raw).and_then(|(query, c)| {
892 let mut validator_prebound = prebound.to_vec();
896 if kind == SparqlQueryKind::Ask {
897 validator_prebound.push("value".to_string());
898 }
899 validate_shacl_prebinding(&query, validator_prebound.iter().map(String::as_str))?;
900 Ok(c)
901 }) {
902 Ok(c) => c,
903 Err(msg) => {
904 self.diag(
905 DiagLevel::Error,
906 format!("invalid SPARQL in validator: {msg}"),
907 &node,
908 );
909 return None;
910 }
911 };
912 let messages = self.g.objects(&node, vocab::SH_MESSAGE);
913 Some(ValidatorDef {
914 kind,
915 query: canonical,
916 messages,
917 })
918 }
919
920 fn parse_rules(&mut self, s: &NamedOrBlankNode, selectors: &[Selector]) {
923 for rule_term in self.g.objects(s, vocab::SH_RULE) {
924 let Some(rn) = term_to_node(&rule_term) else {
925 continue;
926 };
927 let Some(head) = self.parse_rule_head(&rn) else {
928 continue;
929 };
930
931 let conditions: Vec<ShapeId> = self
932 .g
933 .objects(&rn, vocab::SH_CONDITION)
934 .iter()
935 .filter_map(term_to_node)
936 .map(|c| self.lower_shape(&c))
937 .collect();
938 let order = self.order(&rn);
939 let deactivated = self.bool_prop(&rn, vocab::SH_DEACTIVATED);
940
941 for sel in selectors {
942 self.rules.push(Rule {
943 selector: sel.clone(),
944 conditions: conditions.clone(),
945 head: head.clone(),
946 order,
947 deactivated,
948 });
949 }
950 }
951 }
952
953 fn sibling_qualified_shapes(
956 &self,
957 shape: &NamedOrBlankNode,
958 qualifier: &NamedOrBlankNode,
959 ) -> Vec<NamedOrBlankNode> {
960 let mut siblings = HashSet::new();
961 for triple in self.g.graph.triples_for_predicate(vocab::SH_PROPERTY) {
962 if term_to_node(&triple.object.into_owned()).as_ref() != Some(shape) {
963 continue;
964 }
965 let parent = triple.subject.into_owned();
966 for property in self.g.objects(&parent, vocab::SH_PROPERTY) {
967 let Some(property) = term_to_node(&property) else {
968 continue;
969 };
970 for sibling in self.g.objects(&property, vocab::SH_QUALIFIED_VALUE_SHAPE) {
971 if let Some(sibling) = term_to_node(&sibling) {
972 siblings.insert(sibling);
973 }
974 }
975 }
976 }
977 siblings.remove(qualifier);
978 let mut siblings: Vec<_> = siblings.into_iter().collect();
979 siblings.sort_by_key(|node| node.to_string());
980 siblings
981 }
982
983 fn parse_rule_head(&mut self, rn: &NamedOrBlankNode) -> Option<RuleHead> {
984 if let Some(Term::Literal(q)) = self.g.object(rn, vocab::SH_CONSTRUCT) {
987 let query = self.canonical_sparql(rn, q.value(), ExpectedQuery::Construct)?;
988 return Some(RuleHead::Sparql(SparqlConstruct { query }));
989 }
990 let (subj, pred, obj) = (
992 self.g.object(rn, vocab::SH_SUBJECT),
993 self.g.object(rn, vocab::SH_PREDICATE),
994 self.g.object(rn, vocab::SH_OBJECT),
995 );
996 if subj.is_none() && pred.is_none() && obj.is_none() {
997 self.diag(DiagLevel::Unsupported, "unrecognized sh:rule head", rn);
998 return None;
999 }
1000 let (Some(subj), Some(pred), Some(obj)) = (subj, pred, obj) else {
1001 self.diag(
1002 DiagLevel::Error,
1003 "sh:TripleRule missing subject/predicate/object",
1004 rn,
1005 );
1006 return None;
1007 };
1008 Some(RuleHead::Triple {
1009 subject: self.parse_node_expr(subj, rn)?,
1010 predicate: self.parse_node_expr(pred, rn)?,
1011 object: self.parse_node_expr(obj, rn)?,
1012 })
1013 }
1014
1015 fn parse_node_expr(&mut self, term: Term, owner: &NamedOrBlankNode) -> Option<NodeExpr> {
1019 match &term {
1020 Term::NamedNode(n) if n.as_ref() == vocab::SH_THIS => Some(NodeExpr::This),
1021 Term::NamedNode(_) | Term::Literal(_) => Some(NodeExpr::Constant(term)),
1022 Term::BlankNode(_) => {
1023 let node = term_to_node(&term).expect("blank node");
1024 if let Some(path_term) = self.g.object(&node, vocab::SH_PATH) {
1025 match parse_path(self.g, &path_term) {
1026 Ok(path) => Some(NodeExpr::Path(path)),
1027 Err(e) => {
1028 self.diag(
1029 DiagLevel::Error,
1030 format!("invalid node-expression path: {e}"),
1031 owner,
1032 );
1033 None
1034 }
1035 }
1036 } else if self.g.object(&node, vocab::SH_FILTER_SHAPE).is_some() {
1037 self.parse_filter_expr(&node, owner)
1038 } else if let Some(list) = self.g.object(&node, vocab::SH_INTERSECTION) {
1039 self.parse_set_expr(&list, owner)
1040 .map(NodeExpr::Intersection)
1041 } else if let Some(list) = self.g.object(&node, vocab::SH_UNION) {
1042 self.parse_set_expr(&list, owner).map(NodeExpr::Union)
1043 } else if let Some(expr) = self.try_function_call(&node, owner) {
1044 Some(expr)
1045 } else {
1046 self.diag(
1047 DiagLevel::Unsupported,
1048 "complex node expression not yet lowered",
1049 owner,
1050 );
1051 None
1052 }
1053 }
1054 }
1055 }
1056
1057 fn parse_filter_expr(
1060 &mut self,
1061 node: &NamedOrBlankNode,
1062 owner: &NamedOrBlankNode,
1063 ) -> Option<NodeExpr> {
1064 let Some(shape_term) = self.g.object(node, vocab::SH_FILTER_SHAPE) else {
1065 self.diag(DiagLevel::Error, "sh:filterShape missing a value", owner);
1066 return None;
1067 };
1068 let Some(shape_node) = term_to_node(&shape_term) else {
1069 self.diag(
1070 DiagLevel::Error,
1071 "sh:filterShape must reference a shape",
1072 owner,
1073 );
1074 return None;
1075 };
1076 let Some(nodes_term) = self.g.object(node, vocab::SH_NODES) else {
1077 self.diag(
1078 DiagLevel::Error,
1079 "filter expression missing sh:nodes",
1080 owner,
1081 );
1082 return None;
1083 };
1084 let input = self.parse_node_expr(nodes_term, owner)?;
1085 let shape = self.lower_shape(&shape_node);
1086 Some(NodeExpr::Filter {
1087 input: Box::new(input),
1088 shape,
1089 })
1090 }
1091
1092 fn parse_set_expr(
1096 &mut self,
1097 list_head: &Term,
1098 owner: &NamedOrBlankNode,
1099 ) -> Option<Vec<NodeExpr>> {
1100 let members = self.g.read_list(list_head);
1101 if members.is_empty() {
1102 self.diag(
1103 DiagLevel::Error,
1104 "sh:intersection/sh:union expects a non-empty list",
1105 owner,
1106 );
1107 return None;
1108 }
1109 let n = members.len();
1110 let exprs: Vec<NodeExpr> = members
1111 .into_iter()
1112 .filter_map(|t| self.parse_node_expr(t, owner))
1113 .collect();
1114 if exprs.len() != n {
1115 return None;
1116 }
1117 Some(exprs)
1118 }
1119
1120 fn try_function_call(
1125 &mut self,
1126 node: &NamedOrBlankNode,
1127 owner: &NamedOrBlankNode,
1128 ) -> Option<NodeExpr> {
1129 let func_preds: Vec<(NamedNode, Term)> = self
1130 .g
1131 .graph
1132 .triples_for_subject(node)
1133 .map(|t| (t.predicate.into_owned(), t.object.into_owned()))
1134 .filter(|(p, _)| {
1135 let s = p.as_str();
1136 !s.starts_with(vocab::SH)
1137 && !s.starts_with(vocab::RDF)
1138 && !s.starts_with(vocab::RDFS)
1139 && !s.starts_with(vocab::OWL)
1140 })
1141 .collect();
1142
1143 if func_preds.len() != 1 {
1144 return None;
1145 }
1146 let (func_iri, list_head) = func_preds.into_iter().next().unwrap();
1147 let arg_terms = self.g.read_list(&list_head);
1148 let n = arg_terms.len();
1149 let args: Vec<NodeExpr> = arg_terms
1150 .into_iter()
1151 .filter_map(|t| self.parse_node_expr(t, owner))
1152 .collect();
1153 if args.len() != n {
1154 return None;
1155 }
1156 Some(NodeExpr::Function {
1157 iri: func_iri,
1158 args,
1159 })
1160 }
1161
1162 fn order(&self, s: &NamedOrBlankNode) -> Option<i64> {
1163 match self.g.object(s, vocab::SH_ORDER) {
1164 Some(Term::Literal(l)) => l.value().parse().ok(),
1165 _ => None,
1166 }
1167 }
1168
1169 fn class_selector(&mut self, class: Term) -> Selector {
1171 let tn = self.arena.insert(Shape::TestConst(class));
1172 Selector::HasPath(class_path(), tn)
1173 }
1174
1175 fn lower_shape_list(&mut self, list_head: &Term) -> Vec<ShapeId> {
1176 self.g
1177 .read_list(list_head)
1178 .into_iter()
1179 .filter_map(|m| term_to_node(&m))
1180 .map(|n| self.lower_shape(&n))
1181 .collect()
1182 }
1183
1184 fn parse_shape_path(&mut self, s: &NamedOrBlankNode) -> Option<Path> {
1185 let term = self.g.object(s, vocab::SH_PATH)?;
1186 match parse_path(self.g, &term) {
1187 Ok(p) => Some(p),
1188 Err(e) => {
1189 self.diag(DiagLevel::Error, format!("invalid sh:path: {e}"), s);
1190 None
1191 }
1192 }
1193 }
1194
1195 fn closed_allowed(&self, s: &NamedOrBlankNode) -> BTreeSet<oxrdf::NamedNode> {
1196 let mut q = BTreeSet::new();
1197 for prop in self.g.objects(s, vocab::SH_PROPERTY) {
1198 if let Some(pn) = term_to_node(&prop)
1199 && let Some(Term::NamedNode(n)) = self.g.object(&pn, vocab::SH_PATH)
1200 {
1201 q.insert(n);
1202 }
1203 }
1204 for ip in self.g.objects(s, vocab::SH_IGNORED_PROPERTIES) {
1205 for m in self.g.read_list(&ip) {
1206 if let Term::NamedNode(n) = m {
1207 q.insert(n);
1208 }
1209 }
1210 }
1211 q
1212 }
1213
1214 fn bool_prop(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> bool {
1215 matches!(self.g.object(s, pred), Some(Term::Literal(l)) if l.value() == "true")
1216 }
1217
1218 fn int(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> Option<u64> {
1219 match self.g.object(s, pred) {
1220 Some(Term::Literal(l)) => l.value().parse().ok(),
1221 _ => None,
1222 }
1223 }
1224
1225 fn lit(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> Option<Literal> {
1226 match self.g.object(s, pred) {
1227 Some(Term::Literal(l)) => Some(l),
1228 _ => None,
1229 }
1230 }
1231
1232 fn canonical_sparql(
1236 &mut self,
1237 owner: &NamedOrBlankNode,
1238 raw: &str,
1239 expected: ExpectedQuery,
1240 ) -> Option<String> {
1241 let (query, canonical) = match canonical_sparql_query(self.g, owner, raw) {
1242 Ok(result) => result,
1243 Err(message) => {
1244 self.diag(DiagLevel::Error, message, owner);
1245 return None;
1246 }
1247 };
1248 let actual = match &query {
1249 Query::Select { .. } => ExpectedQuery::Select,
1250 Query::Ask { .. } => ExpectedQuery::Ask,
1251 Query::Construct { .. } => ExpectedQuery::Construct,
1252 Query::Describe { .. } => ExpectedQuery::Describe,
1253 };
1254 if actual != expected {
1255 self.diag(
1256 DiagLevel::Error,
1257 format!("expected SPARQL {expected}, found {actual}"),
1258 owner,
1259 );
1260 return None;
1261 }
1262 Some(canonical)
1263 }
1264
1265 fn canonical_shacl_sparql(
1269 &mut self,
1270 owner: &NamedOrBlankNode,
1271 raw: &str,
1272 expected: ExpectedQuery,
1273 prebound: &[&str],
1274 ) -> Option<String> {
1275 let (query, canonical) = match canonical_sparql_query(self.g, owner, raw) {
1276 Ok(result) => result,
1277 Err(message) => {
1278 self.diag(DiagLevel::Error, message, owner);
1279 return None;
1280 }
1281 };
1282 let actual = match &query {
1283 Query::Select { .. } => ExpectedQuery::Select,
1284 Query::Ask { .. } => ExpectedQuery::Ask,
1285 Query::Construct { .. } => ExpectedQuery::Construct,
1286 Query::Describe { .. } => ExpectedQuery::Describe,
1287 };
1288 if actual != expected {
1289 self.diag(
1290 DiagLevel::Error,
1291 format!("expected SPARQL {expected}, found {actual}"),
1292 owner,
1293 );
1294 return None;
1295 }
1296 if let Err(message) = validate_shacl_prebinding(&query, prebound.iter().copied()) {
1297 self.diag(DiagLevel::Error, message, owner);
1298 return None;
1299 }
1300 Some(canonical)
1301 }
1302}
1303
1304fn validate_shacl_prebinding<'a>(
1306 query: &Query,
1307 prebound: impl IntoIterator<Item = &'a str>,
1308) -> Result<(), String> {
1309 let prebound: HashSet<&str> = prebound.into_iter().collect();
1310 let pattern = match query {
1311 Query::Select { pattern, .. }
1312 | Query::Ask { pattern, .. }
1313 | Query::Construct { pattern, .. }
1314 | Query::Describe { pattern, .. } => pattern,
1315 };
1316 let mut in_scope = HashSet::new();
1317 pattern.on_in_scope_variable(|variable| {
1318 in_scope.insert(variable.as_str());
1319 });
1320 let projection_prebound: HashSet<&str> = prebound
1321 .iter()
1322 .copied()
1323 .filter(|name| matches!(*name, "this" | "value" | "PATH") && in_scope.contains(name))
1324 .collect();
1325
1326 fn visit(
1327 pattern: &GraphPattern,
1328 prebound: &HashSet<&str>,
1329 projection_prebound: &HashSet<&str>,
1330 seen_top_projection: bool,
1331 ) -> Result<(), String> {
1332 match pattern {
1333 GraphPattern::Minus { .. } => {
1334 return Err("SHACL-SPARQL queries must not contain MINUS".into());
1335 }
1336 GraphPattern::Service { .. } => {
1337 return Err("SHACL-SPARQL queries must not contain SERVICE".into());
1338 }
1339 GraphPattern::Values { .. } => {
1340 return Err("SHACL-SPARQL queries must not contain VALUES".into());
1341 }
1342 GraphPattern::Extend {
1343 inner, variable, ..
1344 } => {
1345 if prebound.contains(variable.as_str()) {
1346 return Err(format!(
1347 "SHACL-SPARQL must not assign potentially pre-bound variable ?{} with AS",
1348 variable.as_str()
1349 ));
1350 }
1351 visit(inner, prebound, projection_prebound, seen_top_projection)?;
1352 }
1353 GraphPattern::Project { inner, variables } => {
1354 if seen_top_projection {
1355 let projected: HashSet<&str> = variables.iter().map(|v| v.as_str()).collect();
1356 let mut missing: Vec<&str> = projection_prebound
1357 .iter()
1358 .copied()
1359 .filter(|name| !projected.contains(name))
1360 .collect();
1361 missing.sort_unstable();
1362 if !missing.is_empty() {
1363 return Err(format!(
1364 "SHACL-SPARQL subquery does not project potentially pre-bound variable(s): {}",
1365 missing
1366 .into_iter()
1367 .map(|name| format!("?{name}"))
1368 .collect::<Vec<_>>()
1369 .join(", ")
1370 ));
1371 }
1372 }
1373 visit(inner, prebound, projection_prebound, true)?;
1374 }
1375 GraphPattern::Join { left, right }
1376 | GraphPattern::LeftJoin { left, right, .. }
1377 | GraphPattern::Union { left, right }
1378 | GraphPattern::Lateral { left, right } => {
1379 visit(left, prebound, projection_prebound, seen_top_projection)?;
1380 visit(right, prebound, projection_prebound, seen_top_projection)?;
1381 }
1382 GraphPattern::Graph { inner, .. }
1383 | GraphPattern::Filter { inner, .. }
1384 | GraphPattern::OrderBy { inner, .. }
1385 | GraphPattern::Distinct { inner }
1386 | GraphPattern::Reduced { inner }
1387 | GraphPattern::Slice { inner, .. }
1388 | GraphPattern::Group { inner, .. } => {
1389 visit(inner, prebound, projection_prebound, seen_top_projection)?;
1390 }
1391 GraphPattern::Bgp { .. } | GraphPattern::Path { .. } => {}
1392 }
1393 Ok(())
1394 }
1395
1396 visit(pattern, &prebound, &projection_prebound, false)
1402}
1403
1404pub fn canonical_sparql_query(
1416 g: &Loaded,
1417 owner: &NamedOrBlankNode,
1418 raw: &str,
1419) -> Result<(Query, String), String> {
1420 let mut parser = SparqlParser::new();
1421 if let Some(base) = &g.base {
1422 parser = parser
1423 .with_base_iri(base)
1424 .map_err(|e| format!("invalid SPARQL base IRI: {e}"))?;
1425 }
1426 for (prefix, namespace) in &g.prefixes {
1427 parser = parser
1428 .with_prefix(prefix, namespace)
1429 .map_err(|e| format!("invalid SPARQL prefix declaration {prefix}: {e}"))?;
1430 }
1431 let mut prefix_sources: Vec<NamedOrBlankNode> = g
1432 .objects(owner, vocab::SH_PREFIXES)
1433 .iter()
1434 .filter_map(term_to_node)
1435 .collect();
1436 let mut seen_sources = HashSet::new();
1437 while let Some(source) = prefix_sources.pop() {
1438 if !seen_sources.insert(source.clone()) {
1439 continue;
1440 }
1441 prefix_sources.extend(
1442 g.objects(&source, vocab::OWL_IMPORTS)
1443 .iter()
1444 .filter_map(term_to_node),
1445 );
1446 for declaration_term in g.objects(&source, vocab::SH_DECLARE) {
1447 let Some(declaration) = term_to_node(&declaration_term) else {
1448 continue;
1449 };
1450 let (Some(Term::Literal(prefix)), Some(Term::Literal(namespace))) = (
1451 g.object(&declaration, vocab::SH_PREFIX),
1452 g.object(&declaration, vocab::SH_NAMESPACE),
1453 ) else {
1454 continue;
1455 };
1456 parser = parser
1457 .with_prefix(prefix.value(), namespace.value())
1458 .map_err(|e| format!("invalid SHACL SPARQL prefix declaration: {e}"))?;
1459 }
1460 }
1461 let query = parser
1462 .parse_query(raw)
1463 .map_err(|e| format!("invalid SPARQL query: {e}"))?;
1464 let canonical = query.to_string();
1465 Ok((query, canonical))
1466}
1467
1468#[derive(Clone, Copy, PartialEq, Eq)]
1469enum ExpectedQuery {
1470 Select,
1471 Ask,
1472 Construct,
1473 Describe,
1474}
1475
1476impl std::fmt::Display for ExpectedQuery {
1477 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1478 f.write_str(match self {
1479 Self::Select => "SELECT",
1480 Self::Ask => "ASK",
1481 Self::Construct => "CONSTRUCT",
1482 Self::Describe => "DESCRIBE",
1483 })
1484 }
1485}
1486
1487fn class_path() -> Path {
1488 Path::seq(vec![
1489 Path::Pred(vocab::rdf_type()),
1490 Path::star(Path::Pred(vocab::rdfs_subclassof())),
1491 ])
1492}
1493
1494fn node_expr_has_function(e: &NodeExpr) -> bool {
1499 match e {
1500 NodeExpr::Function { .. } => true,
1501 NodeExpr::Filter { input, .. } => node_expr_has_function(input),
1502 NodeExpr::Intersection(es) | NodeExpr::Union(es) => es.iter().any(node_expr_has_function),
1503 NodeExpr::This | NodeExpr::Constant(_) | NodeExpr::Path(_) => false,
1504 }
1505}
1506
1507fn map_node_kind(term: &Term) -> Option<NodeKindSet> {
1508 let Term::NamedNode(n) = term else {
1509 return None;
1510 };
1511 let r = n.as_ref();
1512 Some(if r == vocab::SH_IRI {
1513 NodeKindSet::IRI
1514 } else if r == vocab::SH_BLANK_NODE {
1515 NodeKindSet::BLANK_NODE
1516 } else if r == vocab::SH_LITERAL {
1517 NodeKindSet::LITERAL
1518 } else if r == vocab::SH_BLANK_NODE_OR_IRI {
1519 NodeKindSet::BLANK_NODE_OR_IRI
1520 } else if r == vocab::SH_BLANK_NODE_OR_LITERAL {
1521 NodeKindSet::BLANK_NODE_OR_LITERAL
1522 } else if r == vocab::SH_IRI_OR_LITERAL {
1523 NodeKindSet::IRI_OR_LITERAL
1524 } else {
1525 return None;
1526 })
1527}