refactor: reduce code duplication in filter and item services

Co-authored-by: aider (openai/andrew/openrouter/mistralai/mistral-medium-3.1) <aider@aider.chat>
This commit is contained in:
Andrew Phillips
2025-08-28 20:51:39 -03:00
parent 5542f5592a
commit 4c8466bb21
5 changed files with 268 additions and 151 deletions

View File

@@ -6,6 +6,7 @@ pub mod head;
pub mod tail;
pub mod grep;
pub mod skip;
pub mod utils;
pub trait FilterPlugin: Send {
fn process(&mut self, data: &[u8]) -> Result<Vec<u8>>;
@@ -31,58 +32,77 @@ impl FilterChain {
let mut current_data = data.to_vec();
for plugin in &mut self.plugins {
current_data = plugin.process(&current_data)?;
// Early exit if no data remains
if current_data.is_empty() {
break;
}
}
Ok(current_data)
}
pub fn finish(&mut self) -> Result<Vec<u8>> {
let mut current_data = Vec::new();
let mut result = Vec::new();
let mut all_data = Vec::new();
for plugin in &mut self.plugins {
let processed = plugin.finish()?;
if !processed.is_empty() {
current_data = processed;
all_data.extend(processed);
}
}
Ok(current_data)
// If we have any data from finish, use it
if !all_data.is_empty() {
result = all_data;
}
Ok(result)
}
}
// Helper function to parse filter string and create appropriate plugins
pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
let mut chain = FilterChain::new();
for part in filter_str.split('|') {
let part = part.trim();
if part.is_empty() {
continue;
}
// Define a macro to reduce duplication in filter parsing
macro_rules! parse_filter {
($prefix:expr, $suffix:expr, $constructor:expr) => {{
if let Some(stripped) = part.strip_prefix($prefix).and_then(|s| s.strip_suffix($suffix)) {
let count = utils::parse_number(stripped)?;
chain.add_plugin($constructor(count));
continue;
}
}};
}
// Handle grep filter
if let Some(stripped) = part.strip_prefix("grep(").and_then(|s| s.strip_suffix(')')) {
// Remove quotes if present
let pattern = stripped.trim_matches(|c| c == '\'' || c == '"');
chain.add_plugin(Box::new(grep::GrepFilter::new(pattern.to_string())?));
} else if let Some(stripped) = part.strip_prefix("head_bytes(").and_then(|s| s.strip_suffix(')')) {
let count: usize = stripped.parse().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
chain.add_plugin(Box::new(head::HeadBytesFilter::new(count)));
} else if let Some(stripped) = part.strip_prefix("head_lines(").and_then(|s| s.strip_suffix(')')) {
let count: usize = stripped.parse().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
chain.add_plugin(Box::new(head::HeadLinesFilter::new(count)));
} else if let Some(stripped) = part.strip_prefix("tail_bytes(").and_then(|s| s.strip_suffix(')')) {
let count: usize = stripped.parse().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
chain.add_plugin(Box::new(tail::TailBytesFilter::new(count)?));
} else if let Some(stripped) = part.strip_prefix("tail_lines(").and_then(|s| s.strip_suffix(')')) {
let count: usize = stripped.parse().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
chain.add_plugin(Box::new(tail::TailLinesFilter::new(count)?));
} else if let Some(stripped) = part.strip_prefix("skip_bytes(").and_then(|s| s.strip_suffix(')')) {
let count: usize = stripped.parse().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
chain.add_plugin(Box::new(skip::SkipBytesFilter::new(count)));
} else if let Some(stripped) = part.strip_prefix("skip_lines(").and_then(|s| s.strip_suffix(')')) {
let count: usize = stripped.parse().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
chain.add_plugin(Box::new(skip::SkipLinesFilter::new(count)));
} else {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("Unknown filter: {}", part)));
continue;
}
// Handle other filters using the macro
parse_filter!("head_bytes(", ")", |count| Box::new(head::HeadBytesFilter::new(count)));
parse_filter!("head_lines(", ")", |count| Box::new(head::HeadLinesFilter::new(count)));
parse_filter!("tail_bytes(", ")", |count| Box::new(tail::TailBytesFilter::new(count)));
parse_filter!("tail_lines(", ")", |count| Box::new(tail::TailLinesFilter::new(count)));
parse_filter!("skip_bytes(", ")", |count| Box::new(skip::SkipBytesFilter::new(count)));
parse_filter!("skip_lines(", ")", |count| Box::new(skip::SkipLinesFilter::new(count)));
// If we get here, the filter wasn't recognized
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Unknown filter: {}", part)
));
}
Ok(chain)
}

View File

@@ -0,0 +1,34 @@
use std::io::Result;
/// Helper trait for common filter operations
pub trait FilterUtils {
/// Process data through a filter, handling empty results
fn process_data(&mut self, data: &[u8]) -> Result<Vec<u8>>;
/// Process data and check if we should continue processing
fn process_and_check_continue(&mut self, data: &[u8]) -> Result<(Vec<u8>, bool)>;
}
impl<T: FilterPlugin> FilterUtils for T {
fn process_data(&mut self, data: &[u8]) -> Result<Vec<u8>> {
let result = self.process(data)?;
Ok(result)
}
fn process_and_check_continue(&mut self, data: &[u8]) -> Result<(Vec<u8>, bool)> {
let result = self.process(data)?;
let should_continue = !result.is_empty();
Ok((result, should_continue))
}
}
/// Helper function to create a filter chain from a string
pub fn create_filter_chain(filter_str: &str) -> Result<Option<super::FilterChain>> {
super::parse_filter_string(filter_str).map(Some)
}
/// Helper function to parse a number from a string with error handling
pub fn parse_number<T: std::str::FromStr>(s: &str) -> Result<T> {
s.parse::<T>()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
}