From e7ae12f59504490964f128e27c857d94a0bf526b Mon Sep 17 00:00:00 2001 From: Joshua Rakita Date: Fri, 2 Aug 2024 22:47:11 -0700 Subject: [PATCH] Implement startswith, endswith, add test coverage. --- src/filter/lexer.rs | 21 +++++++++++++++++++-- src/filter/mod.rs | 32 ++++++++++++++++++++++++++++++++ src/filter/parser.rs | 2 +- src/filter/token.rs | 4 ++++ src/filter/value.rs | 20 ++++++++++++++++++++ src/telemetry/mod.rs | 2 +- 6 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/filter/lexer.rs b/src/filter/lexer.rs index 8fc01d9..8a38927 100644 --- a/src/filter/lexer.rs +++ b/src/filter/lexer.rs @@ -87,6 +87,8 @@ impl<'a> Scanner<'a> { "true" => Ok(Token::True), "contains" => Ok(Token::Contains), "in" => Ok(Token::In), + "startswith" => Ok(Token::StartsWith), + "endswith" => Ok(Token::EndsWith), lexeme => Ok(Token::Property(lexeme)), } } @@ -217,8 +219,8 @@ mod tests { #[test] fn test_comparison_operators() { assert_sequence( - "== != contains in", - &[Token::Equals, Token::NotEquals, Token::Contains, Token::In], + "== != contains in startswith endswith", + &[Token::Equals, Token::NotEquals, Token::Contains, Token::In, Token::StartsWith, Token::EndsWith], ); } @@ -265,4 +267,19 @@ mod tests { ], ); } + + #[test] + fn test_negation() { + assert_sequence("repo.public && !release.prerelease && !artifact.source-code", + &[ + Token::Property("repo.public"), + Token::And, + Token::Not, + Token::Property("release.prerelease"), + Token::And, + Token::Not, + Token::Property("artifact.source-code") + ], + ); + } } diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 4ac949c..fb4dfa5 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -111,6 +111,8 @@ impl<'a, T: Filterable> ExprVisitor for FilterContext<'a, T> { Token::NotEquals => (left != right).into(), Token::Contains => left.contains(&right).into(), Token::In => right.contains(&left).into(), + Token::StartsWith => left.startswith(&right).into(), + Token::EndsWith => left.endswith(&right).into(), token => unreachable!("Encountered an unexpected binary operator '{token}'"), } } @@ -271,5 +273,35 @@ mod tests { .expect("parse filter") .matches(&obj) .expect("run filter")); + + assert!(Filter::new("name startswith \"John\"") + .expect("parse filter") + .matches(&obj) + .expect("run filter")); + + assert!(Filter::new("name startswith \"JOHN \"") + .expect("parse filter") + .matches(&obj) + .expect("run filter")); + + assert!(!Filter::new("name startswith \"jane\"") + .expect("parse filter") + .matches(&obj) + .expect("run filter")); + + assert!(Filter::new("name endswith \"Doe\"") + .expect("parse filter") + .matches(&obj) + .expect("run filter")); + + assert!(Filter::new("name endswith \" DOE\"") + .expect("parse filter") + .matches(&obj) + .expect("run filter")); + + assert!(!Filter::new("name endswith \"jane\"") + .expect("parse filter") + .matches(&obj) + .expect("run filter")); } } diff --git a/src/filter/parser.rs b/src/filter/parser.rs index 434688e..1c270e4 100644 --- a/src/filter/parser.rs +++ b/src/filter/parser.rs @@ -77,7 +77,7 @@ impl<'a, I: Iterator, Error>>> Parser<'a, I> { if matches!( self.tokens.peek(), - Some(Ok(Token::In) | Ok(Token::Contains)) + Some(Ok(Token::In)) | Some(Ok(Token::Contains)) | Some(Ok(Token::StartsWith)) | Some(Ok(Token::EndsWith)) ) { let token = self.tokens.next().unwrap().unwrap(); let right = self.unary()?; diff --git a/src/filter/token.rs b/src/filter/token.rs index 3c3f666..95c5027 100644 --- a/src/filter/token.rs +++ b/src/filter/token.rs @@ -20,6 +20,8 @@ pub enum Token<'a> { NotEquals, Contains, In, + StartsWith, + EndsWith, Not, And, @@ -47,6 +49,8 @@ impl Token<'_> { Token::NotEquals => "!=", Token::Contains => "contains", Token::In => "in", + Token::StartsWith => "startswith", + Token::EndsWith => "endswith", Token::Not => "!", Token::And => "&&", diff --git a/src/filter/value.rs b/src/filter/value.rs index b366e6c..a579608 100644 --- a/src/filter/value.rs +++ b/src/filter/value.rs @@ -55,6 +55,26 @@ impl FilterValue { _ => false, } } + + pub fn startswith(&self, other: &FilterValue) -> bool { + match (self, other) { + (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai == b), + (FilterValue::String(a), FilterValue::String(b)) => { + a.to_lowercase().starts_with(&b.to_lowercase()) + } + _ => false, + } + } + + pub fn endswith(&self, other: &FilterValue) -> bool { + match (self, other) { + (FilterValue::Tuple(a), b) => a.iter().any(|ai| ai == b), + (FilterValue::String(a), FilterValue::String(b)) => { + a.to_lowercase().ends_with(&b.to_lowercase()) + } + _ => false, + } + } } impl PartialEq for FilterValue { diff --git a/src/telemetry/mod.rs b/src/telemetry/mod.rs index 2ea9b7b..3f2c414 100644 --- a/src/telemetry/mod.rs +++ b/src/telemetry/mod.rs @@ -32,7 +32,7 @@ fn load_otlp_headers() -> HashMap { #[cfg(debug_assertions)] tracing_metadata.insert( "x-honeycomb-team".into(), - "X6naTEMkzy10PMiuzJKifF".parse().unwrap(), + "X6naTEMkzy10PMiuzJKifF".into(), ); match std::env::var("OTEL_EXPORTER_OTLP_HEADERS").ok() {