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 query language supported by `git-bug-rs`
//!
//! The language is not really connected to any state on disk, but is a quite
//! convenient tool to query the on-disk state. As such it is currently part of
//! `git-bug-rs`.
//!
//! In general, the EBNF grammar of the query language is as follows:
//!
//! ```ebnf
//! Query = Matcher;
//!
//! Matcher = Or | And | MatchKey;
//!
//! Or = "(" Matcher Break "OR" Break Matcher ")";
//! And = "(" Matcher Break "AND" Break Matcher ")";
//!
//! Break = " ";
//!
//! MatchKey = Key ":" Value;
//! Key = {CHAR};   {* Further specified by the Queryable object *}
//! Value = {CHAR}; {* Further specified by the Queryable object *}
//! ```
//!
//! This is obviously rather unwieldy to expect people to actually input (e.g.,
//! just querying for open issues that contain the string “test” in their title
//! would take: `(status:open AND title:test)`. If we now also wanted to add a
//! specific label to the query, we would need to write `((status:open AND
//! title:test) AND label:ack)`.)
//!
//! To avoid this, the query language has parsers for two forms, a strict one
//! and a relaxed one.
//!
//! The relaxed one tries to simplify the query language by inserting defaults
//! or making educated guesses; it that can be normalized through insertion of
//! default conjunctions or keys to the strict query language.
//!
//! The strict parser rejects all input that does not comply with the EBNF
//! grammar.
//!
//! For example, following expressions would be accepted with the relaxed
//! parser:
//! - `status:open title:test label:ack` (implicitly inserting
//!   [`ANDs`][`crate::query::parse::tokenizer::TokenKind::And`] between the `MatchKey`s)
//! - `systemd stage 1 init` (implicitly inserting
//!   [`ANDs`][`crate::query::parse::tokenizer::TokenKind::And`] and search keys for each value.)
//!
//! See the test cases in the strict and relaxed parser for more examples.

use queryable::{QueryKeyValue, Queryable};

use crate::query::parse::splitter;

pub mod normalize;
pub mod parse;
pub mod queryable;

/// The container and root for queries.
///
/// See the module documentation for a explanation.
#[derive(Debug, Clone)]
pub struct Query<E: Queryable> {
    root: Option<Matcher<E>>,
}

/// How to parse this expression.
#[derive(Debug, Clone, Copy)]
pub enum ParseMode {
    /// Follow the specified query language to the letter.
    ///
    /// This should always return the exact query you specified without shifts
    /// in priority.
    Strict,

    /// Try to interpret the best you can, if the query string does not match
    /// exactly.
    ///
    /// This can (and probably will) insert descending query priorities from
    /// right-to-left.
    Relaxed,
}

impl<E: Queryable> Query<E> {
    /// Construct this Query from a continuous string. This will correctly split
    /// the string according to shell splitting rules (i.e., it takes double
    /// and single quotes into account). This is useful, if you only don't
    /// have a shell in between taking this string from your user. If your
    /// already have a split string, use [`Query::from_slice`].
    ///
    /// # Errors
    /// If the input does not parse as a [`Query`].
    pub fn from_continuous_str(
        user_state: &<E::KeyValue as QueryKeyValue>::UserState,
        s: &str,
        parse_mode: ParseMode,
    ) -> Result<Query<E>, parse::parser::Error<E>>
    where
        <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
    {
        let split: Vec<String> = splitter::Splitter::new(s, ' ').collect();
        Self::from_slice(user_state, split.iter().map(String::as_str), parse_mode)
    }

    /// Construct this Query from an already split up string. This will assume
    /// that similar parts are in one split (i.e., `title:Nice AND
    /// status:open`, should resolve to three distinct splits).
    /// Only use this function if your input is already split up by something
    /// like a UNIX shell. Otherwise, you can use
    /// [`Query::from_continuous_str`] to supply a (correctly quoted)
    /// string.
    ///
    /// # Errors
    /// If the input does not parse as a [`Query`].
    pub fn from_slice<'a, T>(
        user_state: &<E::KeyValue as QueryKeyValue>::UserState,
        s: T,
        parse_mode: ParseMode,
    ) -> Result<Query<E>, parse::parser::Error<E>>
    where
        T: Iterator<Item = &'a str>,
        <E::KeyValue as QueryKeyValue>::Err: std::fmt::Debug + std::fmt::Display,
    {
        if let Some(tokenizer) = parse::tokenizer::Tokenizer::from_slice(s) {
            let mut parser = parse::parser::Parser::new(user_state, tokenizer);
            parser.parse(parse_mode)
        } else {
            Ok(Query { root: None })
        }
    }

    /// Check whether this [`Query`] matches the [`Queryable`] object.
    pub fn matches(&self, object: &E) -> bool {
        let Some(root) = &self.root else {
            // An empty query will always match.
            return true;
        };

        root.matches(object)
    }

    /// Construct this [`Query`] from a [`Matcher`].
    ///
    /// This is useful, if you need to construct a query or want to compose
    /// multiple queries together (for example, after accessing matchers via [`Query::as_matcher`]).
    pub fn from_matcher(matcher: Matcher<E>) -> Self {
        Self {
            root: Some(matcher),
        }
    }

    /// Get access to the underlying [`Matcher`] of this [`Query`].
    pub fn as_matcher(&self) -> Option<&Matcher<E>> {
        self.root.as_ref()
    }

    /// Turn this [`Query`] into its underlying [`Matcher`].
    pub fn into_matcher(self) -> Option<Matcher<E>> {
        self.root
    }

    /// Get access to the underlying mutable [`Matcher`] of this [`Query`].
    pub fn as_mut_matcher(&mut self) -> Option<&mut Matcher<E>> {
        self.root.as_mut()
    }
}

/// A node in the [`Query`] AST.
#[derive(Debug, Clone)]
pub enum Matcher<E: Queryable> {
    /// An OR matches if either of its branches match.
    Or {
        /// The left-hand-side of this node.
        lhs: Box<Matcher<E>>,

        /// The right-hand-side of this node.
        rhs: Box<Matcher<E>>,
    },

    /// An AND matches only if both of its branches match.
    And {
        /// The left-hand-side of this node.
        lhs: Box<Matcher<E>>,

        /// The right-hand-side of this node.
        rhs: Box<Matcher<E>>,
    },

    /// A match expression is the basic building block of a [`Query`].
    /// It matches if its `key_value` matches (i.e., [`Queryable::matches`] with this as argument)
    Match {
        /// The key and it's value.
        ///
        /// ```text
        /// status:open
        /// ^^^^^^ ^^^^
        ///    |    |
        ///    |    +---> Value
        ///    |
        ///    +------> Key
        /// ```
        key_value: E::KeyValue,
    },
}

impl<E: Queryable> Matcher<E> {
    /// Check whether this [`Matcher`] matches the [`Queryable`] object.
    fn matches(&self, object: &E) -> bool {
        match self {
            Self::Or { lhs, rhs } => lhs.matches(object) || rhs.matches(object),
            Self::And { lhs, rhs } => lhs.matches(object) && rhs.matches(object),
            Self::Match { key_value } => object.matches(key_value),
        }
    }
}