1use crate::{Translation, XPathError};
24use quarb::reflect::QueryArbor;
25use quarb::{AstAdapter, NodeId, Value};
26
27pub fn export(quarb: &str) -> Result<Translation, XPathError> {
29 let arbor = QueryArbor::parse(quarb)
30 .map_err(|e| XPathError::Syntax(0, format!("parsing Quarb: {e}")))?;
31 if has_group(&arbor) {
32 return Err(XPathError::Syntax(0, "path patterns (groups and quantifiers) have no XPath 1.0 translation".into()));
33 }
34 let mut ex = Exporter {
35 arbor,
36 notes: Vec::new(),
37 };
38 let query = ex.query()?;
39 Ok(Translation {
40 query,
41 notes: ex.notes,
42 })
43}
44
45fn has_group(arbor: &QueryArbor) -> bool {
49 let mut stack = vec![arbor.root()];
50 while let Some(n) = stack.pop() {
51 if arbor.name(n).as_deref() == Some("group") {
52 return true;
53 }
54 stack.extend(arbor.children(n));
55 }
56 false
57}
58
59struct Exporter {
60 arbor: QueryArbor,
61 notes: Vec<String>,
62}
63
64impl Exporter {
65 fn kids(&self, n: NodeId, kind: &str) -> Vec<NodeId> {
66 self.arbor
67 .children(n)
68 .into_iter()
69 .filter(|&c| self.arbor.name(c).as_deref() == Some(kind))
70 .collect()
71 }
72
73 fn kid(&self, n: NodeId, kind: &str) -> Option<NodeId> {
74 self.kids(n, kind).into_iter().next()
75 }
76
77 fn prop(&self, n: NodeId, key: &str) -> Option<Value> {
78 self.arbor.property(n, key)
79 }
80
81 fn prop_s(&self, n: NodeId, key: &str) -> String {
82 self.prop(n, key).map(|v| v.to_string()).unwrap_or_default()
83 }
84
85 fn kind(&self, n: NodeId) -> String {
86 self.arbor.name(n).unwrap_or_default()
87 }
88
89 fn query(&mut self) -> Result<String, XPathError> {
90 let root = self.arbor.root();
91 let q = self
92 .kid(root, "query")
93 .ok_or_else(|| XPathError::Unsupported("empty query".into()))?;
94 if self.kid(q, "query").is_some() {
95 return Err(XPathError::Unsupported(
96 "correlation (<=>) has no XPath equivalent".into(),
97 ));
98 }
99 let branches = self.kids(q, "branch");
100 let paths: Vec<String> = branches
101 .iter()
102 .map(|&b| self.branch(b))
103 .collect::<Result<_, _>>()?;
104 let mut out = paths.join(" | ");
105
106 if let Some(pipe) = self.kid(q, "pipeline") {
107 let stages = self.arbor.children(pipe);
108 if stages.len() != 1 {
109 return Err(XPathError::Unsupported(
110 "a multi-stage pipeline (XPath 1.0 has no pipeline)".into(),
111 ));
112 }
113 let s = stages[0];
114 if self.kind(s) != "agg" {
115 return Err(XPathError::Unsupported(format!(
116 "the '{}' stage (XPath 1.0 has no pipeline)",
117 self.kind(s)
118 )));
119 }
120 out = match self.prop_s(s, "name").as_str() {
121 "count" => format!("count({out})"),
122 "sum" => format!("sum({out})"),
123 other => {
124 return Err(XPathError::Unsupported(format!(
125 "the '{other}' aggregate (XPath 1.0 has count() and sum())"
126 )));
127 }
128 };
129 }
130 Ok(out)
131 }
132
133 fn branch(&mut self, b: NodeId) -> Result<String, XPathError> {
134 let mut out = String::new();
135 for step in self.kids(b, "step") {
136 out.push_str(&self.step(step)?);
137 }
138 if let Some(p) = self.kid(b, "projection") {
139 out.push_str(&self.projection(p)?);
140 }
141 if out.is_empty() {
142 out.push('.');
143 }
144 Ok(out)
145 }
146
147 fn projection(&mut self, p: NodeId) -> Result<String, XPathError> {
148 match self.prop_s(p, "kind").as_str() {
149 "property" => match self.prop(p, "key") {
150 Some(k) if k.to_string() == "text" => {
151 self.notes.push(
152 "text(): Quarb ::text is the concatenated descendant text; \
153 XPath text() selects immediate text nodes (equal on leaf elements)"
154 .to_string(),
155 );
156 Ok("/text()".to_string())
157 }
158 Some(k) => Ok(format!("/@{k}")),
159 None => Err(XPathError::Unsupported(
160 "the bare '::' projection (adapter-specific default value)".into(),
161 )),
162 },
163 other => Err(XPathError::Unsupported(format!(
164 "the {other} metadata projection"
165 ))),
166 }
167 }
168
169 fn step(&mut self, s: NodeId) -> Result<String, XPathError> {
170 let name = match self.prop_s(s, "matcher-kind").as_str() {
171 "name" => self.prop_s(s, "matcher"),
172 "any" => "*".to_string(),
173 other => {
174 return Err(XPathError::Unsupported(format!(
175 "{other} name matching (XPath names are literal)"
176 )));
177 }
178 };
179 let mut out = match self.prop_s(s, "axis").as_str() {
180 "/" => format!("/{name}"),
181 "//" => {
182 self.notes.push(
183 "//: Quarb selects proper descendants; XPath's // is \
184 descendant-or-self"
185 .to_string(),
186 );
187 format!("//{name}")
188 }
189 "\\" => format!("/parent::{name}"),
190 "\\\\" => format!("/ancestor::{name}"),
191 ">>" => format!("/following-sibling::{name}"),
192 "<<" => format!("/preceding-sibling::{name}"),
193 other => {
194 return Err(XPathError::Unsupported(format!(
195 "the '{other}' axis (XPath 1.0 lacks it, or the reach variant)"
196 )));
197 }
198 };
199 if self.kid(s, "trait").is_some() {
200 return Err(XPathError::Unsupported("trait filters (<...>)".into()));
201 }
202 for p in self.kids(s, "predicate") {
203 out.push_str(&self.predicate(p)?);
204 }
205 if self.prop(s, "leaf") == Some(Value::Bool(true)) {
206 out.push_str("[not(*)]");
207 }
208 Ok(out)
209 }
210
211 fn predicate(&mut self, p: NodeId) -> Result<String, XPathError> {
212 match self.prop_s(p, "kind").as_str() {
213 "index" => match self.prop(p, "value") {
214 Some(Value::Int(n)) if n > 0 => Ok(format!("[{n}]")),
215 Some(Value::Int(-1)) => Ok("[last()]".to_string()),
216 Some(Value::Int(n)) => Ok(format!("[last() - {}]", -n - 1)),
217 _ => Err(XPathError::Unsupported("a non-integer index".into())),
218 },
219 "range" => {
220 let from = match self.prop(p, "from") {
221 Some(Value::Int(n)) => Some(n),
222 _ => None,
223 };
224 let to = match self.prop(p, "to") {
225 Some(Value::Int(n)) => Some(n),
226 _ => None,
227 };
228 if from.is_some_and(|n| n < 0) || to.is_some_and(|n| n < 0) {
229 return Err(XPathError::Unsupported("negative range ends".into()));
230 }
231 Ok(match (from, to) {
232 (Some(a), Some(b)) => {
233 format!("[position() >= {a} and position() <= {b}]")
234 }
235 (Some(a), None) => format!("[position() >= {a}]"),
236 (None, Some(b)) => format!("[position() <= {b}]"),
237 (None, None) => String::new(),
238 })
239 }
240 _ => {
241 let parts: Vec<String> = self
242 .arbor
243 .children(p)
244 .into_iter()
245 .map(|c| self.pred_expr(c))
246 .collect::<Result<_, _>>()?;
247 Ok(format!("[{}]", parts.join(" and ")))
248 }
249 }
250 }
251
252 fn pred_expr(&mut self, e: NodeId) -> Result<String, XPathError> {
253 match self.kind(e).as_str() {
254 "and" | "or" => {
255 let op = self.kind(e);
256 let kids: Vec<String> = self
257 .arbor
258 .children(e)
259 .into_iter()
260 .map(|c| self.pred_expr(c))
261 .collect::<Result<_, _>>()?;
262 Ok(format!("({})", kids.join(&format!(" {op} "))))
263 }
264 "not" => {
265 let inner: Vec<String> = self
266 .arbor
267 .children(e)
268 .into_iter()
269 .map(|c| self.pred_expr(c))
270 .collect::<Result<_, _>>()?;
271 Ok(format!("not({})", inner.join(" and ")))
272 }
273 "parens" => {
274 let kids: Vec<String> = self
275 .arbor
276 .children(e)
277 .into_iter()
278 .map(|c| self.pred_expr(c))
279 .collect::<Result<_, _>>()?;
280 Ok(format!("({})", kids.join(" and ")))
281 }
282 "compare" => {
283 let op = self.prop_s(e, "op");
284 let kids = self.arbor.children(e);
285 let l = self.operand(kids[0])?;
286 let r = self.operand(kids[1])?;
287 Ok(match op.as_str() {
288 "=" | "<" | "<=" | ">" | ">=" => format!("{l} {op} {r}"),
289 "!=" => format!("{l} != {r}"),
290 "*=" => format!("contains({l}, {r})"),
291 "=~" => {
292 return Err(XPathError::Unsupported(
293 "regex matching (XPath 1.0 has contains/starts-with)".into(),
294 ));
295 }
296 other => {
297 return Err(XPathError::Unsupported(format!("the '{other}' comparison")));
298 }
299 })
300 }
301 _ => self.operand(e),
302 }
303 }
304
305 fn operand(&mut self, o: NodeId) -> Result<String, XPathError> {
306 match self.kind(o).as_str() {
307 "literal" => {
308 let v = self.prop(o, "value").unwrap_or(Value::Null);
309 Ok(match self.prop_s(o, "type").as_str() {
310 "text" => format!("\"{v}\""),
311 _ => v.to_string(),
312 })
313 }
314 "path" => {
315 let steps = self.kids(o, "step");
316 let mut out = String::new();
317 for s in &steps {
318 out.push_str(&self.step(*s)?);
319 }
320 let out = out.trim_start_matches('/').to_string();
321 match self.kid(o, "projection") {
322 Some(p) => {
323 let proj = self.projection(p)?;
324 Ok(if out.is_empty() {
325 proj.trim_start_matches('/').to_string()
326 } else {
327 format!("{out}{proj}")
328 })
329 }
330 None => Ok(if out.is_empty() { ".".into() } else { out }),
331 }
332 }
333 other => Err(XPathError::Unsupported(format!(
334 "the '{other}' operand (registers and topics are Quarb-side state)"
335 ))),
336 }
337 }
338}