Skip to main content

oxirs_arq/generate/
executor.rs

1//! SPARQL-Generate executor.
2//!
3//! Evaluates a `GenerateQuery` against one or more rows of SPARQL variable
4//! bindings, expanding template expressions to produce text output.
5
6use std::collections::HashMap;
7
8use super::ast::{GenerateLiteral, GenerateQuery, TemplateClause};
9use super::GenerateError;
10
11// ─────────────────────────────────────────────────────────────────────────────
12// Public types
13// ─────────────────────────────────────────────────────────────────────────────
14
15/// A set of SPARQL variable bindings for one solution row.
16///
17/// Keys are variable names **without** a leading `?`; values are the RDF term
18/// strings as they would appear in a SPARQL result (e.g. `"Alice"`,
19/// `<http://example.org/Alice>`, or `"42"^^xsd:integer`).
20pub type Bindings = HashMap<String, String>;
21
22/// The result of evaluating a GENERATE template for a single solution row.
23#[derive(Debug)]
24pub struct GenerateResult {
25    /// The generated text produced by substituting bindings into the template.
26    pub text: String,
27    /// The number of variable bindings that were actually used during evaluation
28    /// of this row (i.e., variable references that resolved to a value).
29    pub binding_count: usize,
30}
31
32// ─────────────────────────────────────────────────────────────────────────────
33// GenerateExecutor
34// ─────────────────────────────────────────────────────────────────────────────
35
36/// Executes a parsed `GenerateQuery` over a set of SPARQL variable bindings.
37///
38/// # Example
39///
40/// ```rust
41/// use std::collections::HashMap;
42/// use oxirs_arq::generate::{GenerateExecutor, Bindings, GenerateQuery, TemplateClause, GenerateLiteral};
43///
44/// let clause = TemplateClause {
45///     prefix: Some("name=".to_string()),
46///     expr: GenerateLiteral::Var("name".to_string()),
47///     suffix: None,
48/// };
49/// let query = GenerateQuery::new(vec![clause], "?s foaf:name ?name .");
50/// let exec  = GenerateExecutor::new(query);
51///
52/// let mut row = HashMap::new();
53/// row.insert("name".to_string(), "Alice".to_string());
54///
55/// let result = exec.evaluate_one(&row).unwrap();
56/// assert_eq!(result.text, "name=Alice");
57/// ```
58pub struct GenerateExecutor {
59    /// The parsed GENERATE query to execute.
60    pub query: GenerateQuery,
61}
62
63impl GenerateExecutor {
64    /// Create a new executor for the given `GenerateQuery`.
65    pub fn new(query: GenerateQuery) -> Self {
66        Self { query }
67    }
68
69    // ── Single-row evaluation ────────────────────────────────────────────────
70
71    /// Evaluate the GENERATE template against a single solution row.
72    ///
73    /// Returns `GenerateResult` containing the concatenated text output and the
74    /// count of distinct bindings used during evaluation.
75    ///
76    /// # Errors
77    ///
78    /// Returns `GenerateError::UnboundVariable` if a `Var` reference in the
79    /// template is not present in `bindings`.
80    pub fn evaluate_one(&self, bindings: &Bindings) -> Result<GenerateResult, GenerateError> {
81        let mut parts = Vec::new();
82        let mut used_vars: std::collections::HashSet<&str> = std::collections::HashSet::new();
83
84        for clause in &self.query.template {
85            let text = self.eval_clause(clause, bindings, &mut used_vars)?;
86            parts.push(text);
87        }
88
89        Ok(GenerateResult {
90            text: parts.concat(),
91            binding_count: used_vars.len(),
92        })
93    }
94
95    // ── Multi-row evaluation ─────────────────────────────────────────────────
96
97    /// Evaluate the GENERATE template over multiple solution rows, collecting
98    /// one `GenerateResult` per row.
99    ///
100    /// # Errors
101    ///
102    /// Propagates any error returned by `evaluate_one`.
103    pub fn evaluate_all(&self, rows: &[Bindings]) -> Result<Vec<GenerateResult>, GenerateError> {
104        rows.iter().map(|row| self.evaluate_one(row)).collect()
105    }
106
107    /// Concatenate all generated texts (one per row) into a single `String`,
108    /// with `separator` between each adjacent pair.
109    ///
110    /// # Errors
111    ///
112    /// Propagates any error returned by `evaluate_all`.
113    pub fn generate_text(
114        &self,
115        rows: &[Bindings],
116        separator: &str,
117    ) -> Result<String, GenerateError> {
118        let results = self.evaluate_all(rows)?;
119        let texts: Vec<&str> = results.iter().map(|r| r.text.as_str()).collect();
120        Ok(texts.join(separator))
121    }
122
123    // ── Internal helpers ─────────────────────────────────────────────────────
124
125    /// Evaluate a single `TemplateClause` given `bindings`, appending variable
126    /// names that are resolved into `used_vars`.
127    fn eval_clause<'b>(
128        &self,
129        clause: &TemplateClause,
130        bindings: &'b Bindings,
131        used_vars: &mut std::collections::HashSet<&'b str>,
132    ) -> Result<String, GenerateError> {
133        let mut buf = String::new();
134
135        if let Some(prefix) = &clause.prefix {
136            buf.push_str(prefix);
137        }
138
139        buf.push_str(&self.eval_literal_tracked(&clause.expr, bindings, used_vars)?);
140
141        if let Some(suffix) = &clause.suffix {
142            buf.push_str(suffix);
143        }
144
145        Ok(buf)
146    }
147
148    /// Evaluate a `GenerateLiteral`, tracking which variables are used.
149    fn eval_literal_tracked<'b>(
150        &self,
151        lit: &GenerateLiteral,
152        bindings: &'b Bindings,
153        used_vars: &mut std::collections::HashSet<&'b str>,
154    ) -> Result<String, GenerateError> {
155        match lit {
156            GenerateLiteral::Text(s) => Ok(s.clone()),
157
158            GenerateLiteral::Var(name) => {
159                let value = bindings
160                    .get(name.as_str())
161                    .ok_or_else(|| GenerateError::UnboundVariable(name.clone()))?;
162                // Track that this variable was resolved.
163                // Safety: the key in `bindings` lives at least as long as `bindings`.
164                if let Some(key) = bindings.keys().find(|k| k.as_str() == name.as_str()) {
165                    used_vars.insert(key.as_str());
166                }
167                Ok(value.clone())
168            }
169
170            GenerateLiteral::Concat(parts) => {
171                let mut result = String::new();
172                for part in parts {
173                    result.push_str(&self.eval_literal_tracked(part, bindings, used_vars)?);
174                }
175                Ok(result)
176            }
177        }
178    }
179
180    /// Evaluate a single `GenerateLiteral` given bindings.
181    ///
182    /// This is the public-facing variant without variable tracking; it is
183    /// useful for unit-testing individual literal expressions.
184    pub fn eval_literal(
185        &self,
186        lit: &GenerateLiteral,
187        bindings: &Bindings,
188    ) -> Result<String, GenerateError> {
189        let mut used = std::collections::HashSet::new();
190        self.eval_literal_tracked(lit, bindings, &mut used)
191    }
192}