From 61444b1ea35ef36091457f074ea1e7f7dea41302 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 4 Jul 2025 16:02:57 -0700 Subject: [PATCH 1/3] templates: improve rustdocs --- cli/src/commit_templater.rs | 3 +++ cli/src/operation_templater.rs | 3 +++ cli/src/template_builder.rs | 9 ++++++++- cli/src/templater.rs | 23 ++++++++++++++++++----- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index 4e74fdc6c5..e4c60bc348 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Template environment for `jj log`, `jj evolog` and similar. + use std::any::Any; use std::cmp::Ordering; use std::cmp::max; @@ -116,6 +118,7 @@ pub trait CommitTemplateLanguageExtension { fn build_cache_extensions(&self, extensions: &mut ExtensionsMap); } +/// Template environment for `jj log` and `jj evolog`. pub struct CommitTemplateLanguage<'repo> { repo: &'repo dyn Repo, path_converter: &'repo RepoPathUiConverter, diff --git a/cli/src/operation_templater.rs b/cli/src/operation_templater.rs index 3bd2d6cfe1..be6be79111 100644 --- a/cli/src/operation_templater.rs +++ b/cli/src/operation_templater.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Template environment for `jj op log`. + use std::any::Any; use std::cmp::Ordering; use std::collections::HashMap; @@ -58,6 +60,7 @@ pub trait OperationTemplateEnvironment { fn current_op_id(&self) -> Option<&OperationId>; } +/// Template environment for `jj op log`. pub struct OperationTemplateLanguage { repo_loader: RepoLoader, current_op_id: Option, diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index 2fbf0e273a..2902d8a79b 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -70,7 +70,11 @@ use crate::templater::WrapTemplateProperty; use crate::text_util; use crate::time_util; -/// Callbacks to build language-specific evaluation objects from AST nodes. +/// Callbacks to build usage-context-specific evaluation objects from AST nodes. +/// +/// This is used to implement different meanings of `self` or different +/// globally available functions in the template language depending on the +/// context in which it is invoked. pub trait TemplateLanguage<'a> { type Property: CoreTemplatePropertyVar<'a>; @@ -87,6 +91,8 @@ pub trait TemplateLanguage<'a> { function: &FunctionCallNode, ) -> TemplateParseResult; + /// Creates a method call thunk for the given `function` of the given + /// `property`. fn build_method( &self, diagnostics: &mut TemplateDiagnostics, @@ -713,6 +719,7 @@ impl<'a, P: CoreTemplatePropertyVar<'a>> Expression

