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}