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) => Ok(format!("/@{k}")),
151 None => {
152 self.notes.push(
153 "text(): Quarb's bare :: is the concatenated descendant text; \
154 XPath text() selects immediate text nodes (equal on leaf elements)"
155 .to_string(),
156 );
157 Ok("/text()".to_string())
158 }
159 },
160 other => Err(XPathError::Unsupported(format!(
161 "the {other} metadata projection"
162 ))),
163 }
164 }
165
166 fn step(&mut self, s: NodeId) -> Result<String, XPathError> {
167 let name = match self.prop_s(s, "matcher-kind").as_str() {
168 "name" => self.prop_s(s, "matcher"),
169 "any" => "*".to_string(),
170 other => {
171 return Err(XPathError::Unsupported(format!(
172 "{other} name matching (XPath names are literal)"
173 )));
174 }
175 };
176 let mut out = match self.prop_s(s, "axis").as_str() {
177 "/" => format!("/{name}"),
178 "//" => {
179 self.notes.push(
180 "//: Quarb selects proper descendants; XPath's // is \
181 descendant-or-self"
182 .to_string(),
183 );
184 format!("//{name}")
185 }
186 "\\" => format!("/parent::{name}"),
187 "\\\\" => format!("/ancestor::{name}"),
188 ">>" => format!("/following-sibling::{name}"),
189 "<<" => format!("/preceding-sibling::{name}"),
190 other => {
191 return Err(XPathError::Unsupported(format!(
192 "the '{other}' axis (XPath 1.0 lacks it, or the reach variant)"
193 )));
194 }
195 };
196 if self.kid(s, "trait").is_some() {
197 return Err(XPathError::Unsupported("trait filters (<...>)".into()));
198 }
199 for p in self.kids(s, "predicate") {
200 out.push_str(&self.predicate(p)?);
201 }
202 if self.prop(s, "leaf") == Some(Value::Bool(true)) {
203 out.push_str("[not(*)]");
204 }
205 Ok(out)
206 }
207
208 fn predicate(&mut self, p: NodeId) -> Result<String, XPathError> {
209 match self.prop_s(p, "kind").as_str() {
210 "index" => match self.prop(p, "value") {
211 Some(Value::Int(n)) if n > 0 => Ok(format!("[{n}]")),
212 Some(Value::Int(-1)) => Ok("[last()]".to_string()),
213 Some(Value::Int(n)) => Ok(format!("[last() - {}]", -n - 1)),
214 _ => Err(XPathError::Unsupported("a non-integer index".into())),
215 },
216 "range" => {
217 let from = match self.prop(p, "from") {
218 Some(Value::Int(n)) => Some(n),
219 _ => None,
220 };
221 let to = match self.prop(p, "to") {
222 Some(Value::Int(n)) => Some(n),
223 _ => None,
224 };
225 if from.is_some_and(|n| n < 0) || to.is_some_and(|n| n < 0) {
226 return Err(XPathError::Unsupported("negative range ends".into()));
227 }
228 Ok(match (from, to) {
229 (Some(a), Some(b)) => {
230 format!("[position() >= {a} and position() <= {b}]")
231 }
232 (Some(a), None) => format!("[position() >= {a}]"),
233 (None, Some(b)) => format!("[position() <= {b}]"),
234 (None, None) => String::new(),
235 })
236 }
237 _ => {
238 let parts: Vec<String> = self
239 .arbor
240 .children(p)
241 .into_iter()
242 .map(|c| self.pred_expr(c))
243 .collect::<Result<_, _>>()?;
244 Ok(format!("[{}]", parts.join(" and ")))
245 }
246 }
247 }
248
249 fn pred_expr(&mut self, e: NodeId) -> Result<String, XPathError> {
250 match self.kind(e).as_str() {
251 "and" | "or" => {
252 let op = self.kind(e);
253 let kids: Vec<String> = self
254 .arbor
255 .children(e)
256 .into_iter()
257 .map(|c| self.pred_expr(c))
258 .collect::<Result<_, _>>()?;
259 Ok(format!("({})", kids.join(&format!(" {op} "))))
260 }
261 "not" => {
262 let inner: Vec<String> = self
263 .arbor
264 .children(e)
265 .into_iter()
266 .map(|c| self.pred_expr(c))
267 .collect::<Result<_, _>>()?;
268 Ok(format!("not({})", inner.join(" and ")))
269 }
270 "parens" => {
271 let kids: Vec<String> = self
272 .arbor
273 .children(e)
274 .into_iter()
275 .map(|c| self.pred_expr(c))
276 .collect::<Result<_, _>>()?;
277 Ok(format!("({})", kids.join(" and ")))
278 }
279 "compare" => {
280 let op = self.prop_s(e, "op");
281 let kids = self.arbor.children(e);
282 let l = self.operand(kids[0])?;
283 let r = self.operand(kids[1])?;
284 Ok(match op.as_str() {
285 "=" | "<" | "<=" | ">" | ">=" => format!("{l} {op} {r}"),
286 "!=" => format!("{l} != {r}"),
287 "*=" => format!("contains({l}, {r})"),
288 "=~" => {
289 return Err(XPathError::Unsupported(
290 "regex matching (XPath 1.0 has contains/starts-with)".into(),
291 ));
292 }
293 other => {
294 return Err(XPathError::Unsupported(format!("the '{other}' comparison")));
295 }
296 })
297 }
298 _ => self.operand(e),
299 }
300 }
301
302 fn operand(&mut self, o: NodeId) -> Result<String, XPathError> {
303 match self.kind(o).as_str() {
304 "literal" => {
305 let v = self.prop(o, "value").unwrap_or(Value::Null);
306 Ok(match self.prop_s(o, "type").as_str() {
307 "text" => format!("\"{v}\""),
308 _ => v.to_string(),
309 })
310 }
311 "path" => {
312 let steps = self.kids(o, "step");
313 let mut out = String::new();
314 for s in &steps {
315 out.push_str(&self.step(*s)?);
316 }
317 let out = out.trim_start_matches('/').to_string();
318 match self.kid(o, "projection") {
319 Some(p) => {
320 let proj = self.projection(p)?;
321 Ok(if out.is_empty() {
322 proj.trim_start_matches('/').to_string()
323 } else {
324 format!("{out}{proj}")
325 })
326 }
327 None => Ok(if out.is_empty() { ".".into() } else { out }),
328 }
329 }
330 other => Err(XPathError::Unsupported(format!(
331 "the '{other}' operand (registers and topics are Quarb-side state)"
332 ))),
333 }
334 }
335}