xee_xpath/
query.rs

1//! Queries you can execute against a document.
2
3use std::rc::Rc;
4
5use xee_interpreter::context::{self, StaticContext};
6use xee_interpreter::error::SpannedResult as Result;
7use xee_interpreter::interpreter::Program;
8use xee_interpreter::sequence::{Item, Sequence};
9
10use crate::{Documents, Itemable};
11
12// import only for documentation purposes
13#[cfg(doc)]
14use crate::context::DynamicContextBuilder;
15#[cfg(doc)]
16use crate::Queries;
17
18/// A query that can be executed against an [`Itemable`]
19///
20/// It gives back a result of type `V`
21pub trait Query<V> {
22    /// Get the program for the query
23    fn program(&self) -> &Program;
24
25    /// Get the static context for the query.
26    fn static_context(&self) -> &StaticContext {
27        self.program().static_context()
28    }
29
30    /// Execute the query against a dynamic context
31    ///
32    /// You can construct one using a [`DynamicContextBuilder`]
33    fn execute_with_context(
34        &self,
35        documents: &mut Documents,
36        context: &context::DynamicContext,
37    ) -> Result<V>;
38
39    /// Get a dynamic context builder for the query, configured with the
40    /// query's static context and the document's documents.
41    ///
42    /// You can use this if you want to construct your own dynamic context
43    /// to use with `execute_with_context`.
44    fn dynamic_context_builder(&self, documents: &Documents) -> context::DynamicContextBuilder {
45        let mut context = self.program().dynamic_context_builder();
46        context.documents(documents.documents().clone());
47        context
48    }
49
50    /// Map the the result of the query to a different type.
51    ///
52    /// You need to provide a function that takes the result of the query,
53    /// the document, and the item, and returns a new result.
54    fn map<T, F>(self, f: F) -> MapQuery<V, T, Self, F>
55    where
56        Self: Sized,
57        F: Fn(V, &mut Documents, &context::DynamicContext) -> Result<T> + Clone,
58    {
59        MapQuery {
60            query: self,
61            f,
62            v: std::marker::PhantomData,
63            t: std::marker::PhantomData,
64        }
65    }
66
67    /// Excute the query against an itemable
68    fn execute(&self, documents: &mut Documents, item: impl Itemable) -> Result<V> {
69        let context_item = item.to_item(documents)?;
70        self.execute_build_context(documents, move |builder| {
71            builder.context_item(context_item);
72        })
73    }
74
75    /// Execute a query with a specific dynamic context.
76    ///
77    /// This is useful if you want to build a dynamic context with specific
78    /// settings (such as variables), and then execute a query against it.
79    fn execute_build_context(
80        &self,
81        documents: &mut Documents,
82        build: impl FnOnce(&mut context::DynamicContextBuilder),
83    ) -> Result<V> {
84        let mut dynamic_context_builder = self.dynamic_context_builder(documents);
85        build(&mut dynamic_context_builder);
86        let context = dynamic_context_builder.build();
87        self.execute_with_context(documents, &context)
88    }
89}
90
91/// A recursive query that can be executed against an [`Itemable`]
92///
93/// It gives back a result of type `V`
94pub trait RecurseQuery<C, V> {
95    /// Get the program for the query
96    fn program(&self) -> &Program;
97
98    /// Get the static context for the query.
99    fn static_context(&self) -> &StaticContext {
100        self.program().static_context()
101    }
102
103    /// Execute the query against an itemable, with context.
104    ///
105    /// To do the conversion pass in a [`Recurse`] object. This
106    /// allows you to use a convert function recursively.
107    fn execute_with_context(
108        &self,
109        document: &mut Documents,
110        context: &context::DynamicContext,
111        recurse: &Recurse<V>,
112    ) -> Result<C>;
113
114    /// Get a dynamic context builder for the query, configured with the
115    /// query's static context and the document's documents.
116    ///
117    /// You can use this if you want to construct your own dynamic context
118    /// to use with `execute_with_context`.
119    fn dynamic_context_builder(&self, document: &Documents) -> context::DynamicContextBuilder {
120        let mut context = self.program().dynamic_context_builder();
121        context.documents(document.documents.clone());
122        context
123    }
124
125    /// Execute the query against an itemable.
126    ///
127    /// To do the conversion pass in a [`Recurse`] object. This
128    /// allows you to use a convert function recursively.
129    fn execute(&self, document: &mut Documents, item: &Item, recurse: &Recurse<V>) -> Result<C> {
130        self.execute_build_context(document, recurse, |builder| {
131            builder.context_item(item.clone());
132        })
133    }
134
135    /// Execute a query against an itemable, building the context.
136    ///
137    /// This is useful if you want to build a dynamic context with specific
138    /// settings (such as variables), and then execute a query against it.
139    fn execute_build_context(
140        &self,
141        document: &mut Documents,
142        recurse: &Recurse<V>,
143        build: impl FnOnce(&mut context::DynamicContextBuilder),
144    ) -> Result<C> {
145        let mut dynamic_context_builder = self.dynamic_context_builder(document);
146        build(&mut dynamic_context_builder);
147        let context = dynamic_context_builder.build();
148        self.execute_with_context(document, &context, recurse)
149    }
150}
151
152/// This is the core conversion function that can be used to turn
153/// an item that results from an XPath query into something useful
154/// in Rust.
155///
156/// Given a [`Documents`] and an [`Item`], convert the item to a value of type `V`
157pub trait Convert<V>: Fn(&mut Documents, &Item) -> Result<V> {}
158impl<V, T> Convert<V> for T where T: Fn(&mut Documents, &Item) -> Result<V> {}
159
160// Recursion was very hard to get right. The trick is to use an intermediate
161// struct.
162// https://stackoverflow.com/questions/16946888/is-it-possible-to-make-a-recursive-closure-in-rust
163
164// The dyn and reference are unavoidable, as closures are not allowed
165// to refer to themselves:
166// https://github.com/rust-lang/rust/issues/46062
167type RecurseFn<'s, V> = &'s dyn Fn(&mut Documents, &Item, &Recurse<'s, V>) -> Result<V>;
168
169/// An object that can be used to use a conversion function recursively.
170pub struct Recurse<'s, V> {
171    f: RecurseFn<'s, V>,
172}
173
174impl<'s, V> Recurse<'s, V> {
175    /// Create a new recurse object given a conversion function.
176    pub fn new(f: RecurseFn<'s, V>) -> Self {
177        Self { f }
178    }
179
180    /// Execute the conversion function against an item.
181    pub fn execute(&self, document: &mut Documents, item: &Item) -> Result<V> {
182        (self.f)(document, item, self)
183    }
184}
185
186/// This is a query that expects a sequence that contains exactly one single item.
187///
188/// Construct this using [`Queries::one`].
189///
190/// If it's empty or has more than one item, an error is returned.
191///
192/// The resulting item is converted into a Rust value using the `convert` function
193/// when constructing this query.
194///
195/// This is useful if you expect a single item to be returned from an XPath query.
196#[derive(Debug, Clone)]
197pub struct OneQuery<V, F>
198where
199    F: Convert<V>,
200{
201    pub(crate) program: Rc<Program>,
202    pub(crate) convert: F,
203    pub(crate) phantom: std::marker::PhantomData<V>,
204}
205
206impl<V, F> OneQuery<V, F>
207where
208    F: Convert<V>,
209{
210    /// Execute the query against a context
211    pub fn execute_with_context(
212        &self,
213        document: &mut Documents,
214        context: &context::DynamicContext,
215    ) -> Result<V> {
216        let sequence = self.program.runnable(context).many(document.xot_mut())?;
217        let item = sequence.one()?;
218        (self.convert)(document, &item)
219    }
220}
221
222impl<V, F> Query<V> for OneQuery<V, F>
223where
224    F: Convert<V>,
225{
226    fn program(&self) -> &Program {
227        &self.program
228    }
229
230    fn execute_with_context(
231        &self,
232        document: &mut Documents,
233        context: &context::DynamicContext,
234    ) -> Result<V> {
235        OneQuery::execute_with_context(self, document, context)
236    }
237}
238
239/// A recursive query that expects a single item as a result.
240#[derive(Debug, Clone)]
241pub struct OneRecurseQuery {
242    pub(crate) program: Rc<Program>,
243}
244
245impl OneRecurseQuery {
246    /// Execute the query against an itemable, with context.
247    ///
248    /// To do the conversion pass in a [`Recurse`] object. This
249    /// allows you to use a convert function recursively.
250    pub fn execute_with_context<V>(
251        &self,
252        document: &mut Documents,
253        context: &context::DynamicContext,
254        recurse: &Recurse<V>,
255    ) -> Result<V> {
256        let sequence = self.program.runnable(context).many(document.xot_mut())?;
257        let item = sequence.one()?;
258        recurse.execute(document, &item)
259    }
260}
261
262impl<V> RecurseQuery<V, V> for OneRecurseQuery {
263    fn program(&self) -> &Program {
264        &self.program
265    }
266
267    fn execute_with_context(
268        &self,
269        document: &mut Documents,
270        context: &context::DynamicContext,
271        recurse: &Recurse<V>,
272    ) -> Result<V> {
273        OneRecurseQuery::execute_with_context(self, document, context, recurse)
274    }
275}
276
277/// This is a query that expects an optional single item.
278///
279/// Construct this using ['Queries::option'].
280///
281/// If the sequence is empty, `None` is returned. If it contains more than one
282/// item, an error is returned.
283///
284/// The result is converted into a Rust value using the `convert` function
285/// when constructing this query.
286///
287/// This is useful if you expect an optional single item to be returned from an XPath query.
288#[derive(Debug, Clone)]
289pub struct OptionQuery<V, F>
290where
291    F: Convert<V>,
292{
293    pub(crate) program: Rc<Program>,
294    pub(crate) convert: F,
295    pub(crate) phantom: std::marker::PhantomData<V>,
296}
297
298impl<V, F> OptionQuery<V, F>
299where
300    F: Convert<V>,
301{
302    /// Execute the query against an itemable, with explicit
303    /// dynamic context.
304    pub fn execute_with_context(
305        &self,
306        document: &mut Documents,
307        context: &context::DynamicContext,
308    ) -> Result<Option<V>> {
309        let sequence = self.program.runnable(context).many(document.xot_mut())?;
310        let item = sequence.option()?;
311        item.map(|item| (self.convert)(document, &item)).transpose()
312    }
313}
314
315impl<V, F> Query<Option<V>> for OptionQuery<V, F>
316where
317    F: Convert<V>,
318{
319    fn program(&self) -> &Program {
320        &self.program
321    }
322
323    fn execute_with_context(
324        &self,
325        document: &mut Documents,
326        context: &context::DynamicContext,
327    ) -> Result<Option<V>> {
328        Self::execute_with_context(self, document, context)
329    }
330}
331
332/// A recursive query that expects an optional single item.
333#[derive(Debug, Clone)]
334pub struct OptionRecurseQuery {
335    pub(crate) program: Rc<Program>,
336}
337
338impl OptionRecurseQuery {
339    /// Execute the recursive query against an explicit dynamic context.
340    pub fn execute_with_context<V>(
341        &self,
342        document: &mut Documents,
343        context: &context::DynamicContext,
344        recurse: &Recurse<V>,
345    ) -> Result<Option<V>> {
346        let sequence = self.program.runnable(context).many(document.xot_mut())?;
347        let item = sequence.option()?;
348        item.map(|item| recurse.execute(document, &item))
349            .transpose()
350    }
351}
352
353impl<V> RecurseQuery<Option<V>, V> for OptionRecurseQuery {
354    fn program(&self) -> &Program {
355        &self.program
356    }
357
358    fn execute_with_context(
359        &self,
360        document: &mut Documents,
361        context: &context::DynamicContext,
362        recurse: &Recurse<V>,
363    ) -> Result<Option<V>> {
364        OptionRecurseQuery::execute_with_context(self, document, context, recurse)
365    }
366}
367
368/// A query that expects many items as a result.
369///
370/// Construct this using [`Queries::many`].
371///
372/// The items are converted into Rust values using the supplied `convert` function.
373///
374/// This is useful if you expect many items to be returned from an XPath query.
375///
376/// The result is converted into a Rust value using the `convert` function
377/// when constructing this query.
378#[derive(Debug, Clone)]
379pub struct ManyQuery<V, F>
380where
381    F: Convert<V>,
382{
383    pub(crate) program: Rc<Program>,
384    pub(crate) convert: F,
385    pub(crate) phantom: std::marker::PhantomData<V>,
386}
387
388impl<V, F> ManyQuery<V, F>
389where
390    F: Convert<V>,
391{
392    fn execute_with_context(
393        &self,
394        document: &mut Documents,
395        context: &context::DynamicContext,
396    ) -> Result<Vec<V>> {
397        let sequence = self.program.runnable(context).many(document.xot_mut())?;
398        let items = sequence
399            .iter()
400            .map(|item| (self.convert)(document, &item))
401            .collect::<Result<Vec<V>>>()?;
402        Ok(items)
403    }
404}
405
406impl<V, F> Query<Vec<V>> for ManyQuery<V, F>
407where
408    F: Convert<V>,
409{
410    fn program(&self) -> &Program {
411        &self.program
412    }
413
414    fn execute_with_context(
415        &self,
416        document: &mut Documents,
417        context: &context::DynamicContext,
418    ) -> Result<Vec<V>> {
419        Self::execute_with_context(self, document, context)
420    }
421}
422
423/// A recursive query that expects many items as a result.
424#[derive(Debug, Clone)]
425pub struct ManyRecurseQuery {
426    pub(crate) program: Rc<Program>,
427}
428
429impl ManyRecurseQuery {
430    /// Execute the query against an itemable, with variables.
431    ///
432    /// To do the conversion pass in a [`Recurse`] object. This
433    /// allows you to use a convert function recursively.
434    pub fn execute_with_context<V>(
435        &self,
436        document: &mut Documents,
437        context: &context::DynamicContext,
438        recurse: &Recurse<V>,
439    ) -> Result<Vec<V>> {
440        let sequence = self.program.runnable(context).many(document.xot_mut())?;
441        let items = sequence
442            .iter()
443            .map(|item| recurse.execute(document, &item))
444            .collect::<Result<Vec<V>>>()?;
445        Ok(items)
446    }
447}
448
449impl<V> RecurseQuery<Vec<V>, V> for ManyRecurseQuery {
450    fn program(&self) -> &Program {
451        &self.program
452    }
453
454    fn execute_with_context(
455        &self,
456        document: &mut Documents,
457        context: &context::DynamicContext,
458        recurse: &Recurse<V>,
459    ) -> Result<Vec<V>> {
460        ManyRecurseQuery::execute_with_context(self, document, context, recurse)
461    }
462}
463
464/// A query that returns a sequence.
465///
466/// This query returns a [`Sequence`] object that can be used to access
467/// the items in the sequence. It represents an XPath sequence. The items
468/// in the sequence are not converted.
469///
470/// Construct this using [`Queries::sequence`].
471///
472/// This is useful if you want to work with the sequence directly.
473#[derive(Debug, Clone)]
474pub struct SequenceQuery {
475    pub(crate) program: Rc<Program>,
476}
477
478impl SequenceQuery {
479    /// Execute the query against an itemable with an explict dynamic context.
480    pub fn execute_with_context(
481        &self,
482        document: &mut Documents,
483        context: &context::DynamicContext,
484    ) -> Result<Sequence> {
485        self.program.runnable(context).many(document.xot_mut())
486    }
487}
488
489impl Query<Sequence> for SequenceQuery {
490    fn program(&self) -> &Program {
491        &self.program
492    }
493
494    fn execute_with_context(
495        &self,
496        document: &mut Documents,
497        context: &context::DynamicContext,
498    ) -> Result<Sequence> {
499        Self::execute_with_context(self, document, context)
500    }
501}
502
503/// A query maps the result of another query to a different type.
504#[derive(Debug, Clone)]
505pub struct MapQuery<V, T, Q: Query<V> + Sized, F>
506where
507    F: Fn(V, &mut Documents, &context::DynamicContext) -> Result<T> + Clone,
508{
509    query: Q,
510    f: F,
511    v: std::marker::PhantomData<V>,
512    t: std::marker::PhantomData<T>,
513}
514
515impl<V, T, Q, F> MapQuery<V, T, Q, F>
516where
517    Q: Query<V> + Sized,
518    F: Fn(V, &mut Documents, &context::DynamicContext) -> Result<T> + Clone,
519{
520    /// Execute the query against an item.
521    pub fn execute(&self, document: &mut Documents, item: &Item) -> Result<T> {
522        let mut dynamic_context_builder = self.query.program().dynamic_context_builder();
523        dynamic_context_builder.context_item(item.clone());
524        let context = dynamic_context_builder.build();
525        self.execute_with_context(document, &context)
526    }
527
528    /// Execute the query against a dynamic context.
529    pub fn execute_with_context(
530        &self,
531        document: &mut Documents,
532        context: &context::DynamicContext,
533    ) -> Result<T> {
534        let v = self.query.execute_with_context(document, context)?;
535        // TODO: this isn't right. need to rewrite in terms of dynamic context too?
536        (self.f)(v, document, context)
537    }
538}
539
540impl<V, T, Q: Query<V> + Sized, F> Query<T> for MapQuery<V, T, Q, F>
541where
542    F: Fn(V, &mut Documents, &context::DynamicContext) -> Result<T> + Clone,
543{
544    fn program(&self) -> &Program {
545        self.query.program()
546    }
547
548    fn execute_with_context(
549        &self,
550        document: &mut Documents,
551        context: &context::DynamicContext,
552    ) -> Result<T> {
553        let v = self.query.execute_with_context(document, context)?;
554        (self.f)(v, document, context)
555    }
556}