This commit is contained in:
Andrew Phillips
2026-02-19 13:57:39 -04:00
parent a72395fe83
commit fdeb5f7951
82 changed files with 2756 additions and 2018 deletions

View File

@@ -1,6 +1,6 @@
use super::{FilterPlugin, FilterOption};
use std::io::{Result, Read, Write, BufRead};
use super::{FilterOption, FilterPlugin};
use regex::Regex;
use std::io::{BufRead, Read, Result, Write};
/// A filter that matches lines against a regular expression pattern.
///
@@ -40,9 +40,7 @@ impl GrepFilter {
pub fn new(pattern: String) -> Result<Self> {
let regex = Regex::new(&pattern)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
Ok(Self {
regex,
})
Ok(Self { regex })
}
}
@@ -99,7 +97,7 @@ impl FilterPlugin for GrepFilter {
regex: self.regex.clone(),
})
}
/// Returns the configuration options for this filter.
///
/// The only option is the required "pattern" for the regex.
@@ -116,12 +114,10 @@ impl FilterPlugin for GrepFilter {
/// assert!(opts[0].required);
/// ```
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
name: "pattern".to_string(),
default: None,
required: true,
}
]
vec![FilterOption {
name: "pattern".to_string(),
default: None,
required: true,
}]
}
}

View File

