git_bug/query/parse/parser/
strict.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 strict query parser
12
13use crate::query::{
14    Matcher,
15    parse::{
16        parser::{format_unexpected_token, format_unknown_key},
17        tokenizer::{Token, TokenKind, TokenSpan},
18    },
19    queryable::{QueryKeyValue, Queryable},
20};
21
22#[derive(Debug, thiserror::Error)]
23#[allow(missing_docs)]
24pub enum Error<E: Queryable>
25where
26    <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
27{
28    #[error(fmt = format_unexpected_token)]
29    UnexpectedToken { expected: TokenKind, found: Token },
30
31    /// An unexpected token was found, and multiple possible token kinds were
32    /// possible.
33    #[error("The input stream contained {found} but expected one of: {expected:?}")]
34    UnexpectedTokens {
35        /// The token kinds that could have occurred.
36        expected: Vec<TokenKind>,
37
38        /// The token we actually found.
39        found: Token,
40    },
41
42    #[error(fmt = format_unknown_key::<E>)]
43    UnknownKey {
44        /// The returned error
45        err: <E::KeyValue as QueryKeyValue>::Err,
46
47        /// The key we found.
48        key: String,
49
50        /// The span this key takes up.
51        at: TokenSpan,
52    },
53
54    #[error("The Query was not completely parsed. Parsing stopped at: {0}")]
55    LeftoverTokens(Token),
56}
57
58impl<E: Queryable> From<super::parsing::Error<E>> for Error<E>
59where
60    <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
61{
62    fn from(value: super::parsing::Error<E>) -> Self {
63        match value {
64            super::parsing::Error::UnexpectedToken { expected, found } => {
65                Self::UnexpectedToken { expected, found }
66            }
67            super::parsing::Error::UnknownKey { err, key, at } => Self::UnknownKey { err, key, at },
68        }
69    }
70}
71
72pub(super) struct Parser<'a, E: Queryable> {
73    parent: &'a mut super::Parser<'a, E>,
74}
75
76impl<'a, E: Queryable> Parser<'a, E>
77where
78    <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
79{
80    pub(super) fn parse(parent: &'a mut super::Parser<'a, E>) -> Result<Matcher<E>, Error<E>> {
81        let mut me = Parser { parent };
82        let output = me.parse_matcher()?;
83
84        let next = me.parent.tokenizer.next_token();
85        if next.kind() == TokenKind::Eof {
86            Ok(output)
87        } else {
88            Err(Error::LeftoverTokens(next))
89        }
90    }
91
92    fn parse_matcher(&mut self) -> Result<Matcher<E>, Error<E>> {
93        if self.parent.tokenizer.peek().kind() == TokenKind::ParenOpen {
94            // We got an AND or an OR.
95            self.parent.expect(TokenKind::ParenOpen)?;
96
97            let lhs = self.parse_matcher()?;
98            self.parent.expect(TokenKind::Break)?;
99
100            let mode = match self.parent.tokenizer.next_token() {
101                token if token.kind == TokenKind::And => TokenKind::And,
102                token if token.kind == TokenKind::Or => TokenKind::Or,
103                other => {
104                    return Err(Error::UnexpectedTokens {
105                        expected: vec![TokenKind::And, TokenKind::Or],
106                        found: other,
107                    });
108                }
109            };
110
111            self.parent.expect(TokenKind::Break)?;
112            let rhs = self.parse_matcher()?;
113            self.parent.expect(TokenKind::ParenClose)?;
114
115            match mode {
116                TokenKind::And => Ok(Matcher::And {
117                    lhs: Box::new(lhs),
118                    rhs: Box::new(rhs),
119                }),
120                TokenKind::Or => Ok(Matcher::Or {
121                    lhs: Box::new(lhs),
122                    rhs: Box::new(rhs),
123                }),
124                _ => unreachable!("Checked above"),
125            }
126        } else {
127            // We got an MatchKey
128            let key_tokens = self
129                .parent
130                .take_while(|t| matches!(t, TokenKind::Char(_)))?;
131            self.parent.expect(TokenKind::Colon)?;
132
133            let value_tokens = self
134                .parent
135                .take_while(|t| matches!(t, TokenKind::Char(_)))?;
136            let value = super::Parser::<'a, E>::parse_value_from(&value_tokens);
137
138            let key_value = self.parent.parse_key_from(
139                &key_tokens,
140                value,
141                value_tokens.last().map_or(0, |t| t.span().stop()),
142            )?;
143
144            Ok(Matcher::Match { key_value })
145        }
146    }
147}
148
149#[cfg(test)]
150mod test {
151    use pretty_assertions::assert_eq;
152
153    use crate::query::{
154        Matcher::{And, Match, Or},
155        ParseMode, Query,
156        parse::parser::test::{
157            QueryTestKey1::{Value1, Value2},
158            QueryTestKeyValue::{Key1, Key2, Key3},
159            QueryTestObj,
160        },
161    };
162
163    #[test]
164    fn test_simple_query() {
165        let input = "(key1:value1 AND 'key2:state p2 󰵘')";
166        let query = Query::<QueryTestObj>::from_continuous_str(&(), input, ParseMode::Strict)
167            .map_err(|err| panic!("{err}"))
168            .unwrap();
169
170        assert_eq!(
171            query,
172            Query {
173                root: Some(And {
174                    lhs: Box::new(Match {
175                        key_value: Key1(Value1),
176                    }),
177                    rhs: Box::new(Match {
178                        key_value: Key2("state p2 \u{f0d58}".to_owned()),
179                    }),
180                },)
181            }
182        );
183    }
184
185    #[test]
186    fn test_complexer_query() {
187        let input = "((key1:value1 AND 'key2:state p2 󰵘') OR ((key1:value2 AND 'key2:state 󰵘') \
188                     AND key3:20))";
189        let query = Query::<QueryTestObj>::from_continuous_str(&(), input, ParseMode::Strict)
190            .map_err(|err| panic!("{err}"))
191            .unwrap();
192
193        assert_eq!(
194            query,
195            Query {
196                root: Some(Or {
197                    lhs: Box::new(And {
198                        lhs: Box::new(Match {
199                            key_value: Key1(Value1,),
200                        }),
201                        rhs: Box::new(Match {
202                            key_value: Key2("state p2 \u{f0d58}".to_owned()),
203                        }),
204                    }),
205                    rhs: Box::new(And {
206                        lhs: Box::new(And {
207                            lhs: Box::new(Match {
208                                key_value: Key1(Value2),
209                            }),
210                            rhs: Box::new(Match {
211                                key_value: Key2("state \u{f0d58}".to_owned()),
212                            }),
213                        }),
214                        rhs: Box::new(Match {
215                            key_value: Key3 {
216                                _value: 20,
217                                original: "20".to_owned(),
218                            },
219                        }),
220                    }),
221                },),
222            }
223        );
224    }
225}