git_bug/query/
mod.rs

1// git-bug-rs - A rust library for interfacing with git-bug repositories
2//
3// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
4// SPDX-License-Identifier: GPL-3.0-or-later
5//
6// This file is part of git-bug-rs/git-gub.
7//
8// You should have received a copy of the License along with this program.
9// If not, see <https://www.gnu.org/licenses/agpl.txt>.
10
11//! # The query language supported by `git-bug-rs`
12//!
13//! The language is not really connected to any state on disk, but is a quite
14//! convenient tool to query the on-disk state. As such it is currently part of
15//! `git-bug-rs`.
16//!
17//! In general, the EBNF grammar of the query language is as follows:
18//!
19//! ```ebnf
20//! Query = Matcher;
21//!
22//! Matcher = Or | And | MatchKey;
23//!
24//! Or = "(" Matcher Break "OR" Break Matcher ")";
25//! And = "(" Matcher Break "AND" Break Matcher ")";
26//!
27//! Break = " ";
28//!
29//! MatchKey = Key ":" Value;
30//! Key = {CHAR};   {* Further specified by the Queryable object *}
31//! Value = {CHAR}; {* Further specified by the Queryable object *}
32//! ```
33//!
34//! This is obviously rather unwieldy to expect people to actually input (e.g.,
35//! just querying for open issues that contain the string “test” in their title
36//! would take: `(status:open AND title:test)`. If we now also wanted to add a
37//! specific label to the query, we would need to write `((status:open AND
38//! title:test) AND label:ack)`.)
39//!
40//! To avoid this, the query language has parsers for two forms, a strict one
41//! and a relaxed one.
42//!
43//! The relaxed one tries to simplify the query language by inserting defaults
44//! or making educated guesses; it that can be normalized through insertion of
45//! default conjunctions or keys to the strict query language.
46//!
47//! The strict parser rejects all input that does not comply with the EBNF
48//! grammar.
49//!
50//! For example, following expressions would be accepted with the relaxed
51//! parser:
52//! - `status:open title:test label:ack` (implicitly inserting
53//!   [`ANDs`][`crate::query::parse::tokenizer::TokenKind::And`] between the `MatchKey`s)
54//! - `systemd stage 1 init` (implicitly inserting
55//!   [`ANDs`][`crate::query::parse::tokenizer::TokenKind::And`] and search keys for each value.)
56//!
57//! See the test cases in the strict and relaxed parser for more examples.
58
59use queryable::{QueryKeyValue, Queryable};
60
61use crate::query::parse::splitter;
62
63pub mod normalize;
64pub mod parse;
65pub mod queryable;
66
67/// The container and root for queries.
68///
69/// See the module documentation for a explanation.
70#[derive(Debug, Clone)]
71pub struct Query<E: Queryable> {
72    root: Option<Matcher<E>>,
73}
74
75/// How to parse this expression.
76#[derive(Debug, Clone, Copy)]
77pub enum ParseMode {
78    /// Follow the specified query language to the letter.
79    ///
80    /// This should always return the exact query you specified without shifts
81    /// in priority.
82    Strict,
83
84    /// Try to interpret the best you can, if the query string does not match
85    /// exactly.
86    ///
87    /// This can (and probably will) insert descending query priorities from
88    /// right-to-left.
89    Relaxed,
90}
91
92impl<E: Queryable> Query<E> {
93    /// Construct this Query from a continuous string. This will correctly split
94    /// the string according to shell splitting rules (i.e., it takes double
95    /// and single quotes into account). This is useful, if you only don't
96    /// have a shell in between taking this string from your user. If your
97    /// already have a split string, use [`Query::from_slice`].
98    ///
99    /// # Errors
100    /// If the input does not parse as a [`Query`].
101    pub fn from_continuous_str(
102        user_state: &<E::KeyValue as QueryKeyValue>::UserState,
103        s: &str,
104        parse_mode: ParseMode,
105    ) -> Result<Query<E>, parse::parser::Error<E>>
106    where
107        <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
108    {
109        let split: Vec<String> = splitter::Splitter::new(s, ' ').collect();
110        Self::from_slice(user_state, split.iter().map(String::as_str), parse_mode)
111    }
112
113    /// Construct this Query from an already split up string. This will assume
114    /// that similar parts are in one split (i.e., `title:Nice AND
115    /// status:open`, should resolve to three distinct splits).
116    /// Only use this function if your input is already split up by something
117    /// like a UNIX shell. Otherwise, you can use
118    /// [`Query::from_continuous_str`] to supply a (correctly quoted)
119    /// string.
120    ///
121    /// # Errors
122    /// If the input does not parse as a [`Query`].
123    pub fn from_slice<'a, T>(
124        user_state: &<E::KeyValue as QueryKeyValue>::UserState,
125        s: T,
126        parse_mode: ParseMode,
127    ) -> Result<Query<E>, parse::parser::Error<E>>
128    where
129        T: Iterator<Item = &'a str>,
130        <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
131    {
132        if let Some(tokenizer) = parse::tokenizer::Tokenizer::from_slice(s) {
133            let mut parser = parse::parser::Parser::new(user_state, tokenizer);
134            parser.parse(parse_mode)
135        } else {
136            Ok(Query { root: None })
137        }
138    }
139
140    /// Check whether this [`Query`] matches the [`Queryable`] object.
141    pub fn matches(&self, object: &E) -> bool {
142        let Some(root) = &self.root else {
143            // An empty query will always match.
144            return true;
145        };
146
147        root.matches(object)
148    }
149
150    /// Construct this [`Query`] from a [`Matcher`].
151    ///
152    /// This is useful, if you need to construct a query or want to compose
153    /// multiple queries together (for example, after accessing matchers via [`Query::as_matcher`]).
154    pub fn from_matcher(matcher: Matcher<E>) -> Self {
155        Self {
156            root: Some(matcher),
157        }
158    }
159
160    /// Get access to the underlying [`Matcher`] of this [`Query`].
161    pub fn as_matcher(&self) -> Option<&Matcher<E>> {
162        self.root.as_ref()
163    }
164
165    /// Turn this [`Query`] into its underlying [`Matcher`].
166    pub fn into_matcher(self) -> Option<Matcher<E>> {
167        self.root
168    }
169
170    /// Get access to the underlying mutable [`Matcher`] of this [`Query`].
171    pub fn as_mut_matcher(&mut self) -> Option<&mut Matcher<E>> {
172        self.root.as_mut()
173    }
174}
175
176/// A node in the [`Query`] AST.
177#[derive(Debug, Clone)]
178pub enum Matcher<E: Queryable> {
179    /// An OR matches if either of its branches match.
180    Or {
181        /// The left-hand-side of this node.
182        lhs: Box<Matcher<E>>,
183
184        /// The right-hand-side of this node.
185        rhs: Box<Matcher<E>>,
186    },
187
188    /// An AND matches only if both of its branches match.
189    And {
190        /// The left-hand-side of this node.
191        lhs: Box<Matcher<E>>,
192
193        /// The right-hand-side of this node.
194        rhs: Box<Matcher<E>>,
195    },
196
197    /// A match expression is the basic building block of a [`Query`].
198    /// It matches if its `key_value` matches (i.e., [`Queryable::matches`] with this as argument)
199    Match {
200        /// The key and it's value.
201        ///
202        /// ```text
203        /// status:open
204        /// ^^^^^^ ^^^^
205        ///    |    |
206        ///    |    +---> Value
207        ///    |
208        ///    +------> Key
209        /// ```
210        key_value: E::KeyValue,
211    },
212}
213
214impl<E: Queryable> Matcher<E> {
215    /// Check whether this [`Matcher`] matches the [`Queryable`] object.
216    fn matches(&self, object: &E) -> bool {
217        match self {
218            Self::Or { lhs, rhs } => lhs.matches(object) || rhs.matches(object),
219            Self::And { lhs, rhs } => lhs.matches(object) && rhs.matches(object),
220            Self::Match { key_value } => object.matches(key_value),
221        }
222    }
223}