bumpversion 0.0.9

Update all version strings in your project and optionally commit and tag the changes
Documentation
use crate::f_string::{MissingArgumentError, PythonFormatString};
use std::collections::HashMap;

/// Wrapper around [`regex::Regex`] with stable ordering and hashing.
#[derive(Debug, Clone)]
pub struct Regex(pub regex::Regex);

impl std::ops::Deref for Regex {
    type Target = regex::Regex;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<'a> TryFrom<&'a str> for Regex {
    type Error = regex::Error;
    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
        regex::RegexBuilder::new(value).build().map(Self)
    }
}

impl std::fmt::Display for Regex {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        std::fmt::Display::fmt(&self.0, f)
    }
}

impl Ord for Regex {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        std::cmp::Ord::cmp(self.0.as_str(), other.0.as_str())
    }
}

impl PartialOrd for Regex {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(std::cmp::Ord::cmp(&self, &other))
    }
}

impl PartialEq for Regex {
    fn eq(&self, other: &Self) -> bool {
        std::cmp::PartialEq::eq(self.0.as_str(), other.0.as_str())
    }
}

impl Eq for Regex {}

impl std::hash::Hash for Regex {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.0.as_str().hash(state);
    }
}

impl From<regex::Regex> for Regex {
    fn from(value: regex::Regex) -> Self {
        Self(value)
    }
}

#[derive(thiserror::Error, Debug, PartialEq)]
/// Errors that can occur when formatting a [`RegexTemplate`].
pub enum RegexTemplateError {
    #[error(transparent)]
    /// Required template argument is missing.
    MissingArgument(#[from] MissingArgumentError),
    #[error(transparent)]
    /// Regex compilation error.
    Regex(#[from] regex::Error),
}

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// Template for producing regex patterns from format strings.
pub enum RegexTemplate {
    /// The formatted string is treated as a regex (values are escaped).
    Regex(PythonFormatString),
    /// The formatted string is escaped so it matches literally.
    Escaped(PythonFormatString),
}

impl std::fmt::Display for RegexTemplate {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_ref())
    }
}

impl AsRef<PythonFormatString> for RegexTemplate {
    fn as_ref(&self) -> &PythonFormatString {
        match self {
            Self::Regex(s) | Self::Escaped(s) => s,
        }
    }
}

impl RegexTemplate {
    #[must_use]
    /// Returns `true` if this template produces a regex pattern.
    pub fn is_regex(&self) -> bool {
        matches!(self, Self::Regex(_))
    }

    #[must_use]
    /// Returns `true` if this template produces an escaped literal pattern.
    pub fn is_escaped(&self) -> bool {
        matches!(self, Self::Escaped(_))
    }

    /// Format this template using `values` and compile it into a [`regex::Regex`].
    pub fn format<K, V>(
        &self,
        values: &HashMap<K, V>,
        strict: bool,
    ) -> Result<regex::Regex, RegexTemplateError>
    where
        K: std::borrow::Borrow<str>,
        K: std::hash::Hash + Eq,
        V: AsRef<str>,
    {
        let raw_pattern = match self {
            Self::Regex(format_string) => {
                let escaped_values: HashMap<&str, String> = values
                    .iter()
                    .map(|(k, v)| (k.borrow(), regex::escape(v.as_ref())))
                    .collect();

                format_string.format(&escaped_values, strict)?
            }
            Self::Escaped(format_string) => {
                let raw_pattern = format_string.format(values, strict)?;
                regex::escape(&raw_pattern)
            }
        };
        let pattern = regex::RegexBuilder::new(&raw_pattern)
            .multi_line(true)
            .build()?;
        Ok(pattern)
    }
}