1use crate::diagnostics::{DiagLevel, Diagnostic};
10use crate::graph::{Loaded, term_to_node};
11use crate::path::parse_path;
12use crate::vocab;
13use oxrdf::{Literal, NamedNode, NamedOrBlankNode, Term};
14use shifty_algebra::{
15 Bound, NodeExpr, NodeKindSet, Path, Rule, RuleHead, Schema, Selector, Severity, Shape,
16 ShapeArena, ShapeId, SparqlConstraint, SparqlConstruct, SparqlQueryKind, SparqlTarget,
17 Statement, ValueType,
18};
19use spargebra::{Query, SparqlParser};
20use std::collections::{BTreeSet, HashMap, HashSet};
21
22pub struct Lowered {
23 pub schema: Schema,
24 pub diagnostics: Vec<Diagnostic>,
25}
26
27pub fn lower(g: &Loaded) -> Lowered {
29 let mut l = Lowerer {
30 g,
31 arena: ShapeArena::new(),
32 cache: HashMap::new(),
33 statements: Vec::new(),
34 rules: Vec::new(),
35 diags: Vec::new(),
36 };
37 let shapes = l.discover_shapes();
38 for s in &shapes {
39 l.lower_shape(s);
40 }
41 l.diagnose_custom_components(&shapes);
42 for s in &shapes {
43 let selectors = l.target_selectors(s);
45 if let Some(shape) = l.cache.get(s).copied() {
46 for sel in &selectors {
47 l.statements.push(Statement {
48 selector: sel.clone(),
49 shape,
50 });
51 }
52 }
53 l.parse_rules(s, &selectors);
54 }
55 let names = l
56 .cache
57 .iter()
58 .filter_map(|(node, id)| match node {
59 NamedOrBlankNode::NamedNode(n) => Some((*id, n.as_str().to_string())),
60 NamedOrBlankNode::BlankNode(_) => None,
61 })
62 .collect();
63 let schema = Schema {
64 arena: l.arena,
65 statements: l.statements,
66 rules: l.rules,
67 names,
68 };
69 schema.arena.debug_assert_finalized();
70 Lowered {
71 schema,
72 diagnostics: l.diags,
73 }
74}
75
76struct Lowerer<'a> {
77 g: &'a Loaded,
78 arena: ShapeArena,
79 cache: HashMap<NamedOrBlankNode, ShapeId>,
80 statements: Vec<Statement>,
81 rules: Vec<Rule>,
82 diags: Vec<Diagnostic>,
83}
84
85impl Lowerer<'_> {
86 fn diag(&mut self, level: DiagLevel, msg: impl Into<String>, subj: &NamedOrBlankNode) {
87 self.diags
88 .push(Diagnostic::new(level, msg, Some(subj.to_string())));
89 }
90
91 fn discover_shapes(&self) -> Vec<NamedOrBlankNode> {
95 let mut found: HashSet<NamedOrBlankNode> = HashSet::new();
96 for triple in self.g.graph.iter() {
97 let p = triple.predicate;
98 let is_target = p == vocab::SH_TARGET_NODE
99 || p == vocab::SH_TARGET_CLASS
100 || p == vocab::SH_TARGET_SUBJECTS_OF
101 || p == vocab::SH_TARGET_OBJECTS_OF
102 || p == vocab::SH_TARGET;
103 if p == vocab::SH_PATH || p == vocab::SH_SPARQL || p == vocab::SH_RULE || is_target {
104 found.insert(triple.subject.into_owned());
105 }
106 if p == vocab::RDF_TYPE
107 && let Term::NamedNode(ty) = triple.object.into_owned()
108 && (ty.as_ref() == vocab::SH_NODE_SHAPE || ty.as_ref() == vocab::SH_PROPERTY_SHAPE)
109 {
110 found.insert(triple.subject.into_owned());
111 }
112 }
113 let mut shapes: Vec<NamedOrBlankNode> = found.into_iter().collect();
114 shapes.sort_by_key(|n| n.to_string());
115 shapes
116 }
117
118 fn lower_shape(&mut self, s: &NamedOrBlankNode) -> ShapeId {
119 if let Some(id) = self.cache.get(s) {
120 return *id;
121 }
122 let id = self.arena.reserve();
123 self.cache.insert(s.clone(), id);
124
125 if self.bool_prop(s, vocab::SH_DEACTIVATED) {
126 self.arena.set(id, Shape::Top);
127 return id;
128 }
129
130 let path = self.parse_shape_path(s);
131 let mut conjuncts: Vec<ShapeId> = Vec::new();
132
133 let value = self.collect_value_constraints(s);
136 if !value.is_empty() {
137 let value_phi = self.arena.and(value);
138 match &path {
139 Some(p) => {
140 let neg = self.arena.not(value_phi);
142 let c = self.arena.count(p.clone(), None, Some(0), neg);
143 conjuncts.push(c);
144 }
145 None => conjuncts.push(value_phi),
146 }
147 }
148
149 self.collect_path_constraints(s, path.as_ref(), &mut conjuncts);
150
151 if self.bool_prop(s, vocab::SH_CLOSED) {
152 let q = self.closed_allowed(s);
153 let c = self.arena.insert(Shape::Closed(q));
154 conjuncts.push(c);
155 }
156
157 for constraint_term in self.g.objects(s, vocab::SH_SPARQL) {
158 let Some(constraint_node) = term_to_node(&constraint_term) else {
159 self.diag(DiagLevel::Error, "sh:sparql must reference a resource", s);
160 continue;
161 };
162 let parsed = if let Some(Term::Literal(query)) =
163 self.g.object(&constraint_node, vocab::SH_SELECT)
164 {
165 self.canonical_sparql(&constraint_node, query.value(), ExpectedQuery::Select)
166 .map(|query| (SparqlQueryKind::Select, query))
167 } else if let Some(Term::Literal(query)) =
168 self.g.object(&constraint_node, vocab::SH_ASK)
169 {
170 self.canonical_sparql(&constraint_node, query.value(), ExpectedQuery::Ask)
171 .map(|query| (SparqlQueryKind::Ask, query))
172 } else {
173 self.diag(
174 DiagLevel::Error,
175 "sh:sparql constraint requires sh:select or sh:ask",
176 &constraint_node,
177 );
178 None
179 };
180 if let Some((kind, query)) = parsed {
181 let shape = Some(match s {
182 NamedOrBlankNode::NamedNode(n) => Term::NamedNode(n.clone()),
183 NamedOrBlankNode::BlankNode(b) => Term::BlankNode(b.clone()),
184 });
185 let mut messages: Vec<Term> = self.g.objects(&constraint_node, vocab::SH_MESSAGE);
188 if messages.is_empty() {
189 messages = self.g.objects(s, vocab::SH_MESSAGE);
190 }
191 let constraint = SparqlConstraint {
192 kind,
193 query,
194 path: path.clone(),
195 shape,
196 messages,
197 };
198 conjuncts.push(self.arena.insert(Shape::Sparql(constraint)));
199 }
200 }
201
202 for expr_term in self.g.objects(s, vocab::SH_EXPRESSION) {
206 if let Some(expr) = self.parse_node_expr(expr_term, s) {
207 if node_expr_has_function(&expr) {
208 self.diag(
212 DiagLevel::Unsupported,
213 "sh:expression with a function call is not yet evaluated",
214 s,
215 );
216 } else {
217 conjuncts.push(self.arena.insert(Shape::Expression(expr)));
218 }
219 }
220 }
221
222 let body = if conjuncts.is_empty() {
223 self.arena.top()
224 } else if conjuncts.len() == 1 {
225 if conjuncts[0] == id {
226 self.arena.top()
227 } else {
228 conjuncts[0]
229 }
230 } else {
231 self.arena.insert(Shape::And(conjuncts))
232 };
233 self.arena.set(
234 id,
235 Shape::Annotated {
236 severity: self.severity(s),
237 shape: body,
238 },
239 );
240 id
241 }
242
243 fn severity(&self, shape: &NamedOrBlankNode) -> Severity {
244 match self.g.object(shape, vocab::SH_SEVERITY) {
245 Some(Term::NamedNode(value)) => Severity::from_named_node(value),
246 _ => Severity::Violation,
247 }
248 }
249
250 fn collect_value_constraints(&mut self, s: &NamedOrBlankNode) -> Vec<ShapeId> {
251 let mut value: Vec<ShapeId> = Vec::new();
252
253 for c in self.g.objects(s, vocab::SH_CLASS) {
255 let tn = self.arena.insert(Shape::TestConst(c));
256 let cc = self.arena.count(class_path(), Some(1), None, tn);
257 value.push(cc);
258 }
259
260 for d in self.g.objects(s, vocab::SH_DATATYPE) {
262 if let Term::NamedNode(n) = d {
263 let id = self.arena.insert(Shape::TestType(ValueType::Datatype(n)));
264 value.push(id);
265 }
266 }
267
268 for k in self.g.objects(s, vocab::SH_NODE_KIND) {
270 if let Some(set) = map_node_kind(&k) {
271 let id = self.arena.insert(Shape::TestKind(set));
272 value.push(id);
273 } else {
274 self.diag(DiagLevel::Warning, "unrecognized sh:nodeKind value", s);
275 }
276 }
277
278 let lo = self
280 .lit(s, vocab::SH_MIN_INCLUSIVE)
281 .map(|value| Bound {
282 value,
283 inclusive: true,
284 })
285 .or_else(|| {
286 self.lit(s, vocab::SH_MIN_EXCLUSIVE).map(|value| Bound {
287 value,
288 inclusive: false,
289 })
290 });
291 let hi = self
292 .lit(s, vocab::SH_MAX_INCLUSIVE)
293 .map(|value| Bound {
294 value,
295 inclusive: true,
296 })
297 .or_else(|| {
298 self.lit(s, vocab::SH_MAX_EXCLUSIVE).map(|value| Bound {
299 value,
300 inclusive: false,
301 })
302 });
303 if lo.is_some() || hi.is_some() {
304 let id = self
305 .arena
306 .insert(Shape::TestType(ValueType::NumericRange { lo, hi }));
307 value.push(id);
308 }
309
310 let min_len = self.int(s, vocab::SH_MIN_LENGTH);
312 let max_len = self.int(s, vocab::SH_MAX_LENGTH);
313 if min_len.is_some() || max_len.is_some() {
314 let id = self.arena.insert(Shape::TestType(ValueType::Length {
315 min: min_len,
316 max: max_len,
317 }));
318 value.push(id);
319 }
320
321 let flags = self
323 .lit(s, vocab::SH_FLAGS)
324 .map(|l| l.value().to_string())
325 .unwrap_or_default();
326 for pat in self.g.objects(s, vocab::SH_PATTERN) {
327 if let Term::Literal(l) = pat {
328 let id = self.arena.insert(Shape::TestType(ValueType::Pattern {
329 regex: l.value().to_string(),
330 flags: flags.clone(),
331 }));
332 value.push(id);
333 }
334 }
335
336 for li in self.g.objects(s, vocab::SH_LANGUAGE_IN) {
338 let langs: Vec<String> = self
339 .g
340 .read_list(&li)
341 .into_iter()
342 .filter_map(|m| match m {
343 Term::Literal(l) => Some(l.value().to_string()),
344 _ => None,
345 })
346 .collect();
347 let id = self.arena.insert(Shape::TestType(ValueType::LangIn(langs)));
348 value.push(id);
349 }
350
351 for inl in self.g.objects(s, vocab::SH_IN) {
353 let alts: Vec<ShapeId> = self
354 .g
355 .read_list(&inl)
356 .into_iter()
357 .map(|m| self.arena.insert(Shape::TestConst(m)))
358 .collect();
359 let or = self.arena.or(alts);
360 value.push(or);
361 }
362
363 for n in self.g.objects(s, vocab::SH_NODE) {
365 if let Some(nn) = term_to_node(&n) {
366 let id = self.lower_shape(&nn);
367 value.push(id);
368 }
369 }
370
371 for prop in self.g.objects(s, vocab::SH_PROPERTY) {
375 if let Some(pn) = term_to_node(&prop) {
376 let id = self.lower_shape(&pn);
377 value.push(id);
378 }
379 }
380
381 for n in self.g.objects(s, vocab::SH_NOT) {
383 if let Some(nn) = term_to_node(&n) {
384 let id = self.lower_shape(&nn);
385 let neg = self.arena.not(id);
386 value.push(neg);
387 }
388 }
389
390 for l in self.g.objects(s, vocab::SH_AND) {
392 let ids = self.lower_shape_list(&l);
393 let a = self.arena.and(ids);
394 value.push(a);
395 }
396 for l in self.g.objects(s, vocab::SH_OR) {
397 let ids = self.lower_shape_list(&l);
398 let o = self.arena.or(ids);
399 value.push(o);
400 }
401 for l in self.g.objects(s, vocab::SH_XONE) {
402 let ids = self.lower_shape_list(&l);
403 let x = self.arena.xone(ids);
404 value.push(x);
405 }
406
407 value
408 }
409
410 fn collect_path_constraints(
414 &mut self,
415 s: &NamedOrBlankNode,
416 path: Option<&Path>,
417 conjuncts: &mut Vec<ShapeId>,
418 ) {
419 let need_path = |me: &mut Self, what: &str| {
420 me.diag(DiagLevel::Warning, format!("{what} ignored: no sh:path"), s);
421 };
422
423 let min_count = self.int(s, vocab::SH_MIN_COUNT);
424 let max_count = self.int(s, vocab::SH_MAX_COUNT);
425 if min_count.is_some() || max_count.is_some() {
426 match path {
427 Some(p) => {
428 let top = self.arena.top();
429 let c = self.arena.count(p.clone(), min_count, max_count, top);
430 conjuncts.push(c);
431 }
432 None => need_path(self, "sh:minCount/sh:maxCount"),
433 }
434 }
435
436 for v in self.g.objects(s, vocab::SH_HAS_VALUE) {
438 match path {
439 Some(p) => {
440 let tc = self.arena.insert(Shape::TestConst(v));
441 let c = self.arena.count(p.clone(), Some(1), None, tc);
442 conjuncts.push(c);
443 }
444 None => {
445 let tc = self.arena.insert(Shape::TestConst(v));
446 conjuncts.push(tc);
447 }
448 }
449 }
450
451 for q in self.g.objects(s, vocab::SH_QUALIFIED_VALUE_SHAPE) {
453 if let Some(qn) = term_to_node(&q) {
454 let qmin = self.int(s, vocab::SH_QUALIFIED_MIN_COUNT);
455 let qmax = self.int(s, vocab::SH_QUALIFIED_MAX_COUNT);
456 match path {
457 Some(p) => {
458 let mut qualifiers = vec![self.lower_shape(&qn)];
459 if self.bool_prop(s, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
460 for sibling in self.sibling_qualified_shapes(s, &qn) {
461 let sibling = self.lower_shape(&sibling);
462 qualifiers.push(self.arena.not(sibling));
463 }
464 }
465 let qualifier = self.arena.and(qualifiers);
466 let c = self.arena.count(p.clone(), qmin, qmax, qualifier);
467 conjuncts.push(c);
468 }
469 None => need_path(self, "sh:qualifiedValueShape"),
470 }
471 }
472 }
473
474 let pairs = [
476 (vocab::SH_EQUALS, "equals"),
477 (vocab::SH_DISJOINT, "disjoint"),
478 (vocab::SH_LESS_THAN, "lessThan"),
479 (vocab::SH_LESS_THAN_OR_EQUALS, "lessThanOrEquals"),
480 ];
481 for (pred, name) in pairs {
482 for other in self.g.objects(s, pred) {
483 let Term::NamedNode(op) = other else { continue };
484 match path {
485 Some(p) => {
486 let shape = match name {
487 "equals" => Shape::Eq(p.clone(), op),
488 "disjoint" => Shape::Disj(p.clone(), op),
489 "lessThan" => Shape::Lt(p.clone(), op),
490 _ => Shape::Le(p.clone(), op),
491 };
492 let c = self.arena.insert(shape);
493 conjuncts.push(c);
494 }
495 None if matches!(name, "equals" | "disjoint") => {
496 let shape = if name == "equals" {
497 Shape::Eq(Path::Id, op)
498 } else {
499 Shape::Disj(Path::Id, op)
500 };
501 let c = self.arena.insert(shape);
502 conjuncts.push(c);
503 }
504 None => need_path(self, &format!("sh:{name}")),
505 }
506 }
507 }
508
509 if self.bool_prop(s, vocab::SH_UNIQUE_LANG) {
511 match path {
512 Some(p) => {
513 let c = self.arena.insert(Shape::UniqueLang(p.clone()));
514 conjuncts.push(c);
515 }
516 None => need_path(self, "sh:uniqueLang"),
517 }
518 }
519 }
520
521 fn target_selectors(&mut self, s: &NamedOrBlankNode) -> Vec<Selector> {
523 let mut sels = Vec::new();
524
525 for c in self.g.objects(s, vocab::SH_TARGET_NODE) {
526 sels.push(Selector::IsConst(c));
527 }
528 for c in self.g.objects(s, vocab::SH_TARGET_CLASS) {
529 sels.push(self.class_selector(c));
530 }
531 for p in self.g.objects(s, vocab::SH_TARGET_SUBJECTS_OF) {
532 if let Term::NamedNode(n) = p {
533 sels.push(Selector::HasOut(n));
534 }
535 }
536 for p in self.g.objects(s, vocab::SH_TARGET_OBJECTS_OF) {
537 if let Term::NamedNode(n) = p {
538 sels.push(Selector::HasIn(n));
539 }
540 }
541
542 if (self.g.is_instance_of(s, vocab::RDFS_CLASS)
544 || self.g.is_instance_of(s, vocab::OWL_CLASS))
545 && let NamedOrBlankNode::NamedNode(n) = s
546 {
547 sels.push(self.class_selector(Term::NamedNode(n.clone())));
548 }
549
550 for target_term in self.g.objects(s, vocab::SH_TARGET) {
551 let Some(target_node) = term_to_node(&target_term) else {
552 self.diag(DiagLevel::Error, "sh:target must reference a resource", s);
553 continue;
554 };
555 match self.g.object(&target_node, vocab::SH_SELECT) {
556 Some(Term::Literal(query)) => {
557 if let Some(query) =
558 self.canonical_sparql(&target_node, query.value(), ExpectedQuery::Select)
559 {
560 sels.push(Selector::Sparql(SparqlTarget { query }));
561 }
562 }
563 _ => self.diag(
564 DiagLevel::Unsupported,
565 "custom sh:target without sh:select is not yet lowered",
566 &target_node,
567 ),
568 }
569 }
570
571 sels
572 }
573
574 fn diagnose_custom_components(&mut self, shapes: &[NamedOrBlankNode]) {
580 let mut components: Vec<(NamedNode, Vec<NamedNode>)> = Vec::new();
582 let mut seen = HashSet::new();
583 for triple in self.g.graph.triples_for_predicate(vocab::SH_PARAMETER) {
584 let subject = triple.subject.into_owned();
585 if !seen.insert(subject.clone()) {
586 continue;
587 }
588 let NamedOrBlankNode::NamedNode(iri) = &subject else {
589 continue;
590 };
591 let has_validator = self.g.object(&subject, vocab::SH_VALIDATOR).is_some()
592 || self.g.object(&subject, vocab::SH_NODE_VALIDATOR).is_some()
593 || self
594 .g
595 .object(&subject, vocab::SH_PROPERTY_VALIDATOR)
596 .is_some();
597 if !has_validator {
598 continue; }
600 let mut mandatory = Vec::new();
601 for p in self.g.objects(&subject, vocab::SH_PARAMETER) {
602 let Some(pn) = term_to_node(&p) else { continue };
603 let optional = matches!(self.g.object(&pn, vocab::SH_OPTIONAL),
604 Some(Term::Literal(l)) if l.value() == "true");
605 if !optional && let Some(Term::NamedNode(path)) = self.g.object(&pn, vocab::SH_PATH)
606 {
607 mandatory.push(path);
608 }
609 }
610 components.push((iri.clone(), mandatory));
611 }
612 if components.is_empty() {
613 return;
614 }
615 for s in shapes {
616 for (iri, mandatory) in &components {
617 if mandatory
618 .iter()
619 .all(|path| self.g.object(s, path.as_ref()).is_some())
620 {
621 self.diag(
622 DiagLevel::Unsupported,
623 format!(
624 "shape activates custom constraint component <{}>, evaluated only on \
625 the report path (validate_report), not in the algebra validator",
626 iri.as_str()
627 ),
628 s,
629 );
630 }
631 }
632 }
633 }
634
635 fn parse_rules(&mut self, s: &NamedOrBlankNode, selectors: &[Selector]) {
638 for rule_term in self.g.objects(s, vocab::SH_RULE) {
639 let Some(rn) = term_to_node(&rule_term) else {
640 continue;
641 };
642 let Some(head) = self.parse_rule_head(&rn) else {
643 continue;
644 };
645
646 let conditions: Vec<ShapeId> = self
647 .g
648 .objects(&rn, vocab::SH_CONDITION)
649 .iter()
650 .filter_map(term_to_node)
651 .map(|c| self.lower_shape(&c))
652 .collect();
653 let order = self.order(&rn);
654 let deactivated = self.bool_prop(&rn, vocab::SH_DEACTIVATED);
655
656 for sel in selectors {
657 self.rules.push(Rule {
658 selector: sel.clone(),
659 conditions: conditions.clone(),
660 head: head.clone(),
661 order,
662 deactivated,
663 });
664 }
665 }
666 }
667
668 fn sibling_qualified_shapes(
671 &self,
672 shape: &NamedOrBlankNode,
673 qualifier: &NamedOrBlankNode,
674 ) -> Vec<NamedOrBlankNode> {
675 let mut siblings = HashSet::new();
676 for triple in self.g.graph.triples_for_predicate(vocab::SH_PROPERTY) {
677 if term_to_node(&triple.object.into_owned()).as_ref() != Some(shape) {
678 continue;
679 }
680 let parent = triple.subject.into_owned();
681 for property in self.g.objects(&parent, vocab::SH_PROPERTY) {
682 let Some(property) = term_to_node(&property) else {
683 continue;
684 };
685 for sibling in self.g.objects(&property, vocab::SH_QUALIFIED_VALUE_SHAPE) {
686 if let Some(sibling) = term_to_node(&sibling) {
687 siblings.insert(sibling);
688 }
689 }
690 }
691 }
692 siblings.remove(qualifier);
693 let mut siblings: Vec<_> = siblings.into_iter().collect();
694 siblings.sort_by_key(|node| node.to_string());
695 siblings
696 }
697
698 fn parse_rule_head(&mut self, rn: &NamedOrBlankNode) -> Option<RuleHead> {
699 if let Some(Term::Literal(q)) = self.g.object(rn, vocab::SH_CONSTRUCT) {
702 let query = self.canonical_sparql(rn, q.value(), ExpectedQuery::Construct)?;
703 return Some(RuleHead::Sparql(SparqlConstruct { query }));
704 }
705 let (subj, pred, obj) = (
707 self.g.object(rn, vocab::SH_SUBJECT),
708 self.g.object(rn, vocab::SH_PREDICATE),
709 self.g.object(rn, vocab::SH_OBJECT),
710 );
711 if subj.is_none() && pred.is_none() && obj.is_none() {
712 self.diag(DiagLevel::Unsupported, "unrecognized sh:rule head", rn);
713 return None;
714 }
715 let (Some(subj), Some(pred), Some(obj)) = (subj, pred, obj) else {
716 self.diag(
717 DiagLevel::Error,
718 "sh:TripleRule missing subject/predicate/object",
719 rn,
720 );
721 return None;
722 };
723 Some(RuleHead::Triple {
724 subject: self.parse_node_expr(subj, rn)?,
725 predicate: self.parse_node_expr(pred, rn)?,
726 object: self.parse_node_expr(obj, rn)?,
727 })
728 }
729
730 fn parse_node_expr(&mut self, term: Term, owner: &NamedOrBlankNode) -> Option<NodeExpr> {
734 match &term {
735 Term::NamedNode(n) if n.as_ref() == vocab::SH_THIS => Some(NodeExpr::This),
736 Term::NamedNode(_) | Term::Literal(_) => Some(NodeExpr::Constant(term)),
737 Term::BlankNode(_) => {
738 let node = term_to_node(&term).expect("blank node");
739 if let Some(path_term) = self.g.object(&node, vocab::SH_PATH) {
740 match parse_path(self.g, &path_term) {
741 Ok(path) => Some(NodeExpr::Path(path)),
742 Err(e) => {
743 self.diag(
744 DiagLevel::Error,
745 format!("invalid node-expression path: {e}"),
746 owner,
747 );
748 None
749 }
750 }
751 } else if self.g.object(&node, vocab::SH_FILTER_SHAPE).is_some() {
752 self.parse_filter_expr(&node, owner)
753 } else if let Some(list) = self.g.object(&node, vocab::SH_INTERSECTION) {
754 self.parse_set_expr(&list, owner)
755 .map(NodeExpr::Intersection)
756 } else if let Some(list) = self.g.object(&node, vocab::SH_UNION) {
757 self.parse_set_expr(&list, owner).map(NodeExpr::Union)
758 } else if let Some(expr) = self.try_function_call(&node, owner) {
759 Some(expr)
760 } else {
761 self.diag(
762 DiagLevel::Unsupported,
763 "complex node expression not yet lowered",
764 owner,
765 );
766 None
767 }
768 }
769 }
770 }
771
772 fn parse_filter_expr(
775 &mut self,
776 node: &NamedOrBlankNode,
777 owner: &NamedOrBlankNode,
778 ) -> Option<NodeExpr> {
779 let Some(shape_term) = self.g.object(node, vocab::SH_FILTER_SHAPE) else {
780 self.diag(DiagLevel::Error, "sh:filterShape missing a value", owner);
781 return None;
782 };
783 let Some(shape_node) = term_to_node(&shape_term) else {
784 self.diag(
785 DiagLevel::Error,
786 "sh:filterShape must reference a shape",
787 owner,
788 );
789 return None;
790 };
791 let Some(nodes_term) = self.g.object(node, vocab::SH_NODES) else {
792 self.diag(
793 DiagLevel::Error,
794 "filter expression missing sh:nodes",
795 owner,
796 );
797 return None;
798 };
799 let input = self.parse_node_expr(nodes_term, owner)?;
800 let shape = self.lower_shape(&shape_node);
801 Some(NodeExpr::Filter {
802 input: Box::new(input),
803 shape,
804 })
805 }
806
807 fn parse_set_expr(
811 &mut self,
812 list_head: &Term,
813 owner: &NamedOrBlankNode,
814 ) -> Option<Vec<NodeExpr>> {
815 let members = self.g.read_list(list_head);
816 if members.is_empty() {
817 self.diag(
818 DiagLevel::Error,
819 "sh:intersection/sh:union expects a non-empty list",
820 owner,
821 );
822 return None;
823 }
824 let n = members.len();
825 let exprs: Vec<NodeExpr> = members
826 .into_iter()
827 .filter_map(|t| self.parse_node_expr(t, owner))
828 .collect();
829 if exprs.len() != n {
830 return None;
831 }
832 Some(exprs)
833 }
834
835 fn try_function_call(
840 &mut self,
841 node: &NamedOrBlankNode,
842 owner: &NamedOrBlankNode,
843 ) -> Option<NodeExpr> {
844 let func_preds: Vec<(NamedNode, Term)> = self
845 .g
846 .graph
847 .triples_for_subject(node)
848 .map(|t| (t.predicate.into_owned(), t.object.into_owned()))
849 .filter(|(p, _)| {
850 let s = p.as_str();
851 !s.starts_with(vocab::SH)
852 && !s.starts_with(vocab::RDF)
853 && !s.starts_with(vocab::RDFS)
854 && !s.starts_with(vocab::OWL)
855 })
856 .collect();
857
858 if func_preds.len() != 1 {
859 return None;
860 }
861 let (func_iri, list_head) = func_preds.into_iter().next().unwrap();
862 let arg_terms = self.g.read_list(&list_head);
863 let n = arg_terms.len();
864 let args: Vec<NodeExpr> = arg_terms
865 .into_iter()
866 .filter_map(|t| self.parse_node_expr(t, owner))
867 .collect();
868 if args.len() != n {
869 return None;
870 }
871 Some(NodeExpr::Function {
872 iri: func_iri,
873 args,
874 })
875 }
876
877 fn order(&self, s: &NamedOrBlankNode) -> Option<i64> {
878 match self.g.object(s, vocab::SH_ORDER) {
879 Some(Term::Literal(l)) => l.value().parse().ok(),
880 _ => None,
881 }
882 }
883
884 fn class_selector(&mut self, class: Term) -> Selector {
886 let tn = self.arena.insert(Shape::TestConst(class));
887 Selector::HasPath(class_path(), tn)
888 }
889
890 fn lower_shape_list(&mut self, list_head: &Term) -> Vec<ShapeId> {
891 self.g
892 .read_list(list_head)
893 .into_iter()
894 .filter_map(|m| term_to_node(&m))
895 .map(|n| self.lower_shape(&n))
896 .collect()
897 }
898
899 fn parse_shape_path(&mut self, s: &NamedOrBlankNode) -> Option<Path> {
900 let term = self.g.object(s, vocab::SH_PATH)?;
901 match parse_path(self.g, &term) {
902 Ok(p) => Some(p),
903 Err(e) => {
904 self.diag(DiagLevel::Error, format!("invalid sh:path: {e}"), s);
905 None
906 }
907 }
908 }
909
910 fn closed_allowed(&self, s: &NamedOrBlankNode) -> BTreeSet<oxrdf::NamedNode> {
911 let mut q = BTreeSet::new();
912 for prop in self.g.objects(s, vocab::SH_PROPERTY) {
913 if let Some(pn) = term_to_node(&prop)
914 && let Some(Term::NamedNode(n)) = self.g.object(&pn, vocab::SH_PATH)
915 {
916 q.insert(n);
917 }
918 }
919 for ip in self.g.objects(s, vocab::SH_IGNORED_PROPERTIES) {
920 for m in self.g.read_list(&ip) {
921 if let Term::NamedNode(n) = m {
922 q.insert(n);
923 }
924 }
925 }
926 q
927 }
928
929 fn bool_prop(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> bool {
930 matches!(self.g.object(s, pred), Some(Term::Literal(l)) if l.value() == "true")
931 }
932
933 fn int(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> Option<u64> {
934 match self.g.object(s, pred) {
935 Some(Term::Literal(l)) => l.value().parse().ok(),
936 _ => None,
937 }
938 }
939
940 fn lit(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> Option<Literal> {
941 match self.g.object(s, pred) {
942 Some(Term::Literal(l)) => Some(l),
943 _ => None,
944 }
945 }
946
947 fn canonical_sparql(
951 &mut self,
952 owner: &NamedOrBlankNode,
953 raw: &str,
954 expected: ExpectedQuery,
955 ) -> Option<String> {
956 let (query, canonical) = match canonical_sparql_query(self.g, owner, raw) {
957 Ok(result) => result,
958 Err(message) => {
959 self.diag(DiagLevel::Error, message, owner);
960 return None;
961 }
962 };
963 let actual = match &query {
964 Query::Select { .. } => ExpectedQuery::Select,
965 Query::Ask { .. } => ExpectedQuery::Ask,
966 Query::Construct { .. } => ExpectedQuery::Construct,
967 Query::Describe { .. } => ExpectedQuery::Describe,
968 };
969 if actual != expected {
970 self.diag(
971 DiagLevel::Error,
972 format!("expected SPARQL {expected}, found {actual}"),
973 owner,
974 );
975 return None;
976 }
977 Some(canonical)
978 }
979}
980
981pub fn canonical_sparql_query(
993 g: &Loaded,
994 owner: &NamedOrBlankNode,
995 raw: &str,
996) -> Result<(Query, String), String> {
997 let mut parser = SparqlParser::new();
998 if let Some(base) = &g.base {
999 parser = parser
1000 .with_base_iri(base)
1001 .map_err(|e| format!("invalid SPARQL base IRI: {e}"))?;
1002 }
1003 for (prefix, namespace) in &g.prefixes {
1004 parser = parser
1005 .with_prefix(prefix, namespace)
1006 .map_err(|e| format!("invalid SPARQL prefix declaration {prefix}: {e}"))?;
1007 }
1008 let mut prefix_sources: Vec<NamedOrBlankNode> = g
1009 .objects(owner, vocab::SH_PREFIXES)
1010 .iter()
1011 .filter_map(term_to_node)
1012 .collect();
1013 let mut seen_sources = HashSet::new();
1014 while let Some(source) = prefix_sources.pop() {
1015 if !seen_sources.insert(source.clone()) {
1016 continue;
1017 }
1018 prefix_sources.extend(
1019 g.objects(&source, vocab::OWL_IMPORTS)
1020 .iter()
1021 .filter_map(term_to_node),
1022 );
1023 for declaration_term in g.objects(&source, vocab::SH_DECLARE) {
1024 let Some(declaration) = term_to_node(&declaration_term) else {
1025 continue;
1026 };
1027 let (Some(Term::Literal(prefix)), Some(Term::Literal(namespace))) = (
1028 g.object(&declaration, vocab::SH_PREFIX),
1029 g.object(&declaration, vocab::SH_NAMESPACE),
1030 ) else {
1031 continue;
1032 };
1033 parser = parser
1034 .with_prefix(prefix.value(), namespace.value())
1035 .map_err(|e| format!("invalid SHACL SPARQL prefix declaration: {e}"))?;
1036 }
1037 }
1038 let query = parser
1039 .parse_query(raw)
1040 .map_err(|e| format!("invalid SPARQL query: {e}"))?;
1041 let canonical = query.to_string();
1042 Ok((query, canonical))
1043}
1044
1045#[derive(Clone, Copy, PartialEq, Eq)]
1046enum ExpectedQuery {
1047 Select,
1048 Ask,
1049 Construct,
1050 Describe,
1051}
1052
1053impl std::fmt::Display for ExpectedQuery {
1054 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1055 f.write_str(match self {
1056 Self::Select => "SELECT",
1057 Self::Ask => "ASK",
1058 Self::Construct => "CONSTRUCT",
1059 Self::Describe => "DESCRIBE",
1060 })
1061 }
1062}
1063
1064fn class_path() -> Path {
1065 Path::seq(vec![
1066 Path::Pred(vocab::rdf_type()),
1067 Path::star(Path::Pred(vocab::rdfs_subclassof())),
1068 ])
1069}
1070
1071fn node_expr_has_function(e: &NodeExpr) -> bool {
1076 match e {
1077 NodeExpr::Function { .. } => true,
1078 NodeExpr::Filter { input, .. } => node_expr_has_function(input),
1079 NodeExpr::Intersection(es) | NodeExpr::Union(es) => es.iter().any(node_expr_has_function),
1080 NodeExpr::This | NodeExpr::Constant(_) | NodeExpr::Path(_) => false,
1081 }
1082}
1083
1084fn map_node_kind(term: &Term) -> Option<NodeKindSet> {
1085 let Term::NamedNode(n) = term else {
1086 return None;
1087 };
1088 let r = n.as_ref();
1089 Some(if r == vocab::SH_IRI {
1090 NodeKindSet::IRI
1091 } else if r == vocab::SH_BLANK_NODE {
1092 NodeKindSet::BLANK_NODE
1093 } else if r == vocab::SH_LITERAL {
1094 NodeKindSet::LITERAL
1095 } else if r == vocab::SH_BLANK_NODE_OR_IRI {
1096 NodeKindSet::BLANK_NODE_OR_IRI
1097 } else if r == vocab::SH_BLANK_NODE_OR_LITERAL {
1098 NodeKindSet::BLANK_NODE_OR_LITERAL
1099 } else if r == vocab::SH_IRI_OR_LITERAL {
1100 NodeKindSet::IRI_OR_LITERAL
1101 } else {
1102 return None;
1103 })
1104}