{ } } +/// Environment (locals and self) in a stack frame. pub struct BuildContext<'i, P> { /// Map of functions to create `L::Property`. local_variables: HashMap<&'i str, &'i dyn Fn() -> P>, diff --git a/cli/src/templater.rs b/cli/src/templater.rs index 63e8104bcd..f50d419eab 100644 --- a/cli/src/templater.rs +++ b/cli/src/templater.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Tools for lazily evaluating templates that produce text in a fallible +//! manner. + use std::cell::RefCell; use std::error; use std::fmt; @@ -34,7 +37,11 @@ use crate::formatter::PlainTextFormatter; use crate::text_util; use crate::time_util; -/// Represents printable type or compiled template containing placeholder value. +/// Represents a printable type or a compiled template containing a placeholder +/// value. +/// +/// This is analogous to [`std::fmt::Display`], but with customized error +/// handling. pub trait Template { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()>; } @@ -315,7 +322,7 @@ pub struct TemplatePropertyError(pub Box); // Implements conversion from any error type to support `expr?` in function // binding. This type doesn't implement `std::error::Error` instead. -// https://github.com/dtolnay/anyhow/issues/25#issuecomment-544140480 +// impl From for TemplatePropertyError where E: error::Error + Send + Sync + 'static, @@ -325,6 +332,7 @@ where } } +/// Lazily evaluated value which can fail to evaluate. pub trait TemplateProperty { type Output; @@ -369,11 +377,12 @@ tuple_impls! { (0 T0, 1 T1, 2 T2, 3 T3) } +/// Type-erased [`TemplateProperty`]. pub type BoxedTemplateProperty<'a, O> = Box + 'a>; pub type BoxedSerializeProperty<'a> = BoxedTemplateProperty<'a, Box>; -/// `TemplateProperty` adapters that are useful when implementing methods. +/// [`TemplateProperty`] adapters that are useful when implementing methods. pub trait TemplatePropertyExt: TemplateProperty { /// Translates to a property that will apply fallible `function` to an /// extracted value. @@ -448,8 +457,8 @@ impl TemplatePropertyExt for P {} /// Wraps template property of type `O` in tagged type. /// -/// This is basically `From>`, but is restricted to -/// property types. +/// This is basically [`From>`], but is restricted +/// to property types. #[diagnostic::on_unimplemented( message = "the template property of type `{O}` cannot be wrapped in `{Self}`" )] @@ -594,6 +603,10 @@ where } } +/// Template which selects an output based on a boolean condition. +/// +/// When `None` is specified for the false template and the condition is false, +/// this writes nothing. pub struct ConditionalTemplate { pub condition: P, pub true_template: T, From 5fb3c61fdb84571f799f84b5b0f247eb270166ad Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 4 Jul 2025 16:02:57 -0700 Subject: [PATCH 2/3] templates: support string patterns in template language This is a basic implementation of the same string pattern system as in the revset language. It's currently only used for `string.matches`, so you can now do: ``` "foo".matches(regex:'[a-f]o+') ``` In the future this could be added to more string functions (and e.g. the ability to parse things out of strings could be added). CC: https://github.com/jj-vcs/jj/issues/6893 --- cli/src/template.pest | 14 +++++ cli/src/template_builder.rs | 14 +++++ cli/src/template_parser.rs | 103 ++++++++++++++++++++++++++++++++++-- docs/templates.md | 18 ++++++- 4 files changed, 144 insertions(+), 5 deletions(-) diff --git a/cli/src/template.pest b/cli/src/template.pest index 7a2ad77507..eeac31a0e2 100644 --- a/cli/src/template.pest +++ b/cli/src/template.pest @@ -33,6 +33,8 @@ string_literal = ${ "\"" ~ (string_content | string_escape)* ~ "\"" } raw_string_content = @{ (!"'" ~ ANY)* } raw_string_literal = ${ "'" ~ raw_string_content ~ "'" } +any_string_literal = _{ string_literal | raw_string_literal } + integer_literal = @{ ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* | "0" @@ -56,6 +58,7 @@ div_op = { "/" } rem_op = { "%" } logical_not_op = { "!" } negate_op = { "-" } +pattern_kind_op = { ":" } prefix_ops = _{ logical_not_op | negate_op } infix_ops = _{ logical_or_op @@ -88,10 +91,21 @@ formal_parameters = { | "" } +// NOTE: string pattern identifiers additionally allow "-" in them, which +// results in some oddness with the `-` operator, though does not yet cause +// ambiguity. This may prove annoying at some future point. +string_pattern_identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")* } +string_pattern = ${ + // Unlike the revset language, we're not allowing bare words here because + // templates are generally not written on-the-fly. + string_pattern_identifier ~ pattern_kind_op ~ any_string_literal +} + primary = _{ ("(" ~ template ~ ")") | function | lambda + | string_pattern | identifier | string_literal | raw_string_literal diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index 2902d8a79b..a4a4f46fe3 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -1946,6 +1946,10 @@ pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>( let property = Literal(value.clone()).into_dyn_wrapped(); Ok(Expression::unlabeled(property)) } + ExpressionKind::StringPattern { .. } => Err(TemplateParseError::expression( + "String patterns may not be used as expression values", + node.span, + )), ExpressionKind::Unary(op, arg_node) => { let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?; Ok(Expression::unlabeled(property)) @@ -2969,6 +2973,16 @@ mod tests { env.render_ok(r#""description 123".contains(description.first_line())"#), @"true"); + // String patterns are not stringifiable + insta::assert_snapshot!(env.parse_err(r#""fa".starts_with(regex:'[a-f]o+')"#), @r#" + --> 1:18 + | + 1 | "fa".starts_with(regex:'[a-f]o+') + | ^-------------^ + | + = String patterns may not be used as expression values + "#); + // inner template error should propagate insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @""); insta::assert_snapshot!( diff --git a/cli/src/template_parser.rs b/cli/src/template_parser.rs index 5c7cc3b46d..a4e53b1325 100644 --- a/cli/src/template_parser.rs +++ b/cli/src/template_parser.rs @@ -33,6 +33,7 @@ use jj_lib::dsl_util::FunctionCallParser; use jj_lib::dsl_util::InvalidArguments; use jj_lib::dsl_util::StringLiteralParser; use jj_lib::dsl_util::collect_similar; +use jj_lib::str_util::StringPattern; use pest::Parser as _; use pest::iterators::Pair; use pest::iterators::Pairs; @@ -69,6 +70,7 @@ impl Rule { Self::string_literal => None, Self::raw_string_content => None, Self::raw_string_literal => None, + Self::any_string_literal => None, Self::integer_literal => None, Self::identifier => None, Self::concat_op => Some("++"), @@ -87,6 +89,7 @@ impl Rule { Self::rem_op => Some("%"), Self::logical_not_op => Some("!"), Self::negate_op => Some("-"), + Self::pattern_kind_op => Some(":"), Self::prefix_ops => None, Self::infix_ops => None, Self::function => None, @@ -95,6 +98,8 @@ impl Rule { Self::function_arguments => None, Self::lambda => None, Self::formal_parameters => None, + Self::string_pattern_identifier => None, + Self::string_pattern => None, Self::primary => None, Self::term => None, Self::expression => None, @@ -285,6 +290,11 @@ pub enum ExpressionKind<'i> { Boolean(bool), Integer(i64), String(String), + /// `:""` + StringPattern { + kind: &'i str, + value: String, + }, Unary(UnaryOp, Box>), Binary(BinaryOp, Box>, Box>), Concat(Vec>), @@ -302,7 +312,10 @@ impl<'i> FoldableExpression<'i> for ExpressionKind<'i> { { match self { Self::Identifier(name) => folder.fold_identifier(name, span), - Self::Boolean(_) | Self::Integer(_) | Self::String(_) => Ok(self), + ExpressionKind::Boolean(_) + | ExpressionKind::Integer(_) + | ExpressionKind::String(_) + | ExpressionKind::StringPattern { .. } => Ok(self), Self::Unary(op, arg) => { let arg = Box::new(folder.fold_expression(*arg)?); Ok(Self::Unary(op, arg)) @@ -458,6 +471,12 @@ fn parse_lambda_node(pair: Pair) -> TemplateParseResult { }) } +fn parse_raw_string_literal(pair: Pair) -> String { + let [content] = pair.into_inner().collect_array().unwrap(); + assert_eq!(content.as_rule(), Rule::raw_string_content); + content.as_str().to_owned() +} + fn parse_term_node(pair: Pair) -> TemplateParseResult { assert_eq!(pair.as_rule(), Rule::term); let mut inner = pair.into_inner(); @@ -469,9 +488,7 @@ fn parse_term_node(pair: Pair) -> TemplateParseResult { ExpressionNode::new(ExpressionKind::String(text), span) } Rule::raw_string_literal => { - let [content] = expr.into_inner().collect_array().unwrap(); - assert_eq!(content.as_rule(), Rule::raw_string_content); - let text = content.as_str().to_owned(); + let text = parse_raw_string_literal(expr); ExpressionNode::new(ExpressionKind::String(text), span) } Rule::integer_literal => { @@ -480,6 +497,21 @@ fn parse_term_node(pair: Pair) -> TemplateParseResult { })?; ExpressionNode::new(ExpressionKind::Integer(value), span) } + Rule::string_pattern => { + let [kind, op, literal] = expr.into_inner().collect_array().unwrap(); + assert_eq!(kind.as_rule(), Rule::string_pattern_identifier); + assert_eq!(op.as_rule(), Rule::pattern_kind_op); + let kind = kind.as_str(); + let text = match literal.as_rule() { + Rule::string_literal => STRING_LITERAL_PARSER.parse(literal.into_inner()), + Rule::raw_string_literal => parse_raw_string_literal(literal), + other => { + panic!("Unexpected literal rule in string pattern: {other:?}") + } + }; + // The actual parsing and construction of the pattern is deferred to later. + ExpressionNode::new(ExpressionKind::StringPattern { kind, value: text }, span) + } Rule::identifier => ExpressionNode::new(parse_identifier_or_literal(expr), span), Rule::function => { let function = Box::new(FUNCTION_CALL_PARSER.parse( @@ -663,6 +695,23 @@ pub fn expect_string_literal<'a>(node: &'a ExpressionNode<'_>) -> TemplateParseR }) } +/// Unwraps inner value if the given `node` is a string pattern +/// +/// This forces it to be static so that it need not be part of the type system. +pub fn expect_string_pattern(node: &ExpressionNode<'_>) -> TemplateParseResult { + catch_aliases_no_diagnostics(node, |node| match &node.kind { + ExpressionKind::StringPattern { kind, value } => StringPattern::from_str_kind(value, kind) + .map_err(|err| { + TemplateParseError::expression("Bad string pattern", node.span).with_source(err) + }), + ExpressionKind::String(string) => Ok(StringPattern::Substring(string.clone())), + _ => Err(TemplateParseError::expression( + "Expected string pattern", + node.span, + )), + }) +} + /// Unwraps inner node if the given `node` is a lambda. pub fn expect_lambda<'a, 'i>( node: &'a ExpressionNode<'i>, @@ -835,6 +884,7 @@ mod tests { | ExpressionKind::Boolean(_) | ExpressionKind::Integer(_) | ExpressionKind::String(_) => node.kind, + ExpressionKind::StringPattern { .. } => node.kind, ExpressionKind::Unary(op, arg) => { let arg = Box::new(normalize_tree(*arg)); ExpressionKind::Unary(op, arg) @@ -1139,6 +1189,51 @@ mod tests { ); } + #[test] + fn test_string_pattern() { + assert_eq!( + parse_into_kind(r#"regex:"meow""#), + Ok(ExpressionKind::StringPattern { + kind: "regex", + value: "meow".to_owned() + }), + ); + assert_eq!( + parse_into_kind(r#"regex:'\r\n'"#), + Ok(ExpressionKind::StringPattern { + kind: "regex", + value: r#"\r\n"#.to_owned() + }) + ); + assert_eq!( + parse_into_kind(r#"regex-i:'\r\n'"#), + Ok(ExpressionKind::StringPattern { + kind: "regex-i", + value: r#"\r\n"#.to_owned() + }) + ); + assert_eq!( + parse_into_kind("regex:meow"), + Err(TemplateParseErrorKind::SyntaxError), + "no bare words in string patterns in templates" + ); + assert_eq!( + parse_into_kind("regex: 'with spaces'"), + Err(TemplateParseErrorKind::SyntaxError), + "no spaces after" + ); + assert_eq!( + parse_into_kind("regex :'with spaces'"), + Err(TemplateParseErrorKind::SyntaxError), + "no spaces before either" + ); + assert_eq!( + parse_into_kind("regex : 'with spaces'"), + Err(TemplateParseErrorKind::SyntaxError), + "certainly not both" + ); + } + #[test] fn test_integer_literal() { assert_eq!(parse_into_kind("0"), Ok(ExpressionKind::Integer(0))); diff --git a/docs/templates.md b/docs/templates.md index 9fab4e3afb..888a7fc359 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -424,7 +424,8 @@ A string can be implicitly converted to `Boolean`. The following methods are defined. * `.len() -> Integer`: Length in UTF-8 bytes. -* `.contains(needle: Stringify) -> Boolean` +* `.contains(needle: Stringify) -> Boolean`: Whether the string contains the + provided stringifiable value as a substring. * `.first_line() -> String` * `.lines() -> List`: Split into lines excluding newline characters. * `.upper() -> String` @@ -474,6 +475,21 @@ An expression that can be converted to a `String`. Any types that can be converted to `Template` can also be `Stringify`. Unlike `Template`, color labels are stripped. +### `StringPattern` type + +_Conversion: `Boolean`: no, `Serialize`: no, `Template`: no_ + +These are the exact same as the [String pattern type] in revsets, except that +quotes are mandatory. + +Literal strings may be used, which are interpreted as case-sensitive substring +matching. + +Currently `StringPattern` values cannot be passed around as values and may +only occur directly in the call site they are used in. + +[String pattern type]: revsets.md#string-patterns + ### `Template` type _Conversion: `Boolean`: no, `Serialize`: no, `Template`: yes_ From a4f17048e6106491aecd068b3cb350c6845717ab Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Thu, 17 Jul 2025 15:31:42 -0700 Subject: [PATCH 3/3] templates: add string.match function This allows for any matcher type and allows extracting a capture group by number. --- CHANGELOG.md | 3 ++ cli/src/template_builder.rs | 45 ++++++++++++++++++++++++++++++ docs/templates.md | 4 +++ lib/src/str_util.rs | 55 +++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed592c4c8c..7c72e918b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). output using template expressions, similar to `jj op log`. Also added `--no-op-diff` flag to suppress the operation diff. +* A nearly identical string pattern system as revsets is now supported in the + template language, and is exposed as `string.match(pattern)`. + ### Fixed bugs * `jj git clone` now correctly fetches all tags, unless `--fetch-tags` is diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index a4a4f46fe3..6a0b6ccd61 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -909,6 +909,25 @@ fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>() Ok(out_property.into_dyn_wrapped()) }, ); + map.insert( + "match", + |_language, _diagnostics, _build_ctx, self_property, function| { + let [needle_node] = function.expect_exact_arguments()?; + let needle = template_parser::expect_string_pattern(needle_node)?; + let regex = needle.to_regex(); + + let out_property = self_property.and_then(move |haystack| { + if let Some(m) = regex.find(haystack.as_bytes()) { + Ok(std::str::from_utf8(m.as_bytes())?.to_owned()) + } else { + // We don't have optional strings, so empty string is the + // right null value. + Ok(String::new()) + } + }); + Ok(out_property.into_dyn_wrapped()) + }, + ); map.insert( "starts_with", |language, diagnostics, build_ctx, self_property, function| { @@ -2990,6 +3009,32 @@ mod tests { insta::assert_snapshot!( env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @""); + insta::assert_snapshot!(env.render_ok(r#""fooo".match(regex:'[a-f]o+')"#), @"fooo"); + insta::assert_snapshot!(env.render_ok(r#""fa".match(regex:'[a-f]o+')"#), @""); + insta::assert_snapshot!(env.render_ok(r#""hello".match(regex:"h(ell)o")"#), @"hello"); + insta::assert_snapshot!(env.render_ok(r#""HEllo".match(regex-i:"h(ell)o")"#), @"HEllo"); + insta::assert_snapshot!(env.render_ok(r#""hEllo".match(glob:"h*o")"#), @"hEllo"); + insta::assert_snapshot!(env.render_ok(r#""Hello".match(glob:"h*o")"#), @""); + insta::assert_snapshot!(env.render_ok(r#""HEllo".match(glob-i:"h*o")"#), @"HEllo"); + insta::assert_snapshot!(env.render_ok(r#""hello".match("he")"#), @"he"); + insta::assert_snapshot!(env.render_ok(r#""hello".match(substring:"he")"#), @"he"); + insta::assert_snapshot!(env.render_ok(r#""hello".match(exact:"he")"#), @""); + + // Evil regexes can cause invalid UTF-8 output, which nothing can + // really be done about given we're matching against non-UTF-8 stuff a + // lot as well. + insta::assert_snapshot!(env.render_ok(r#""🥺".match(regex:'(?-u)^(?:.)')"#), @""); + + insta::assert_snapshot!(env.parse_err(r#""🥺".match(not-a-pattern:"abc")"#), @r#" + --> 1:11 + | + 1 | "🥺".match(not-a-pattern:"abc") + | ^-----------------^ + | + = Bad string pattern + Invalid string pattern kind `not-a-pattern:` + "#); + insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @""); insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo"); diff --git a/docs/templates.md b/docs/templates.md index 888a7fc359..f036974c63 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -426,6 +426,10 @@ defined. * `.len() -> Integer`: Length in UTF-8 bytes. * `.contains(needle: Stringify) -> Boolean`: Whether the string contains the provided stringifiable value as a substring. +* `.match(needle: StringPattern) -> String`: Extracts + the first matching part of the string for the given pattern. + + An empty string is returned if there is no match. * `.first_line() -> String` * `.lines() -> List`: Split into lines excluding newline characters. * `.upper() -> String` diff --git a/lib/src/str_util.rs b/lib/src/str_util.rs index 31308f49a2..e3e83399c7 100644 --- a/lib/src/str_util.rs +++ b/lib/src/str_util.rs @@ -60,6 +60,11 @@ impl GlobPattern { pub fn as_str(&self) -> &str { self.glob.glob() } + + /// Converts this glob pattern to a bytes regex. + pub fn to_regex(&self) -> regex::bytes::Regex { + self.regex.clone() + } } impl Debug for GlobPattern { @@ -289,6 +294,35 @@ impl StringPattern { } } + /// Converts the pattern into a bytes regex. + pub fn to_regex(&self) -> regex::bytes::Regex { + match self { + Self::Exact(literal) => { + regex::bytes::RegexBuilder::new(&format!("^{}$", regex::escape(literal))) + .build() + .expect("impossible to fail to compile regex of literal") + } + Self::ExactI(literal) => { + regex::bytes::RegexBuilder::new(&format!("^{}$", regex::escape(literal))) + .case_insensitive(true) + .build() + .expect("impossible to fail to compile regex of literal") + } + Self::Substring(literal) => regex::bytes::RegexBuilder::new(®ex::escape(literal)) + .build() + .expect("impossible to fail to compile regex of literal"), + Self::SubstringI(literal) => regex::bytes::RegexBuilder::new(®ex::escape(literal)) + .case_insensitive(true) + .build() + .expect("impossible to fail to compile regex of literal"), + Self::Glob(glob_pattern) => glob_pattern.to_regex(), + // The regex generated represents the case insensitivity itself + Self::GlobI(glob_pattern) => glob_pattern.to_regex(), + Self::Regex(regex) => regex.clone(), + Self::RegexI(regex) => regex.clone(), + } + } + /// Iterates entries of the given `map` whose string keys match this /// pattern. pub fn filter_btree_map<'a, 'b, K: Borrow + Ord, V>( @@ -487,4 +521,25 @@ mod tests { .is_match("\u{c0}") ); } + + #[test] + fn test_string_pattern_to_regex() { + let check = |pattern: StringPattern, match_to: &str| { + let regex = pattern.to_regex(); + regex.is_match(match_to.as_bytes()) + }; + assert!(check(StringPattern::exact("$a"), "$a")); + assert!(!check(StringPattern::exact("$a"), "$A")); + assert!(!check(StringPattern::exact("a"), "aa")); + assert!(!check(StringPattern::exact("a"), "aa")); + assert!(check(StringPattern::exact_i("a"), "A")); + assert!(check(StringPattern::substring("$a"), "$abc")); + assert!(!check(StringPattern::substring("$a"), "$Abc")); + assert!(check(StringPattern::substring_i("$a"), "$Abc")); + assert!(!check(StringPattern::glob("a").unwrap(), "A")); + assert!(check(StringPattern::glob_i("a").unwrap(), "A")); + assert!(check(StringPattern::regex("^a{1,3}").unwrap(), "abcde")); + assert!(!check(StringPattern::regex("^a{1,3}").unwrap(), "Abcde")); + assert!(check(StringPattern::regex_i("^a{1,3}").unwrap(), "Abcde")); + } }