@@ -1,7 +1,7 @@
use super::{FilterPlugin, FilterOption};
use std::io::{Result, Read, Write, BufRead};
use super::{FilterOption, FilterPlugin};
use crate::common::PIPESIZE;
use crate::services::filter_service::register_filter_plugin;
use std::io::{BufRead, Read, Result, Write};
/// A filter that reads the first N bytes from the input stream.
///
@@ -41,9 +41,7 @@ impl HeadBytesFilter {
/// assert_eq!(filter.remaining, 1024);
/// ```
pub fn new(count: usize) -> Self {
Self {
remaining: count,
}
Self { remaining: count }
}
}
@@ -76,7 +74,7 @@ impl FilterPlugin for HeadBytesFilter {
if self.remaining == 0 {
return Ok(());
}
let mut buffer = vec![0; PIPESIZE];
while self.remaining > 0 {
let to_read = std::cmp::min(self.remaining, PIPESIZE);
@@ -102,7 +100,7 @@ impl FilterPlugin for HeadBytesFilter {
remaining: self.remaining,
})
}
/// Returns the configuration options for this filter.
///
/// Defines the "count" parameter as required with no default.
@@ -111,13 +109,11 @@ impl FilterPlugin for HeadBytesFilter {
///
/// Vector of `FilterOption` describing parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}]
}
}
@@ -152,42 +148,39 @@ impl HeadLinesFilter {
/// assert_eq!(filter.remaining, 3);
/// ```
pub fn new(count: usize) -> Self {
Self {
remaining: count,
}
Self { remaining: count }
}
}
/// Filters input by reading only the first N lines and writing them to the output.
///
///
/// Uses buffered line reading to process input line-by-line until the limit or EOF.
///
///
/// # Arguments
///
///
/// * `reader` - Mutable reference to the input data stream.
/// * `writer` - Mutable reference to the output stream.
///
///
/// # Returns
///
///
/// * `Result<()>` - Success if filtering completes, or I/O error.
///
///
/// # Errors
///
///
/// * `io::Error` from line reading or writing operations.
///
///
/// # Examples
///
///
/// ```
/// // Assuming a filter chain with head_lines(2)
/// // Input: "Line1\nLine2\nLine3" becomes "Line1\nLine2\n"
/// ```
impl FilterPlugin for HeadLinesFilter {
fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> {
if self.remaining == 0 {
return Ok(());
}
let mut buf_reader = std::io::BufReader::new(reader);
for line in buf_reader.by_ref().lines() {
let line = line?;
@@ -201,33 +194,31 @@ impl FilterPlugin for HeadLinesFilter {
}
/// Clones this filter into a new boxed instance.
///
///
/// Creates an independent copy with the same configuration.
///
///
/// # Returns
///
///
/// A new `Box<dyn FilterPlugin>` clone.
fn clone_box(&self) -> Box<dyn FilterPlugin> {
Box::new(Self {
remaining: self.remaining,
})
}
/// Returns the configuration options for this filter.
///
///
/// Defines the "count" parameter as required with no default.
///
///
/// # Returns
///
///
/// Vector of `FilterOption` describing parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}]
}
}

View File

@@ -1,7 +1,8 @@
use std::io::{Result, Read, Write};
use std::io::{Read, Result, Write};
use std::str::FromStr;
use strum::EnumString;
pub mod grep;
/// Filter plugin module for processing input streams.
///
/// This module defines the `FilterPlugin` trait and `FilterChain` for chaining filters,
@@ -17,19 +18,18 @@ use strum::EnumString;
/// chain.filter(&mut reader, &mut writer)?;
/// ```
pub mod head;
pub mod tail;
pub mod skip;
pub mod grep;
pub mod strip_ansi;
pub mod tail;
pub mod utils;
use std::collections::HashMap;
pub use head::{HeadBytesFilter, HeadLinesFilter};
pub use tail::{TailBytesFilter, TailLinesFilter};
pub use skip::{SkipBytesFilter, SkipLinesFilter};
pub use grep::GrepFilter;
pub use head::{HeadBytesFilter, HeadLinesFilter};
pub use skip::{SkipBytesFilter, SkipLinesFilter};
pub use strip_ansi::StripAnsiFilter;
pub use tail::{TailBytesFilter, TailLinesFilter};
/// Represents an option for a filter plugin.
///
@@ -195,7 +195,6 @@ pub struct FilterChain {
/// chain.add_plugin(Box::new(HeadLinesFilter::new(10)));
/// chain.filter(&mut reader, &mut writer)?;
/// ```
impl Clone for FilterChain {
/// Clones this filter chain.
///
@@ -222,6 +221,12 @@ impl Clone for Box<dyn FilterPlugin> {
}
}
impl Default for FilterChain {
fn default() -> Self {
Self::new()
}
}
impl FilterChain {
/// Creates a new empty filter chain.
///
@@ -286,19 +291,19 @@ impl FilterChain {
std::io::copy(reader, writer)?;
return Ok(());
}
// For multiple plugins, we need to chain them together
// We'll use a temporary buffer to hold intermediate results
let mut current_data = Vec::new();
std::io::copy(reader, &mut current_data)?;
// Store the plugins length to avoid borrowing issues
let plugins_len = self.plugins.len();
for i in 0..plugins_len {
// Create a cursor for the current data
let mut input = std::io::Cursor::new(std::mem::take(&mut current_data));
// For the last plugin, write directly to the output writer
if i == plugins_len - 1 {
self.plugins[i].filter(&mut input, writer)?;
@@ -337,14 +342,14 @@ pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
// 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();
@@ -356,10 +361,11 @@ pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
unnamed_params.push(value);
}
}
// Create the appropriate filter plugin
if let Ok(filter_type) = FilterType::from_str(filter_name) {
let plugin = create_filter_with_options(filter_type, &unnamed_params, &options)?;
let plugin =
create_filter_with_options(filter_type, &unnamed_params, &options)?;
chain.add_plugin(plugin);
continue;
}
@@ -375,7 +381,7 @@ pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Filter '{}' requires parameters", part)
format!("Filter '{}' requires parameters", part),
));
}
}
@@ -385,7 +391,7 @@ pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
// If we get here, the filter wasn't recognized
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Unknown filter: {}", part)
format!("Unknown filter: {}", part),
));
}
@@ -420,17 +426,20 @@ fn create_filter_with_options(
FilterType::SkipLines => skip::SkipLinesFilter::new(0).options(),
FilterType::StripAnsi => strip_ansi::StripAnsiFilter::new().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())
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;
@@ -438,19 +447,19 @@ fn create_filter_with_options(
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)
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) {
@@ -459,12 +468,12 @@ fn create_filter_with_options(
} else if opt_def.required {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Missing required option '{}'", opt_def.name)
format!("Missing required option '{}'", opt_def.name),
));
}
}
}
// Create the specific filter with the processed options
create_specific_filter(filter_type, &options)
}
@@ -485,72 +494,93 @@ fn create_specific_filter(
) -> Result<Box<dyn FilterPlugin>> {
match filter_type {
FilterType::Grep => {
let pattern = options.get("pattern")
let pattern = options
.get("pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"grep filter requires 'pattern' parameter"
))?;
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"grep filter requires 'pattern' parameter",
)
})?;
grep::GrepFilter::new(pattern.to_string()).map(|f| Box::new(f) as Box<dyn FilterPlugin>)
}
FilterType::HeadBytes => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"head_bytes filter requires 'count' parameter"
))?;
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"head_bytes filter requires 'count' parameter",
)
})?;
Ok(Box::new(head::HeadBytesFilter::new(count)))
}
FilterType::HeadLines => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"head_lines filter requires 'count' parameter"
))?;
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"head_lines filter requires 'count' parameter",
)
})?;
Ok(Box::new(head::HeadLinesFilter::new(count)))
}
FilterType::TailBytes => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"tail_bytes filter requires 'count' parameter"
))?;
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"tail_bytes filter requires 'count' parameter",
)
})?;
Ok(Box::new(tail::TailBytesFilter::new(count)))
}
FilterType::TailLines => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"tail_lines filter requires 'count' parameter"
))?;
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"tail_lines filter requires 'count' parameter",
)
})?;
Ok(Box::new(tail::TailLinesFilter::new(count)))
}
FilterType::SkipBytes => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"skip_bytes filter requires 'count' parameter"
))?;
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"skip_bytes filter requires 'count' parameter",
)
})?;
Ok(Box::new(skip::SkipBytesFilter::new(count)))
}
FilterType::SkipLines => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"skip_lines filter requires 'count' parameter"
))?;
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"skip_lines filter requires 'count' parameter",
)
})?;
Ok(Box::new(skip::SkipLinesFilter::new(count)))
}
FilterType::StripAnsi => {
@@ -558,7 +588,7 @@ fn create_specific_filter(
if !options.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"strip_ansi filter doesn't take parameters"
"strip_ansi filter doesn't take parameters",
));
}
Ok(Box::new(strip_ansi::StripAnsiFilter::new()))
@@ -578,17 +608,17 @@ fn create_specific_filter(
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));
}
if let Ok(num) = input.parse::<f64>()
&& 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));
@@ -596,7 +626,7 @@ fn parse_option_value(input: &str) -> Result<serde_json::Value> {
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

@@ -1,7 +1,7 @@
use super::{FilterPlugin, FilterOption};
use std::io::{Result, Read, Write, BufRead};
use super::{FilterOption, FilterPlugin};
use crate::common::PIPESIZE;
use crate::services::filter_service::register_filter_plugin;
use std::io::{BufRead, Read, Result, Write};
/// A filter that skips the first N bytes from the input stream.
pub struct SkipBytesFilter {
@@ -15,9 +15,7 @@ impl SkipBytesFilter {
///
/// * `count` - The number of bytes to skip from the beginning of the input.
pub fn new(count: usize) -> Self {
Self {
remaining: count,
}
Self { remaining: count }
}
}
@@ -45,7 +43,7 @@ impl FilterPlugin for SkipBytesFilter {
self.remaining -= bytes_read;
}
}
// Copy the remaining data using io::copy for efficiency
std::io::copy(reader, writer)?;
Ok(())
@@ -61,20 +59,18 @@ impl FilterPlugin for SkipBytesFilter {
remaining: self.remaining,
})
}
/// Returns the configuration options for this filter.
///
/// # Returns
///
/// A vector of `FilterOption` describing the filter's configurable parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}]
}
}
@@ -90,9 +86,7 @@ impl SkipLinesFilter {
///
/// * `count` - The number of lines to skip from the beginning of the input.
pub fn new(count: usize) -> Self {
Self {
remaining: count,
}
Self { remaining: count }
}
}
@@ -130,20 +124,18 @@ impl FilterPlugin for SkipLinesFilter {
remaining: self.remaining,
})
}
/// Returns the configuration options for this filter.
///
/// # Returns
///
/// A vector of `FilterOption` describing the filter's configurable parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}]
}
}

