1use std::collections::{HashMap, HashSet};
2
3use crate::expression::NodeExpression;
4use crate::functions::{Functions, NativeQuery};
5use bigdecimal::BigDecimal;
6use oxigraph::model::*;
7use spargebra::algebra::GraphPattern;
8use spargebra::{Query, SparqlParser};
9
10use crate::constraints::{self, Constraint};
11use crate::graph::*;
12use crate::path::Path;
13use crate::{Error, Result};
14use shacl_structural_model as model;
15
16#[cfg(test)]
17#[path = "compile_tests.rs"]
18mod tests;
19
20pub(crate) enum Target {
21 Node(Term),
22 Class(Term),
23 Subjects(NamedNode),
24 Objects(NamedNode),
25 Sparql {
26 query: Box<NativeQuery>,
27 bindings: Vec<(Variable, Term)>,
28 },
29}
30
31pub(crate) struct Rule {
32 pub node: Term,
33 pub order: BigDecimal,
34 pub deactivated: bool,
35 pub conditions: Vec<usize>,
36 pub queries: Vec<NativeQuery>,
37 pub triple: Option<[NodeExpression; 3]>,
38}
39
40pub(crate) struct Shape {
41 pub node: Term,
42 pub path: Option<Path>,
43 pub path_node: Option<Term>,
44 pub order: BigDecimal,
45 pub deactivated: bool,
46 pub targets: Vec<Target>,
47 pub constraints: Vec<Constraint>,
48 pub properties: Vec<usize>,
49 pub rules: Vec<Rule>,
50 pub messages: Vec<Term>,
51 pub severity: NamedNode,
52}
53
54pub struct CompiledShapes {
56 pub(crate) graph: Dataset,
57 pub(crate) shapes: Vec<Shape>,
58 pub(crate) execution_order: Vec<usize>,
59 pub(crate) functions: Functions,
60 pub(crate) namespaces: Vec<(String, String)>,
61}
62
63fn order(value: Option<Term>, node: &Term) -> Result<BigDecimal> {
64 match value {
65 None => Ok(BigDecimal::from(0)),
66 Some(Term::Literal(v))
67 if ["integer", "decimal", "float", "double"]
68 .iter()
69 .any(|t| v.datatype() == iri(XSD, t).as_ref()) =>
70 {
71 v.value()
72 .parse()
73 .map_err(|_| Error::Invalid(format!("Invalid sh:order on {node}")))
74 }
75 _ => Err(Error::Invalid(format!(
76 "sh:order must be numeric on {node}"
77 ))),
78 }
79}
80
81fn query(
82 store: &Dataset,
83 declaration: model::SparqlQuery<Term>,
84 construct: bool,
85 namespaces: &[(String, String)],
86) -> Result<NativeQuery> {
87 let Term::Literal(text) = declaration.text else {
88 return Err(Error::Invalid(
89 "SPARQL query must be a string literal".into(),
90 ));
91 };
92 if text.datatype() != iri(XSD, "string").as_ref() {
93 return Err(Error::Invalid("SPARQL query must be an xsd:string".into()));
94 }
95 let mut parsed = SparqlParser::new()
96 .parse_query(&format!(
97 "{}\n{}",
98 crate::sparql::prefixes(store, declaration.prefixes, namespaces)?,
99 text.value()
100 ))
101 .map_err(|e| Error::Invalid(format!("SPARQL parse error: {e}")))?;
102 if (construct && !matches!(parsed, Query::Construct { .. }))
103 || (!construct && !matches!(parsed, Query::Select { .. }))
104 {
105 return Err(Error::Invalid(format!(
106 "SPARQL query must be {}",
107 if construct { "CONSTRUCT" } else { "SELECT" }
108 )));
109 }
110 match &mut parsed {
111 Query::Select { pattern, .. } => {
112 let mut has_this = false;
113 pattern.on_in_scope_variable(|v| has_this |= v.as_str() == "this");
114 if !has_this {
115 return Err(Error::Invalid("SPARQL target must project ?this".into()));
116 }
117 }
118 Query::Construct { pattern, .. } => {
119 let mut variables = HashSet::new();
123 pattern.on_in_scope_variable(|v| {
124 variables.insert(v.clone());
125 });
126 variables.insert(Variable::new_unchecked("this"));
127 *pattern = GraphPattern::Project {
128 inner: Box::new(pattern.clone()),
129 variables: variables.into_iter().collect(),
130 };
131 }
132 _ => unreachable!("Query kind was checked above"),
133 }
134 Ok(NativeQuery::new(parsed))
135}
136
137fn lower_target(
138 graph: &Dataset,
139 target: model::Target<Term>,
140 namespaces: &[(String, String)],
141) -> Result<Target> {
142 use crate::structural::named_iri;
143 Ok(match target {
144 model::Target::Node(node) => Target::Node(node),
145 model::Target::Class(class) => Target::Class(named_iri(class).into()),
146 model::Target::ImplicitClass(class) => Target::Class(class),
147 model::Target::SubjectsOf(predicate) => Target::Subjects(named_iri(predicate)),
148 model::Target::ObjectsOf(predicate) => Target::Objects(named_iri(predicate)),
149 model::Target::Sparql(target) => Target::Sparql {
150 query: Box::new(query(graph, target.select, false, namespaces)?),
151 bindings: Vec::new(),
152 },
153 model::Target::Custom(target) => {
154 let definition = crate::structural::target_type(graph, &target.target_type)?;
155 let mut bindings = Vec::new();
156 for parameter in crate::functions::lower_parameters(definition.parameters)? {
157 let values = &target
158 .parameters
159 .iter()
160 .find(|p| p.path.as_str() == parameter.path.as_str())
161 .ok_or_else(|| {
162 Error::Invalid(format!("Missing target parameter {}", parameter.path))
163 })?
164 .values;
165 if values.len() > 1 || (values.is_empty() && !parameter.optional) {
166 return Err(Error::Invalid(format!(
167 "Target {} requires one value for {}",
168 target.id, parameter.path
169 )));
170 }
171 if let Some(value) = values.first() {
172 bindings.push((parameter.variable, value.clone()));
173 }
174 }
175 let mut parsed = query(graph, definition.select, false, namespaces)?.parsed;
176 crate::functions::project_bindings(
177 &mut parsed,
178 &bindings.iter().map(|(v, _)| v.clone()).collect::<Vec<_>>(),
179 );
180 Target::Sparql {
181 query: Box::new(NativeQuery::new(parsed)),
182 bindings,
183 }
184 }
185 })
186}
187
188fn lower_rule(
189 graph: &Dataset,
190 declaration: model::ShaclRule<Term>,
191 indices: &HashMap<Term, usize>,
192 namespaces: &[(String, String)],
193) -> Result<Rule> {
194 let mut queries = Vec::new();
195 let mut triple = None;
196 for kind in declaration.kinds {
197 match kind {
198 model::RuleKind::Sparql(body) => queries.push(query(graph, body, true, namespaces)?),
199 model::RuleKind::Triple(body) => {
200 if triple.is_some() {
201 return Err(Error::Invalid("Multiple TripleRule bodies".into()));
202 }
203 triple = Some([
204 NodeExpression::lower(body.subject, indices)?,
205 NodeExpression::lower(body.predicate, indices)?,
206 NodeExpression::lower(body.object, indices)?,
207 ]);
208 }
209 }
210 }
211 Ok(Rule {
212 order: order(declaration.order, &declaration.id)?,
213 node: declaration.id,
214 deactivated: declaration.deactivated,
215 conditions: declaration
216 .conditions
217 .iter()
218 .map(|n| {
219 indices
220 .get(n)
221 .copied()
222 .ok_or_else(|| Error::Invalid("Unknown condition shape".into()))
223 })
224 .collect::<Result<_>>()?,
225 queries,
226 triple,
227 })
228}
229
230fn check_vocabulary(
231 store: &Dataset,
232 advanced: bool,
233 executable_nodes: &HashSet<Term>,
234) -> Result<()> {
235 const ALLOWED: &[&str] = &[
238 "targetNode",
239 "targetClass",
240 "targetSubjectsOf",
241 "targetObjectsOf",
242 "target",
243 "select",
244 "prefixes",
245 "property",
246 "path",
247 "minCount",
248 "maxCount",
249 "hasValue",
250 "message",
251 "severity",
252 "deactivated",
253 "order",
254 "rule",
255 "condition",
256 "construct",
257 "name",
258 "description",
259 "group",
260 "defaultValue",
261 "nodeKind",
262 "optional",
263 "parameter",
264 "validator",
265 "nodeValidator",
266 "propertyValidator",
267 "ask",
268 "labelTemplate",
269 "label",
270 "keyParameter",
271 "returnType",
272 "subject",
273 "predicate",
274 "object",
275 "expression",
276 "filterShape",
277 "nodes",
278 "union",
279 "intersection",
280 ];
281 for quad in store.iter() {
282 let quad = quad.into_owned();
283 if advanced
285 && quad.predicate == iri(RDF, "type")
286 && let Term::NamedNode(ref n) = quad.object
287 && let Some(name) = n.as_str().strip_prefix(SH)
288 && ["Function", "SHACLFunction", "JSFunction"].contains(&name)
289 {
290 return Err(Error::Unsupported(format!("sh:{name}")));
291 }
292 if !executable_nodes.contains(&Term::from(quad.subject.clone())) {
295 continue;
296 }
297 if let Some(name) = quad.predicate.as_str().strip_prefix(SH) {
298 if !ALLOWED.contains(&name) && !constraints::PARAMETERS.contains(&name) {
299 return Err(Error::Unsupported(format!("sh:{name} on {}", quad.subject)));
300 }
301 if !advanced && ["target", "rule", "construct", "condition"].contains(&name) {
302 return Err(Error::Unsupported(format!(
303 "sh:{name} requires advanced=True"
304 )));
305 }
306 }
307 if quad.predicate == iri(RDF, "type")
308 && let Term::NamedNode(ref n) = quad.object
309 && let Some(name) = n.as_str().strip_prefix(SH)
310 && ![
311 "NodeShape",
312 "PropertyShape",
313 "SPARQLRule",
314 "SPARQLTarget",
315 "SPARQLConstraint",
316 "ConstraintComponent",
317 "Parameter",
318 "SPARQLAskValidator",
319 "SPARQLSelectValidator",
320 "SPARQLFunction",
321 "TripleRule",
322 "SPARQLTargetType",
323 "TargetType",
324 "SPARQLConstraintObject",
325 ]
326 .contains(&name)
327 {
328 return Err(Error::Unsupported(format!("sh:{name}")));
329 }
330 }
331 Ok(())
332}
333
334impl CompiledShapes {
335 pub fn shape_nodes(&self) -> impl Iterator<Item = &Term> {
337 self.shapes.iter().map(|shape| &shape.node)
338 }
339
340 pub fn compile(store: Dataset, advanced: bool) -> Result<Self> {
341 Self::compile_with_boolean_policy(store, advanced, true)
342 }
343
344 pub fn compile_with_boolean_policy(
345 store: Dataset,
346 advanced: bool,
347 numeric_booleans: bool,
348 ) -> Result<Self> {
349 Self::compile_with_namespaces(store, advanced, numeric_booleans, &[])
350 }
351
352 pub fn compile_with_namespaces(
353 mut store: Dataset,
354 advanced: bool,
355 numeric_booleans: bool,
356 namespaces: &[(String, String)],
357 ) -> Result<Self> {
358 let original = store.clone();
359 if !numeric_booleans {
360 let replaced: Vec<_> = store.iter().filter(|q| {
361 ["closed", "uniqueLang", "deactivated", "qualifiedValueShapesDisjoint", "optional"].iter().any(|p| q.predicate == iri(SH, p).as_ref())
362 && matches!(q.object, TermRef::Literal(l) if l.datatype() == iri(XSD, "boolean").as_ref() && l.value() == "1")
363 }).map(|q| q.into_owned()).collect();
364 for mut quad in replaced {
365 store.remove(&quad);
366 quad.object = Literal::from(false).into();
367 store.insert(&quad);
368 }
369 }
370 if store.iter().any(|q| !q.graph_name.is_default_graph()) {
371 return Err(Error::Unsupported("named shapes graphs".into()));
372 }
373 let mut paths = HashMap::new();
376 for q in store.quads_for_pattern(None, Some(iri(SH, "path").as_ref()), None, None) {
377 let node = q.object.into_owned();
378 if let std::collections::hash_map::Entry::Vacant(entry) = paths.entry(node.clone()) {
379 entry.insert(Path::read(&store, &node)?);
380 }
381 }
382 let components = crate::components::Component::discover(&store)?;
383 let functions = if advanced {
384 Functions::compile(&store, namespaces)?
385 } else {
386 Functions::default()
387 };
388 let mut nodes = HashSet::new();
389 for kind in ["NodeShape", "PropertyShape"] {
390 nodes.extend(subjects(
391 &store,
392 &iri(RDF, "type"),
393 Some(&iri(SH, kind).into()),
394 )?);
395 }
396 for name in [
397 "targetNode",
398 "targetClass",
399 "targetSubjectsOf",
400 "targetObjectsOf",
401 "target",
402 "rule",
403 ] {
404 nodes.extend(subjects(&store, &iri(SH, name), None)?);
405 }
406 let mut parameters = instances(&store, &iri(SH, "Parameter").into())?;
407 parameters.extend(
408 store
409 .quads_for_pattern(None, Some(iri(SH, "parameter").as_ref()), None, None)
410 .map(|q| q.object.into_owned()),
411 );
412 for name in ["property", "node"] {
413 nodes.extend(
418 subjects(&store, &iri(SH, name), None)?
419 .into_iter()
420 .filter(|n| !parameters.contains(n)),
421 );
422 }
423 nodes.extend(constraints::shape_references(&store)?);
424 for quad in store.quads_for_pattern(None, Some(iri(SH, "filterShape").as_ref()), None, None)
425 {
426 nodes.insert(quad.object.into_owned());
427 }
428 let mut executable_nodes = nodes.clone();
429 for node in &nodes {
430 for predicate in ["rule", "target", "sparql"] {
431 executable_nodes.extend(values(&store, node, predicate)?);
432 }
433 }
434 for component in &components {
435 executable_nodes.insert(component.node.clone().into());
436 executable_nodes.extend(
437 component
438 .parameters
439 .iter()
440 .map(|p| p.declaration.id.clone()),
441 );
442 executable_nodes.extend(component.validators.iter().cloned());
443 }
444 executable_nodes.extend(nodes.iter().cloned());
448 check_vocabulary(&store, advanced, &executable_nodes)?;
449 let mut nodes: Vec<_> = nodes.into_iter().collect();
450 nodes.sort_by_key(ToString::to_string);
451 let indices: HashMap<_, _> = nodes.iter().cloned().zip(0..).collect();
452 let mut shapes = Vec::new();
453 for node in nodes {
454 let declaration = crate::structural::read_shape(
455 &store,
456 &node,
457 &paths,
458 advanced,
459 &functions,
460 &components,
461 )?;
462 let (declaration, path, path_node) = match declaration {
463 crate::structural::ShapeDeclaration::Node(shape) => (*shape, None, None),
464 crate::structural::ShapeDeclaration::Property(shape) => (
465 model::NodeShape {
466 id: shape.id,
467 targets: shape.targets,
468 components: shape.components,
469 property_shapes: shape.property_shapes,
470 deactivated: shape.deactivated,
471 severity: shape.severity,
472 messages: shape.messages,
473 rules: shape.rules,
474 order: shape.order,
475 },
476 Some(Path::from(shape.path)),
477 shape.path_node,
478 ),
479 };
480 let targets = declaration
481 .targets
482 .into_iter()
483 .map(|target| lower_target(&store, target, namespaces))
484 .collect::<Result<Vec<_>>>()?;
485 let mut constraints = Vec::new();
486 for component in declaration.components {
487 match component {
488 model::Component::Sparql(sparql) => {
489 constraints.push(Constraint::Sparql(Box::new(crate::sparql::lower(
490 &store,
491 *sparql,
492 path.as_ref(),
493 namespaces,
494 )?)))
495 }
496 model::Component::Expression(expression) => {
497 constraints.push(Constraint::NodeExpression {
498 node: expression.expression.node.clone(),
499 expression: NodeExpression::lower(expression.expression, &indices)?,
500 messages: expression.messages,
501 })
502 }
503 model::Component::Custom(application) => {
504 let definition = components
505 .iter()
506 .find(|c| c.node.as_str() == application.component.as_str())
507 .ok_or_else(|| {
508 Error::Invalid(format!(
509 "Unknown constraint component {}",
510 application.component
511 ))
512 })?;
513 constraints.extend(definition.lower(
514 &store,
515 application,
516 path.as_ref(),
517 namespaces,
518 )?);
519 }
520 core => {
521 if let Some(constraint) = constraints::lower_one(
522 &store,
523 &declaration.id,
524 path.is_some(),
525 &indices,
526 core,
527 )? {
528 constraints.push(constraint);
529 }
530 }
531 }
532 }
533 let properties = declaration
534 .property_shapes
535 .iter()
536 .map(|n| {
537 indices
538 .get(n)
539 .copied()
540 .ok_or_else(|| Error::Invalid("Unknown property shape".into()))
541 })
542 .collect::<Result<_>>()?;
543 let mut rules = declaration
544 .rules
545 .into_iter()
546 .map(|rule| lower_rule(&store, rule, &indices, namespaces))
547 .collect::<Result<Vec<_>>>()?;
548 rules.sort_by(|a, b| {
549 a.order
550 .cmp(&b.order)
551 .then_with(|| a.node.to_string().cmp(&b.node.to_string()))
552 });
553 shapes.push(Shape {
554 order: order(declaration.order, &declaration.id)?,
555 deactivated: declaration.deactivated,
556 node: declaration.id,
557 path,
558 path_node,
559 targets,
560 constraints,
561 properties,
562 rules,
563 messages: declaration.messages,
564 severity: NamedNode::new_unchecked(declaration.severity.as_iri()),
565 });
566 }
567 for shape in &shapes {
568 for &p in &shape.properties {
569 if shapes[p].path.is_none() {
570 return Err(Error::Invalid(format!(
571 "sh:property points to {} without sh:path",
572 shapes[p].node
573 )));
574 }
575 }
576 }
577 let mut execution_order: Vec<_> = (0..shapes.len()).collect();
578 execution_order.sort_by(|&a, &b| shapes[a].order.cmp(&shapes[b].order));
579 Ok(Self {
580 graph: original,
581 shapes,
582 execution_order,
583 functions,
584 namespaces: namespaces.to_vec(),
585 })
586 }
587}