git-bug 0.2.4

A rust library for interfacing with git-bug repositories
Documentation
// git-bug-rs - A rust library for interfacing with git-bug repositories
//
// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of git-bug-rs/git-gub.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/agpl.txt>.

//! The strict query parser

use crate::query::{
    Matcher,
    parse::{
        parser::{format_unexpected_token, format_unknown_key},
        tokenizer::{Token, TokenKind, TokenSpan},
    },
    queryable::{QueryKeyValue, Queryable},
};

#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error<E: Queryable>
where
    <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
{
    #[error(fmt = format_unexpected_token)]
    UnexpectedToken { expected: TokenKind, found: Token },

    /// An unexpected token was found, and multiple possible token kinds were
    /// possible.
    #[error("The input stream contained {found} but expected one of: {expected:?}")]
    UnexpectedTokens {
        /// The token kinds that could have occurred.
        expected: Vec<TokenKind>,

        /// The token we actually found.
        found: Token,
    },

    #[error(fmt = format_unknown_key::<E>)]
    UnknownKey {
        /// The returned error
        err: <E::KeyValue as QueryKeyValue>::Err,

        /// The key we found.
        key: String,

        /// The span this key takes up.
        at: TokenSpan,
    },

    #[error("The Query was not completely parsed. Parsing stopped at: {0}")]
    LeftoverTokens(Token),
}

impl<E: Queryable> From<super::parsing::Error<E>> for Error<E>
where
    <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
{
    fn from(value: super::parsing::Error<E>) -> Self {
        match value {
            super::parsing::Error::UnexpectedToken { expected, found } => {
                Self::UnexpectedToken { expected, found }
            }
            super::parsing::Error::UnknownKey { err, key, at } => Self::UnknownKey { err, key, at },
        }
    }
}

pub(super) struct Parser<'a, E: Queryable> {
    parent: &'a mut super::Parser<'a, E>,
}

impl<'a, E: Queryable> Parser<'a, E>
where
    <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
{
    pub(super) fn parse(parent: &'a mut super::Parser<'a, E>) -> Result<Matcher<E>, Error<E>> {
        let mut me = Parser { parent };
        let output = me.parse_matcher()?;

        let next = me.parent.tokenizer.next_token();
        if next.kind() == TokenKind::Eof {
            Ok(output)
        } else {
            Err(Error::LeftoverTokens(next))
        }
    }

    fn parse_matcher(&mut self) -> Result<Matcher<E>, Error<E>> {
        if self.parent.tokenizer.peek().kind() == TokenKind::ParenOpen {
            // We got an AND or an OR.
            self.parent.expect(TokenKind::ParenOpen)?;

            let lhs = self.parse_matcher()?;
            self.parent.expect(TokenKind::Break)?;

            let mode = match self.parent.tokenizer.next_token() {
                token if token.kind == TokenKind::And => TokenKind::And,
                token if token.kind == TokenKind::Or => TokenKind::Or,
                other => {
                    return Err(Error::UnexpectedTokens {
                        expected: vec![TokenKind::And, TokenKind::Or],
                        found: other,
                    });
                }
            };

            self.parent.expect(TokenKind::Break)?;
            let rhs = self.parse_matcher()?;
            self.parent.expect(TokenKind::ParenClose)?;

            match mode {
                TokenKind::And => Ok(Matcher::And {
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                }),
                TokenKind::Or => Ok(Matcher::Or {
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                }),
                _ => unreachable!("Checked above"),
            }
        } else {
            // We got an MatchKey
            let key_tokens = self
                .parent
                .take_while(|t| matches!(t, TokenKind::Char(_)))?;
            self.parent.expect(TokenKind::Colon)?;

            let value_tokens = self
                .parent
                .take_while(|t| matches!(t, TokenKind::Char(_)))?;
            let value = super::Parser::<'a, E>::parse_value_from(&value_tokens);

            let key_value = self.parent.parse_key_from(
                &key_tokens,
                value,
                value_tokens.last().map_or(0, |t| t.span().stop()),
            )?;

            Ok(Matcher::Match { key_value })
        }
    }
}

#[cfg(test)]
mod test {
    use pretty_assertions::assert_eq;

    use crate::query::{
        Matcher::{And, Match, Or},
        ParseMode, Query,
        parse::parser::test::{
            QueryTestKey1::{Value1, Value2},
            QueryTestKeyValue::{Key1, Key2, Key3},
            QueryTestObj,
        },
    };

    #[test]
    fn test_simple_query() {
        let input = "(key1:value1 AND 'key2:state p2 󰵘')";
        let query = Query::<QueryTestObj>::from_continuous_str(&(), input, ParseMode::Strict)
            .map_err(|err| panic!("{err}"))
            .unwrap();

        assert_eq!(
            query,
            Query {
                root: Some(And {
                    lhs: Box::new(Match {
                        key_value: Key1(Value1),
                    }),
                    rhs: Box::new(Match {
                        key_value: Key2("state p2 \u{f0d58}".to_owned()),
                    }),
                },)
            }
        );
    }

    #[test]
    fn test_complexer_query() {
        let input = "((key1:value1 AND 'key2:state p2 󰵘') OR ((key1:value2 AND 'key2:state 󰵘') \
                     AND key3:20))";
        let query = Query::<QueryTestObj>::from_continuous_str(&(), input, ParseMode::Strict)
            .map_err(|err| panic!("{err}"))
            .unwrap();

        assert_eq!(
            query,
            Query {
                root: Some(Or {
                    lhs: Box::new(And {
                        lhs: Box::new(Match {
                            key_value: Key1(Value1,),
                        }),
                        rhs: Box::new(Match {
                            key_value: Key2("state p2 \u{f0d58}".to_owned()),
                        }),
                    }),
                    rhs: Box::new(And {
                        lhs: Box::new(And {
                            lhs: Box::new(Match {
                                key_value: Key1(Value2),
                            }),
                            rhs: Box::new(Match {
                                key_value: Key2("state \u{f0d58}".to_owned()),
                            }),
                        }),
                        rhs: Box::new(Match {
                            key_value: Key3 {
                                _value: 20,
                                original: "20".to_owned(),
                            },
                        }),
                    }),
                },),
            }
        );
    }
}