feat: Implement structured filter options with FilterOption trait

Co-authored-by: aider (openai/andrew/openrouter/deepseek/deepseek-chat-v3.1) <aider@aider.chat>
This commit is contained in:
Andrew Phillips
2025-09-03 08:24:55 -03:00
parent 1480ef504b
commit bd2a8af186
4 changed files with 185 additions and 36 deletions

View File

@@ -33,4 +33,14 @@ impl FilterPlugin for GrepFilter {
regex: self.regex.clone(), regex: self.regex.clone(),
}) })
} }
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
name: "pattern".to_string(),
default: None,
required: true,
}
]
}
} }

View File

@@ -38,6 +38,16 @@ impl FilterPlugin for HeadBytesFilter {
remaining: self.remaining, remaining: self.remaining,
}) })
} }
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
}
} }
pub struct HeadLinesFilter { pub struct HeadLinesFilter {

View File

@@ -9,9 +9,20 @@ pub mod skip;
pub mod strip_ansi; pub mod strip_ansi;
pub mod utils; pub mod utils;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct FilterOption {
pub name: String,
pub default: Option<serde_json::Value>,
pub required: bool,
}
pub trait FilterPlugin: Send { pub trait FilterPlugin: Send {
fn filter(&mut self, reader: Box<&mut dyn Read>, writer: Box<&mut dyn Write>) -> Result<()>; fn filter(&mut self, reader: Box<&mut dyn Read>, writer: Box<&mut dyn Write>) -> Result<()>;
fn clone_box(&self) -> Box<dyn FilterPlugin>; fn clone_box(&self) -> Box<dyn FilterPlugin>;
// Get the filter options definition
fn options(&self) -> Vec<FilterOption>;
} }
#[derive(Debug, EnumString, strum::VariantNames)] #[derive(Debug, EnumString, strum::VariantNames)]
@@ -104,7 +115,69 @@ pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
continue; continue;
} }
// Parse the filter type // Parse the filter name and parameters
if let Some((filter_name, params)) = part.split_once('(') {
if let Some(params) = params.strip_suffix(')') {
// Parse parameters
let mut options = HashMap::new();
let mut unnamed_params = Vec::new();
// Split parameters by commas
for param in params.split(',') {
let param = param.trim();
if param.is_empty() {
continue;
}
// Check if it's a named parameter (key=value)
if let Some((key, value)) = param.split_once('=') {
let key = key.trim();
let value = parse_option_value(value.trim())?;
options.insert(key.to_string(), value);
} else {
// Unnamed parameter
let value = parse_option_value(param)?;
unnamed_params.push(value);
}
}
// Create the appropriate filter plugin
if let Ok(filter_type) = FilterType::from_str(filter_name) {
let plugin = match filter_type {
FilterType::Grep => {
create_filter_with_options::<grep::GrepFilter>(&unnamed_params, &options)?
}
FilterType::HeadBytes => {
create_filter_with_options::<head::HeadBytesFilter>(&unnamed_params, &options)?
}
FilterType::HeadLines => {
create_filter_with_options::<head::HeadLinesFilter>(&unnamed_params, &options)?
}
FilterType::TailBytes => {
create_filter_with_options::<tail::TailBytesFilter>(&unnamed_params, &options)?
}
FilterType::TailLines => {
create_filter_with_options::<tail::TailLinesFilter>(&unnamed_params, &options)?
}
FilterType::SkipBytes => {
create_filter_with_options::<skip::SkipBytesFilter>(&unnamed_params, &options)?
}
FilterType::SkipLines => {
create_filter_with_options::<skip::SkipLinesFilter>(&unnamed_params, &options)?
}
FilterType::StripAnsi => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"strip_ansi filter doesn't take parameters"
));
}
};
chain.add_plugin(plugin);
continue;
}
}
} else {
// Handle filters without parameters
if let Ok(filter_type) = FilterType::from_str(part) { if let Ok(filter_type) = FilterType::from_str(part) {
match filter_type { match filter_type {
FilterType::StripAnsi => { FilterType::StripAnsi => {
@@ -119,53 +192,6 @@ pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
} }
} }
} }
// Handle filters with parameters
// Extract the filter name and parameters
if let Some((filter_name, params)) = part.split_once('(') {
if let Some(params) = params.strip_suffix(')') {
if let Ok(filter_type) = FilterType::from_str(filter_name) {
match filter_type {
FilterType::Grep => {
// Remove quotes if present
let pattern = params.trim_matches(|c| c == '\'' || c == '"');
chain.add_plugin(Box::new(grep::GrepFilter::new(pattern.to_string())?));
}
FilterType::HeadBytes => {
let count = utils::parse_number(params)?;
chain.add_plugin(Box::new(head::HeadBytesFilter::new(count)));
}
FilterType::HeadLines => {
let count = utils::parse_number(params)?;
chain.add_plugin(Box::new(head::HeadLinesFilter::new(count)));
}
FilterType::TailBytes => {
let count = utils::parse_number(params)?;
chain.add_plugin(Box::new(tail::TailBytesFilter::new(count)));
}
FilterType::TailLines => {
let count = utils::parse_number(params)?;
chain.add_plugin(Box::new(tail::TailLinesFilter::new(count)));
}
FilterType::SkipBytes => {
let count = utils::parse_number(params)?;
chain.add_plugin(Box::new(skip::SkipBytesFilter::new(count)));
}
FilterType::SkipLines => {
let count = utils::parse_number(params)?;
chain.add_plugin(Box::new(skip::SkipLinesFilter::new(count)));
}
FilterType::StripAnsi => {
// This should not happen as strip_ansi doesn't take parameters
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"strip_ansi filter doesn't take parameters"
));
}
}
continue;
}
}
} }
// If we get here, the filter wasn't recognized // If we get here, the filter wasn't recognized
@@ -177,3 +203,102 @@ pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
Ok(chain) Ok(chain)
} }
// Helper function to create filter with proper option handling
fn create_filter_with_options<T: FilterPlugin + Default>(
unnamed_params: &[serde_json::Value],
named_options: &HashMap<String, serde_json::Value>,
) -> Result<Box<dyn FilterPlugin>> {
let mut plugin = T::default();
let option_defs = plugin.options();
let mut options = HashMap::new();
// Process unnamed parameters
if unnamed_params.len() > option_defs.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Too many unnamed parameters (expected at most {})", option_defs.len())
));
}
for (i, param) in unnamed_params.iter().enumerate() {
if i >= option_defs.len() {
break;
}
let option_name = &option_defs[i].name;
options.insert(option_name.clone(), param.clone());
}
// Process named options
for (key, value) in named_options {
// Check if the option exists
if !option_defs.iter().any(|opt| &opt.name == key) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Unknown option '{}'", key)
));
}
options.insert(key.clone(), value.clone());
}
// Fill in defaults and check required options
for opt_def in option_defs {
if !options.contains_key(&opt_def.name) {
if let Some(default) = &opt_def.default {
options.insert(opt_def.name.clone(), default.clone());
} else if opt_def.required {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Missing required option '{}'", opt_def.name)
));
}
}
}
// Create the specific filter with the processed options
// This part needs to be implemented for each filter type
// For now, we'll use a match on the type name
// Note: This is a placeholder - you'll need to implement proper constructors for each filter
Ok(create_specific_filter::<T>(&options)?)
}
// Helper to create specific filter instances based on options
fn create_specific_filter<T: FilterPlugin + Default>(
options: &HashMap<String, serde_json::Value>,
) -> Result<Box<dyn FilterPlugin>> {
// This is a simplified implementation
// In practice, you'd need to handle each filter type specifically
let mut plugin = T::default();
// For now, just return the default plugin
// You'll need to implement proper initialization based on options
Ok(Box::new(plugin))
}
// Helper function to parse option values
fn parse_option_value(input: &str) -> Result<serde_json::Value> {
// Remove quotes if present
let input = input.trim_matches(|c| c == '\'' || c == '"');
// Try to parse as number
if let Ok(num) = input.parse::<i64>() {
return Ok(serde_json::Value::Number(num.into()));
}
if let Ok(num) = input.parse::<f64>() {
if let Some(number) = serde_json::Number::from_f64(num) {
return Ok(serde_json::Value::Number(number));
}
}
// Try to parse as boolean
if input.eq_ignore_ascii_case("true") {
return Ok(serde_json::Value::Bool(true));
}
if input.eq_ignore_ascii_case("false") {
return Ok(serde_json::Value::Bool(false));
}
// Treat as string
Ok(serde_json::Value::String(input.to_string()))
}

View File

@@ -20,4 +20,8 @@ impl FilterPlugin for StripAnsiFilter {
fn clone_box(&self) -> Box<dyn FilterPlugin> { fn clone_box(&self) -> Box<dyn FilterPlugin> {
Box::new(Self) Box::new(Self)
} }
fn options(&self) -> Vec<FilterOption> {
Vec::new() // strip_ansi doesn't take any options
}
} }