166 lines · 5.7 KB · Rust
RawDownload
// Copyright 2021-2023 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. //! String helpers. use std::borrow::{Borrow, Cow};use std::collections::BTreeMap;use std::fmt; use either::Either;use thiserror::Error; /// Error occurred during pattern string parsing.#[derive(Debug, Error)]pub enum StringPatternParseError {    /// Unknown pattern kind is specified.    #[error(r#"Invalid string pattern kind "{0}""#)]    InvalidKind(String),    /// Failed to parse glob pattern.    #[error(transparent)]    GlobPattern(glob::PatternError),} /// Pattern to be tested against string property like commit description or/// branch name.#[derive(Clone, Debug, Eq, PartialEq)]pub enum StringPattern {    /// Matches strings exactly equal to `string`.    Exact(String),    /// Unix-style shell wildcard pattern.    Glob(glob::Pattern),    /// Matches strings that contain `substring`.    Substring(String),} impl StringPattern {    /// Pattern that matches any string.    pub const fn everything() -> Self {        StringPattern::Substring(String::new())    }     /// Creates pattern that matches exactly.    pub fn exact(src: impl Into<String>) -> Self {        StringPattern::Exact(src.into())    }     /// Parses the given string as glob pattern.    pub fn glob(src: &str) -> Result<Self, StringPatternParseError> {        // TODO: might be better to do parsing and compilation separately since        // not all backends would use the compiled pattern object.        // TODO: if no meta character found, it can be mapped to Exact.        let pattern = glob::Pattern::new(src).map_err(StringPatternParseError::GlobPattern)?;        Ok(StringPattern::Glob(pattern))    }     /// Parses the given string as pattern of the specified `kind`.    pub fn from_str_kind(src: &str, kind: &str) -> Result<Self, StringPatternParseError> {        match kind {            "exact" => Ok(StringPattern::exact(src)),            "glob" => StringPattern::glob(src),            "substring" => Ok(StringPattern::Substring(src.to_owned())),            _ => Err(StringPatternParseError::InvalidKind(kind.to_owned())),        }    }     /// Returns true if this pattern matches input strings exactly.    pub fn is_exact(&self) -> bool {        self.as_exact().is_some()    }     /// Returns a literal pattern if this should match input strings exactly.    ///    /// This can be used to optimize map lookup by exact key.    pub fn as_exact(&self) -> Option<&str> {        match self {            StringPattern::Exact(literal) => Some(literal),            StringPattern::Glob(_) | StringPattern::Substring(_) => None,        }    }     /// Returns the original string of this pattern.    pub fn as_str(&self) -> &str {        match self {            StringPattern::Exact(literal) => literal,            StringPattern::Glob(pattern) => pattern.as_str(),            StringPattern::Substring(needle) => needle,        }    }     /// Converts this pattern to a glob string. Returns `None` if the pattern    /// can't be represented as a glob.    pub fn to_glob(&self) -> Option<Cow<'_, str>> {        // TODO: If we add Regex pattern, it will return None.        match self {            StringPattern::Exact(literal) => Some(glob::Pattern::escape(literal).into()),            StringPattern::Glob(pattern) => Some(pattern.as_str().into()),            StringPattern::Substring(needle) if needle.is_empty() => Some("*".into()),            StringPattern::Substring(needle) => {                Some(format!("*{}*", glob::Pattern::escape(needle)).into())            }        }    }     /// Returns true if this pattern matches the `haystack`.    pub fn matches(&self, haystack: &str) -> bool {        match self {            StringPattern::Exact(literal) => haystack == literal,            StringPattern::Glob(pattern) => pattern.matches(haystack),            StringPattern::Substring(needle) => haystack.contains(needle),        }    }     /// Iterates entries of the given `map` whose keys matches this pattern.    pub fn filter_btree_map<'a: 'b, 'b, K: Borrow<str> + Ord, V>(        &'b self,        map: &'a BTreeMap<K, V>,    ) -> impl Iterator<Item = (&'a K, &'a V)> + 'b {        if let Some(key) = self.as_exact() {            Either::Left(map.get_key_value(key).into_iter())        } else {            Either::Right(map.iter().filter(|&(key, _)| self.matches(key.borrow())))        }    }} impl fmt::Display for StringPattern {    /// Shows the original string of this pattern.    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        write!(f, "{}", self.as_str())    }} #[cfg(test)]mod tests {    use super::*;     #[test]    fn test_string_pattern_to_glob() {        assert_eq!(StringPattern::everything().to_glob(), Some("*".into()));        assert_eq!(StringPattern::exact("a").to_glob(), Some("a".into()));        assert_eq!(StringPattern::exact("*").to_glob(), Some("[*]".into()));        assert_eq!(            StringPattern::glob("*").unwrap().to_glob(),            Some("*".into())        );        assert_eq!(            StringPattern::Substring("a".into()).to_glob(),            Some("*a*".into())        );        assert_eq!(            StringPattern::Substring("*".into()).to_glob(),            Some("*[*]*".into())        );    }}