View File

@@ -1,6 +1,6 @@
use std::io::{Result, Read, Write};
use super::{FilterOption, FilterPlugin};
use std::io::{Read, Result, Write};
use strip_ansi_escapes::Writer;
use super::{FilterPlugin, FilterOption};
/// A filter that removes ANSI escape sequences from the input.
///
@@ -47,7 +47,7 @@ impl FilterPlugin for StripAnsiFilter {
fn clone_box(&self) -> Box<dyn FilterPlugin> {
Box::new(Self)
}
/// Returns the configuration options for this filter (none required).
///
/// # Returns

View File

@@ -1,8 +1,8 @@
use super::{FilterPlugin, FilterOption};
use std::io::{Result, Read, Write, BufRead};
use std::collections::VecDeque;
use super::{FilterOption, FilterPlugin};
use crate::common::PIPESIZE;
use crate::services::filter_service::register_filter_plugin;
use std::collections::VecDeque;
use std::io::{BufRead, Read, Result, Write};
/// A filter that reads the last N bytes from the input stream.
pub struct TailBytesFilter {
@@ -42,7 +42,7 @@ impl FilterPlugin for TailBytesFilter {
if bytes_read == 0 {
break;
}
// Add new data to the buffer
for &byte in &temp_buffer[..bytes_read] {
if self.buffer.len() == self.count {
@@ -51,7 +51,7 @@ impl FilterPlugin for TailBytesFilter {
self.buffer.push_back(byte);
}
}
// Write the buffered data at the end
let result: Vec<u8> = self.buffer.iter().cloned().collect();
writer.write_all(&result)?;
@@ -69,20 +69,18 @@ impl FilterPlugin for TailBytesFilter {
count: self.count,
})
}
/// Returns the configuration options for this filter.
///
/// # Returns
///
/// A vector of `FilterOption` describing the filter's configurable parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}]
}
}
@@ -126,7 +124,7 @@ impl FilterPlugin for TailLinesFilter {
}
self.lines.push_back(line);
}
// Write the buffered lines
for line in &self.lines {
writeln!(writer, "{}", line)?;
@@ -145,20 +143,18 @@ impl FilterPlugin for TailLinesFilter {
count: self.count,
})
}
/// Returns the configuration options for this filter.
///
/// # Returns
///
/// A vector of `FilterOption` describing the filter's configurable parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}]
}
}