Skip to main content

oxirs_shacl/
node_expr_types.rs

1//! Type definitions for SHACL node expressions (SHACL-AF `sh:values`).
2//!
3//! This module contains the node expression AST, RDF term type, property path,
4//! the minimal graph trait and an in-memory implementation, the evaluation
5//! configuration, statistics, error type, and the fluent `ExprBuilder` API.
6//!
7//! Split from `node_expressions.rs` to keep each source file below the
8//! workspace 2000-line refactor threshold while preserving the public API
9//! surface re-exported through `super::node_expressions`.
10//!
11//! ## References
12//!
13//! - <https://www.w3.org/TR/shacl-af/#node-expressions>
14
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::fmt;
18
19// ---------------------------------------------------------------------------
20// Node expression AST
21// ---------------------------------------------------------------------------
22
23/// A SHACL node expression that can be evaluated against a focus node and graph.
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub enum NodeExpression {
26    /// The current focus node itself (`sh:this`).
27    FocusNode,
28
29    /// A constant term.
30    Constant(NodeTerm),
31
32    /// Follow a property path from the focus node.
33    Path(PropertyPath),
34
35    /// Filter nodes through a shape.
36    FilterShape {
37        /// The set of candidate nodes.
38        nodes: Box<NodeExpression>,
39        /// The shape IRI to filter against.
40        shape: String,
41    },
42
43    /// Intersection of multiple node sets.
44    Intersection(Vec<NodeExpression>),
45
46    /// Union of multiple node sets.
47    Union(Vec<NodeExpression>),
48
49    /// Set difference: nodes in `base` but not in `excluded`.
50    Minus {
51        /// Base node set.
52        base: Box<NodeExpression>,
53        /// Nodes to exclude.
54        excluded: Box<NodeExpression>,
55    },
56
57    /// Conditional: if condition evaluates to non-empty, use `then_expr`,
58    /// otherwise use `else_expr`.
59    IfThenElse {
60        /// Condition expression.
61        condition: Box<NodeExpression>,
62        /// Expression when condition is true.
63        then_expr: Box<NodeExpression>,
64        /// Expression when condition is false.
65        else_expr: Box<NodeExpression>,
66    },
67
68    /// Apply a function to arguments.
69    FunctionCall {
70        /// Function IRI.
71        function: String,
72        /// Argument expressions.
73        args: Vec<NodeExpression>,
74    },
75
76    /// Count the number of nodes from the inner expression.
77    Count(Box<NodeExpression>),
78
79    /// Deduplicate results from the inner expression.
80    Distinct(Box<NodeExpression>),
81
82    /// Sort results by a property path.
83    OrderBy {
84        /// Expression to sort.
85        expr: Box<NodeExpression>,
86        /// Property path to sort by.
87        sort_path: PropertyPath,
88        /// Ascending order.
89        ascending: bool,
90    },
91
92    /// Limit the number of results.
93    Limit {
94        /// Expression to limit.
95        expr: Box<NodeExpression>,
96        /// Maximum number of results.
97        limit: usize,
98    },
99
100    /// Skip the first N results.
101    Offset {
102        /// Expression to offset.
103        expr: Box<NodeExpression>,
104        /// Number of results to skip.
105        offset: usize,
106    },
107
108    /// Group-concat: join string values with a separator.
109    GroupConcat {
110        /// Expression producing the values.
111        expr: Box<NodeExpression>,
112        /// Separator string.
113        separator: String,
114    },
115}
116
117/// A simplified property path for node expression evaluation.
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub enum PropertyPath {
120    /// A single predicate IRI.
121    Predicate(String),
122    /// Inverse path (^predicate).
123    Inverse(Box<PropertyPath>),
124    /// Sequence path (p1 / p2).
125    Sequence(Vec<PropertyPath>),
126    /// Alternative path (p1 | p2).
127    Alternative(Vec<PropertyPath>),
128    /// Zero-or-more repetition (p*).
129    ZeroOrMore(Box<PropertyPath>),
130    /// One-or-more repetition (p+).
131    OneOrMore(Box<PropertyPath>),
132    /// Zero-or-one (p?).
133    ZeroOrOne(Box<PropertyPath>),
134}
135
136impl fmt::Display for PropertyPath {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        match self {
139            Self::Predicate(iri) => write!(f, "<{iri}>"),
140            Self::Inverse(p) => write!(f, "^{p}"),
141            Self::Sequence(steps) => {
142                let parts: Vec<_> = steps.iter().map(|s| s.to_string()).collect();
143                write!(f, "{}", parts.join(" / "))
144            }
145            Self::Alternative(alts) => {
146                let parts: Vec<_> = alts.iter().map(|s| s.to_string()).collect();
147                write!(f, "{}", parts.join(" | "))
148            }
149            Self::ZeroOrMore(p) => write!(f, "{p}*"),
150            Self::OneOrMore(p) => write!(f, "{p}+"),
151            Self::ZeroOrOne(p) => write!(f, "{p}?"),
152        }
153    }
154}
155
156// ---------------------------------------------------------------------------
157// RDF term
158// ---------------------------------------------------------------------------
159
160/// An RDF term in node expressions.
161#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
162pub enum NodeTerm {
163    /// An IRI reference.
164    Iri(String),
165    /// A typed or untyped literal.
166    Literal {
167        value: String,
168        datatype: Option<String>,
169        lang: Option<String>,
170    },
171    /// A blank node.
172    BlankNode(String),
173}
174
175impl fmt::Display for NodeTerm {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match self {
178            Self::Iri(iri) => write!(f, "<{iri}>"),
179            Self::Literal {
180                value,
181                datatype,
182                lang,
183            } => {
184                write!(f, "\"{value}\"")?;
185                if let Some(dt) = datatype {
186                    write!(f, "^^<{dt}>")?;
187                }
188                if let Some(l) = lang {
189                    write!(f, "@{l}")?;
190                }
191                Ok(())
192            }
193            Self::BlankNode(id) => write!(f, "_:{id}"),
194        }
195    }
196}
197
198impl NodeTerm {
199    /// Create an IRI term.
200    pub fn iri(s: impl Into<String>) -> Self {
201        Self::Iri(s.into())
202    }
203
204    /// Create a string literal.
205    pub fn literal(s: impl Into<String>) -> Self {
206        Self::Literal {
207            value: s.into(),
208            datatype: None,
209            lang: None,
210        }
211    }
212
213    /// Create a typed literal.
214    pub fn typed_literal(s: impl Into<String>, dt: impl Into<String>) -> Self {
215        Self::Literal {
216            value: s.into(),
217            datatype: Some(dt.into()),
218            lang: None,
219        }
220    }
221
222    /// Create a language-tagged literal.
223    pub fn lang_literal(s: impl Into<String>, lang: impl Into<String>) -> Self {
224        Self::Literal {
225            value: s.into(),
226            datatype: None,
227            lang: Some(lang.into()),
228        }
229    }
230
231    /// Check if this is an IRI.
232    pub fn is_iri(&self) -> bool {
233        matches!(self, Self::Iri(_))
234    }
235
236    /// Check if this is a literal.
237    pub fn is_literal(&self) -> bool {
238        matches!(self, Self::Literal { .. })
239    }
240
241    /// Get the string value of this term.
242    pub fn as_str(&self) -> &str {
243        match self {
244            Self::Iri(s) | Self::BlankNode(s) => s,
245            Self::Literal { value, .. } => value,
246        }
247    }
248
249    /// Try to parse a literal as an integer.
250    pub fn as_integer(&self) -> Option<i64> {
251        if let Self::Literal { value, .. } = self {
252            value.parse().ok()
253        } else {
254            None
255        }
256    }
257
258    /// Try to parse a literal as a float.
259    pub fn as_float(&self) -> Option<f64> {
260        if let Self::Literal { value, .. } = self {
261            value.parse().ok()
262        } else {
263            None
264        }
265    }
266}
267
268// ---------------------------------------------------------------------------
269// Simple triple store for node expression evaluation
270// ---------------------------------------------------------------------------
271
272/// A triple in the graph.
273#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
274pub struct Triple {
275    /// Subject.
276    pub subject: NodeTerm,
277    /// Predicate.
278    pub predicate: NodeTerm,
279    /// Object.
280    pub object: NodeTerm,
281}
282
283/// A minimal graph interface for evaluating node expressions.
284pub trait NodeExprGraph {
285    /// Get all objects for a given subject and predicate.
286    fn objects(&self, subject: &NodeTerm, predicate: &str) -> Vec<NodeTerm>;
287
288    /// Get all subjects for a given predicate and object.
289    fn subjects(&self, predicate: &str, object: &NodeTerm) -> Vec<NodeTerm>;
290
291    /// Check if a node conforms to a shape (by IRI).
292    fn conforms_to_shape(&self, node: &NodeTerm, shape_iri: &str) -> bool;
293
294    /// Get all triples in the graph.
295    fn all_triples(&self) -> Vec<Triple>;
296}
297
298/// In-memory graph for testing and simple evaluation.
299#[derive(Debug, Clone, Default)]
300pub struct InMemoryGraph {
301    triples: Vec<Triple>,
302    /// Shape conformance answers (for testing).
303    shape_conformance: HashMap<(String, String), bool>,
304}
305
306impl InMemoryGraph {
307    /// Create a new empty graph.
308    pub fn new() -> Self {
309        Self::default()
310    }
311
312    /// Add a triple to the graph.
313    pub fn add_triple(&mut self, subject: NodeTerm, predicate: NodeTerm, object: NodeTerm) {
314        self.triples.push(Triple {
315            subject,
316            predicate,
317            object,
318        });
319    }
320
321    /// Add a shape conformance fact (for testing shape filters).
322    pub fn set_conforms(&mut self, node: &str, shape: &str, conforms: bool) {
323        self.shape_conformance
324            .insert((node.to_string(), shape.to_string()), conforms);
325    }
326
327    /// Number of triples in the graph.
328    pub fn len(&self) -> usize {
329        self.triples.len()
330    }
331
332    /// Check if the graph is empty.
333    pub fn is_empty(&self) -> bool {
334        self.triples.is_empty()
335    }
336}
337
338impl NodeExprGraph for InMemoryGraph {
339    fn objects(&self, subject: &NodeTerm, predicate: &str) -> Vec<NodeTerm> {
340        self.triples
341            .iter()
342            .filter(|t| {
343                &t.subject == subject && matches!(&t.predicate, NodeTerm::Iri(p) if p == predicate)
344            })
345            .map(|t| t.object.clone())
346            .collect()
347    }
348
349    fn subjects(&self, predicate: &str, object: &NodeTerm) -> Vec<NodeTerm> {
350        self.triples
351            .iter()
352            .filter(|t| {
353                &t.object == object && matches!(&t.predicate, NodeTerm::Iri(p) if p == predicate)
354            })
355            .map(|t| t.subject.clone())
356            .collect()
357    }
358
359    fn conforms_to_shape(&self, node: &NodeTerm, shape_iri: &str) -> bool {
360        let node_str = node.as_str().to_string();
361        self.shape_conformance
362            .get(&(node_str, shape_iri.to_string()))
363            .copied()
364            .unwrap_or(false)
365    }
366
367    fn all_triples(&self) -> Vec<Triple> {
368        self.triples.clone()
369    }
370}
371
372// ---------------------------------------------------------------------------
373// Configuration, statistics, errors
374// ---------------------------------------------------------------------------
375
376/// Configuration for node expression evaluation.
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct EvalConfig {
379    /// Maximum path traversal depth to prevent infinite loops.
380    pub max_path_depth: usize,
381    /// Maximum number of results before truncation.
382    pub max_results: usize,
383    /// Enable deduplication by default.
384    pub deduplicate: bool,
385}
386
387impl Default for EvalConfig {
388    fn default() -> Self {
389        Self {
390            max_path_depth: 100,
391            max_results: 10000,
392            deduplicate: false,
393        }
394    }
395}
396
397/// Statistics from evaluation.
398#[derive(Debug, Clone, Default, Serialize, Deserialize)]
399pub struct EvalStats {
400    /// Number of graph lookups performed.
401    pub graph_lookups: u64,
402    /// Number of nodes evaluated.
403    pub nodes_evaluated: u64,
404    /// Number of path traversals.
405    pub path_traversals: u64,
406    /// Number of shape checks.
407    pub shape_checks: u64,
408    /// Number of function calls.
409    pub function_calls: u64,
410}
411
412/// Errors from node expression evaluation.
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub enum NodeExprError {
415    /// Path depth exceeded.
416    PathDepthExceeded { depth: usize, max: usize },
417    /// Result limit exceeded.
418    ResultLimitExceeded { count: usize, max: usize },
419    /// Unknown function.
420    UnknownFunction(String),
421    /// Invalid argument count.
422    InvalidArgCount {
423        function: String,
424        expected: usize,
425        got: usize,
426    },
427    /// Type error in computation.
428    TypeError(String),
429    /// Empty sequence where at least one value was expected.
430    EmptySequence(String),
431}
432
433impl fmt::Display for NodeExprError {
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        match self {
436            Self::PathDepthExceeded { depth, max } => {
437                write!(f, "Path depth {depth} exceeded maximum {max}")
438            }
439            Self::ResultLimitExceeded { count, max } => {
440                write!(f, "Result count {count} exceeded limit {max}")
441            }
442            Self::UnknownFunction(name) => write!(f, "Unknown function: {name}"),
443            Self::InvalidArgCount {
444                function,
445                expected,
446                got,
447            } => {
448                write!(f, "Function {function} expects {expected} args, got {got}")
449            }
450            Self::TypeError(msg) => write!(f, "Type error: {msg}"),
451            Self::EmptySequence(ctx) => write!(f, "Empty sequence in {ctx}"),
452        }
453    }
454}
455
456impl std::error::Error for NodeExprError {}
457
458// ---------------------------------------------------------------------------
459// Expression builder (fluent API)
460// ---------------------------------------------------------------------------
461
462/// Fluent builder for constructing node expressions.
463pub struct ExprBuilder;
464
465impl ExprBuilder {
466    /// The focus node expression.
467    pub fn focus() -> NodeExpression {
468        NodeExpression::FocusNode
469    }
470
471    /// A constant term expression.
472    pub fn constant(term: NodeTerm) -> NodeExpression {
473        NodeExpression::Constant(term)
474    }
475
476    /// A path expression (single predicate).
477    pub fn path(predicate: impl Into<String>) -> NodeExpression {
478        NodeExpression::Path(PropertyPath::Predicate(predicate.into()))
479    }
480
481    /// A sequence path (p1 / p2 / ...).
482    pub fn sequence_path(predicates: &[&str]) -> NodeExpression {
483        NodeExpression::Path(PropertyPath::Sequence(
484            predicates
485                .iter()
486                .map(|p| PropertyPath::Predicate(p.to_string()))
487                .collect(),
488        ))
489    }
490
491    /// An intersection of expressions.
492    pub fn intersection(exprs: Vec<NodeExpression>) -> NodeExpression {
493        NodeExpression::Intersection(exprs)
494    }
495
496    /// A union of expressions.
497    pub fn union(exprs: Vec<NodeExpression>) -> NodeExpression {
498        NodeExpression::Union(exprs)
499    }
500
501    /// A set-minus expression.
502    pub fn minus(base: NodeExpression, excluded: NodeExpression) -> NodeExpression {
503        NodeExpression::Minus {
504            base: Box::new(base),
505            excluded: Box::new(excluded),
506        }
507    }
508
509    /// An if-then-else expression.
510    pub fn if_then_else(
511        condition: NodeExpression,
512        then_expr: NodeExpression,
513        else_expr: NodeExpression,
514    ) -> NodeExpression {
515        NodeExpression::IfThenElse {
516            condition: Box::new(condition),
517            then_expr: Box::new(then_expr),
518            else_expr: Box::new(else_expr),
519        }
520    }
521
522    /// A count expression.
523    pub fn count(inner: NodeExpression) -> NodeExpression {
524        NodeExpression::Count(Box::new(inner))
525    }
526
527    /// A distinct expression.
528    pub fn distinct(inner: NodeExpression) -> NodeExpression {
529        NodeExpression::Distinct(Box::new(inner))
530    }
531
532    /// A limit expression.
533    pub fn limit(inner: NodeExpression, limit: usize) -> NodeExpression {
534        NodeExpression::Limit {
535            expr: Box::new(inner),
536            limit,
537        }
538    }
539
540    /// An offset expression.
541    pub fn offset(inner: NodeExpression, offset: usize) -> NodeExpression {
542        NodeExpression::Offset {
543            expr: Box::new(inner),
544            offset,
545        }
546    }
547}