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>.

//! Parser implementation for the query language.

use super::tokenizer::{Token, TokenSpan, Tokenizer};
use crate::query::{
    ParseMode, Query,
    parse::tokenizer::TokenKind,
    queryable::{QueryKeyValue, Queryable},
};

pub mod relaxed;
pub mod strict;

#[derive(Debug, thiserror::Error)]
/// The error returned by [`Query::from_slice`] and
/// [`Query::from_continuous_str`].
pub enum Error<E: Queryable>
where
    <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
{
    #[error(transparent)]
    /// The errors from the strict parser
    Strict(#[from] strict::Error<E>),

    #[error(transparent)]
    /// The errors from the relaxed parser
    Relaxed(#[from] relaxed::Error<E>),
}

// The argument passing is not in our control
#[allow(clippy::trivially_copy_pass_by_ref)]
fn format_unexpected_token(
    expected: &TokenKind,
    found: &Token,
    f: &mut std::fmt::Formatter<'_>,
) -> Result<(), std::fmt::Error> {
    write!(
        f,
        "Expected token '{expected}' but got token '{found}' instead."
    )
}
fn format_unknown_key<E: Queryable>(
    err: &<E::KeyValue as QueryKeyValue>::Err,
    key: &str,
    _at: &TokenSpan,
    f: &mut std::fmt::Formatter<'_>,
) -> Result<(), std::fmt::Error>
where
    <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
{
    write!(f, "The input stream countained a unknown key {key}: {err}")
}

#[allow(missing_docs)]
pub mod parsing {
    use crate::query::{
        parse::{
            parser::format_unexpected_token,
            tokenizer::{Token, TokenKind, TokenSpan},
        },
        queryable::{QueryKeyValue, Queryable},
    };

    #[derive(Debug, thiserror::Error)]
    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 },

        /// A key in a `MatchKey` expression was unknown.
        #[error("The input stream countained a unknown key {key}: {err}")]
        UnknownKey {
            /// The returned error
            err: <E::KeyValue as QueryKeyValue>::Err,

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

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

pub(crate) struct Parser<'a, E: Queryable> {
    tokenizer: Tokenizer<'a>,
    user_state: &'a <E::KeyValue as QueryKeyValue>::UserState,
}

impl<'a, E: Queryable> Parser<'a, E>
where
    <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
{
    pub(crate) fn new(
        user_state: &'a <E::KeyValue as QueryKeyValue>::UserState,
        tokenizer: Tokenizer<'a>,
    ) -> Self {
        Self {
            tokenizer,
            user_state,
        }
    }

    /// Start the parsing process and drive it to completion
    pub(crate) fn parse(&'a mut self, mode: ParseMode) -> Result<Query<E>, Error<E>> {
        let matcher = match mode {
            ParseMode::Strict => strict::Parser::parse(self)?,
            ParseMode::Relaxed => relaxed::Parser::parse(self)?,
        };

        Ok(Query {
            root: Some(matcher),
        })
    }

    fn parse_key_from(
        &self,
        key_tokens: &[Token],
        value: String,
        value_tokens_end: usize,
    ) -> Result<E::KeyValue, parsing::Error<E>> {
        let key = key_tokens.iter().fold(String::new(), |mut acc, t| {
            if let TokenKind::Char(ch) = t.kind {
                acc.push(ch);
                acc
            } else {
                unreachable!("We filtered by chars in the `take_while`");
            }
        });

        E::KeyValue::from_key_value(self.user_state, &key, value).map_err(|err| {
            parsing::Error::UnknownKey {
                err,
                key,
                at: TokenSpan {
                    start: key_tokens.first().expect("Exists").span.start,
                    stop: value_tokens_end,
                },
            }
        })
    }

    fn parse_value_from(tokens: &[Token]) -> String {
        tokens.iter().fold(String::new(), |mut acc, t| {
            if let TokenKind::Char(ch) = t.kind {
                acc.push(ch);
                acc
            } else {
                unreachable!("We filtered by Chars()s in the `take_while`");
            }
        })
    }

    fn expect(&mut self, expected: TokenKind) -> Result<Token, parsing::Error<E>> {
        let next = self.tokenizer.next_token();
        if next.kind == expected {
            Ok(next)
        } else {
            Err(parsing::Error::UnexpectedToken {
                expected,
                found: next,
            })
        }
    }

    fn take_while<F>(&mut self, pred: F) -> Result<Vec<Token>, parsing::Error<E>>
    where
        F: Fn(TokenKind) -> bool,
    {
        let mut output = vec![];
        loop {
            let token = self.tokenizer.peek();

            if !pred(token.kind) {
                return Ok(output);
            }

            output.push(self.tokenizer.next_token());
        }
    }
}

#[cfg(test)]
pub(crate) mod test {
    macro_rules! query {
        ($name:ident {
            lhs: $lhs:tt,
            rhs: $rhs:tt $(,)?
        }) => {
            Query {
                root: Some($name {
                    lhs: Box::new(query!(@run $lhs)),
                    rhs: Box::new(query!(@run $rhs)),
                })
            }
        };

        (@run {$name:ident {
            lhs: $lhs:tt,
            rhs: $rhs:tt $(,)?
        } $(,)?}) => {
                $name {
                    lhs: Box::new(query!(@run $lhs)),
                    rhs: Box::new(query!(@run $rhs)),
                }
        };
        (@run { Match(Key1 , $value:tt) }) => {
            Match {
                key_value: Key1($value),
            }
        };
        (@run { Match(Key2 , $value:expr) }) => {
            Match {
                key_value: Key2($value.to_owned()),
            }
        };
        (@run { Match(Key3 , $number:expr) }) => {
            Match {
                key_value: Key3 {
                    _value: $number,
                    original: stringify!($number).to_owned(),
                },
            }
        };
    }
    pub(crate) use query;

    use crate::query::{
        Matcher, Query,
        queryable::{QueryKeyValue, Queryable},
    };

    impl PartialEq for Query<QueryTestObj> {
        fn eq(&self, other: &Self) -> bool {
            if let Some(me) = &self.root {
                if let Some(other) = &other.root {
                    me == other
                } else {
                    false
                }
            } else {
                self.root.is_none() && other.root.is_none()
            }
        }
    }

    impl PartialEq for Matcher<QueryTestObj> {
        fn eq(&self, other: &Self) -> bool {
            match self {
                Matcher::Or {
                    lhs: me_lhs,
                    rhs: me_rhs,
                } => match other {
                    Matcher::Or { lhs, rhs } => me_lhs == lhs && me_rhs == rhs,
                    Matcher::And { .. } | Matcher::Match { .. } => false,
                },
                Matcher::And {
                    lhs: me_lhs,
                    rhs: me_rhs,
                } => match other {
                    Matcher::And { lhs, rhs } => me_lhs == lhs && me_rhs == rhs,
                    Matcher::Or { .. } | Matcher::Match { .. } => false,
                },
                Matcher::Match {
                    key_value: me_key_value,
                } => match other {
                    Matcher::Or { .. } | Matcher::And { .. } => false,
                    Matcher::Match { key_value } => key_value == me_key_value,
                },
            }
        }
    }

    #[derive(Debug)]
    pub(crate) struct QueryTestObj;

    impl Queryable for QueryTestObj {
        type KeyValue = QueryTestKeyValue;

        fn matches(&self, _key: &Self::KeyValue) -> bool {
            true
        }
    }

    #[derive(Debug, PartialEq, Eq, Clone)]
    pub(crate) enum QueryTestKeyValue {
        Key1(QueryTestKey1),
        Key2(String),
        Key3 { _value: u32, original: String },
    }

    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
    pub(crate) enum QueryTestKey1 {
        Value1,
        Value2,
    }

    impl QueryKeyValue for QueryTestKeyValue {
        type Err = QueryTestError;
        type UserState = ();

        fn from_key_value(
            _user_state: &Self::UserState,
            key: &str,
            value: String,
        ) -> Result<Self, Self::Err>
        where
            Self: Sized,
        {
            match key {
                "key1" => Ok(Self::Key1(match value.as_str() {
                    "value1" => QueryTestKey1::Value1,
                    "value2" => QueryTestKey1::Value2,
                    other => todo!("{other}"),
                })),
                "key2" => Ok(Self::Key2(value)),
                "key3" => Ok(Self::Key3 {
                    _value: value.parse().unwrap(),
                    original: value,
                }),
                other => todo!("{other}"),
            }
        }

        fn from_value(_user_state: &Self::UserState, value: String) -> Result<Self, Self::Err>
        where
            Self: Sized,
        {
            Ok(Self::Key2(value))
        }

        fn to_key_and_value(&self) -> (&str, &str)
        where
            Self: Sized,
        {
            match self {
                QueryTestKeyValue::Key1(query_test_key1) => {
                    let query_test_key1_str = match query_test_key1 {
                        QueryTestKey1::Value1 => "value1",
                        QueryTestKey1::Value2 => "value2",
                    };

                    ("key1", query_test_key1_str)
                }
                QueryTestKeyValue::Key2(val) => ("key2", val.as_str()),
                QueryTestKeyValue::Key3 { original, .. } => ("key3", original.as_str()),
            }
        }
    }

    #[derive(Debug, thiserror::Error)]
    pub(crate) enum QueryTestError {}
}