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}