From a8107aed3ad37e7655a7c640cf141f3393e67eeb Mon Sep 17 00:00:00 2001 From: Chip Senkbeil Date: Thu, 8 Sep 2022 19:16:48 -0500 Subject: [PATCH] Update SearchQueryCondition to support logical or and contains types; also update non-regex types to escape regex --- CHANGELOG.md | 5 ++ Cargo.lock | 1 + distant-core/Cargo.toml | 1 + distant-core/src/data/search.rs | 89 ++++++++++++++++++++++++++++++--- 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8441dc8..05d8307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- New `contains` and `or` types for `SearchQueryCondition` + ### Changed +- `SearchQueryCondition` now escapes regex for all types except `regex` - Removed `min_depth` option from search - Updated search to properly use binary detection, filter out common ignore file patterns, and execute in parallel via the `ignore` crate and `num_cpus` diff --git a/Cargo.lock b/Cargo.lock index a60a5aa..bdc2fe2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,6 +772,7 @@ dependencies = [ "portable-pty", "predicates", "rand 0.8.5", + "regex", "rstest", "schemars", "serde", diff --git a/distant-core/Cargo.toml b/distant-core/Cargo.toml index ea23d00..7b33729 100644 --- a/distant-core/Cargo.toml +++ b/distant-core/Cargo.toml @@ -30,6 +30,7 @@ num_cpus = "1.13.1" once_cell = "1.13.0" portable-pty = "0.7.0" rand = { version = "0.8.5", features = ["getrandom"] } +regex = "1.1" serde = { version = "1.0.142", features = ["derive"] } serde_bytes = "0.11.7" serde_json = "1.0.83" diff --git a/distant-core/src/data/search.rs b/distant-core/src/data/search.rs index dfe2bd2..cdf393c 100644 --- a/distant-core/src/data/search.rs +++ b/distant-core/src/data/search.rs @@ -70,20 +70,33 @@ impl SearchQueryTarget { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")] pub enum SearchQueryCondition { - /// Begins with some text + /// Text is found anywhere (all regex patterns are escaped) + Contains { value: String }, + + /// Begins with some text (all regex patterns are escaped) EndsWith { value: String }, - /// Matches some text exactly + /// Matches some text exactly (all regex patterns are escaped) Equals { value: String }, + /// Any of the conditions match + Or { value: Vec }, + /// Matches some regex Regex { value: String }, - /// Begins with some text + /// Begins with some text (all regex patterns are escaped) StartsWith { value: String }, } impl SearchQueryCondition { + /// Creates a new instance with `Contains` variant + pub fn contains(value: impl Into) -> Self { + Self::Contains { + value: value.into(), + } + } + /// Creates a new instance with `EndsWith` variant pub fn ends_with(value: impl Into) -> Self { Self::EndsWith { @@ -98,6 +111,17 @@ impl SearchQueryCondition { } } + /// Creates a new instance with `Or` variant + pub fn or(value: I) -> Self + where + I: IntoIterator, + C: Into, + { + Self::Or { + value: value.into_iter().map(|s| s.into()).collect(), + } + } + /// Creates a new instance with `Regex` variant pub fn regex(value: impl Into) -> Self { Self::Regex { @@ -115,10 +139,21 @@ impl SearchQueryCondition { /// Converts the condition in a regex string pub fn to_regex_string(&self) -> String { match self { - Self::EndsWith { value } => format!(r"{value}$"), - Self::Equals { value } => format!(r"^{value}$"), + Self::Contains { value } => regex::escape(value), + Self::EndsWith { value } => format!(r"{}$", regex::escape(value)), + Self::Equals { value } => format!(r"^{}$", regex::escape(value)), Self::Regex { value } => value.to_string(), - Self::StartsWith { value } => format!(r"^{value}"), + Self::StartsWith { value } => format!(r"^{}", regex::escape(value)), + Self::Or { value } => { + let mut s = String::new(); + for (i, condition) in value.iter().enumerate() { + if i > 0 { + s.push('|'); + } + s.push_str(&condition.to_regex_string()); + } + s + } } } } @@ -349,3 +384,45 @@ impl SearchQueryMatchData { schemars::schema_for!(SearchQueryMatchData) } } + +#[cfg(test)] +mod tests { + use super::*; + + mod search_query_condition { + use super::*; + + #[test] + fn to_regex_string_should_convert_to_appropriate_regex_and_escape_as_needed() { + assert_eq!( + SearchQueryCondition::contains("t^es$t").to_regex_string(), + r"t\^es\$t" + ); + assert_eq!( + SearchQueryCondition::ends_with("t^es$t").to_regex_string(), + r"t\^es\$t$" + ); + assert_eq!( + SearchQueryCondition::equals("t^es$t").to_regex_string(), + r"^t\^es\$t$" + ); + assert_eq!( + SearchQueryCondition::or([ + SearchQueryCondition::contains("t^es$t"), + SearchQueryCondition::equals("t^es$t"), + SearchQueryCondition::regex("^test$"), + ]) + .to_regex_string(), + r"t\^es\$t|^t\^es\$t$|^test$" + ); + assert_eq!( + SearchQueryCondition::regex("test").to_regex_string(), + "test" + ); + assert_eq!( + SearchQueryCondition::starts_with("t^es$t").to_regex_string(), + r"^t\^es\$t" + ); + } + } +}