feat: implement filter string parsing with Pest
Co-authored-by: aider (openai/andrew/openrouter/deepseek/deepseek-chat-v3.1) <aider@aider.chat>
This commit is contained in:
30
src/filter.pest
Normal file
30
src/filter.pest
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
WHITESPACE = _{ " " | "\t" | "\n" | "\r" }
|
||||||
|
|
||||||
|
filters = { filter ~ ("," ~ filters)? }
|
||||||
|
filter = { filter_name ~ ("(" ~ options ~ ")")? }
|
||||||
|
filter_name = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }
|
||||||
|
|
||||||
|
options = { option ~ ("," ~ options)? }
|
||||||
|
option = { (option_name ~ "=")? ~ option_value }
|
||||||
|
option_name = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }
|
||||||
|
|
||||||
|
option_value = {
|
||||||
|
JSON_NUMBER |
|
||||||
|
JSON_STRING |
|
||||||
|
JSON_BOOLEAN
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON_NUMBER = @{
|
||||||
|
("-")? ~
|
||||||
|
("0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*) ~
|
||||||
|
("." ~ ASCII_DIGIT*)? ~
|
||||||
|
(("e" | "E") ~ ("+" | "-")? ~ ASCII_DIGIT+)?
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON_STRING = ${
|
||||||
|
"\"" ~
|
||||||
|
(("\\" ~ ANY) | (!("\"" | "\\") ~ ANY))* ~
|
||||||
|
"\""
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON_BOOLEAN = ${ "true" | "false" }
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
use pest::Parser;
|
||||||
|
use pest_derive::Parser;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[grammar = "filter.pest"]
|
||||||
|
pub struct FilterParser;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Filter {
|
||||||
|
pub name: String,
|
||||||
|
pub options: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_filter_string(input: &str) -> Result<Vec<Filter>, Box<dyn std::error::Error>> {
|
||||||
|
let mut filters = Vec::new();
|
||||||
|
let pairs = FilterParser::parse(Rule::filters, input)?;
|
||||||
|
|
||||||
|
for pair in pairs {
|
||||||
|
if pair.as_rule() == Rule::filter {
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut options = HashMap::new();
|
||||||
|
|
||||||
|
for inner_pair in pair.into_inner() {
|
||||||
|
match inner_pair.as_rule() {
|
||||||
|
Rule::filter_name => {
|
||||||
|
name = inner_pair.as_str().to_string();
|
||||||
|
}
|
||||||
|
Rule::options => {
|
||||||
|
for option_pair in inner_pair.into_inner() {
|
||||||
|
if option_pair.as_rule() == Rule::option {
|
||||||
|
let mut option_name = None;
|
||||||
|
let mut option_value = None;
|
||||||
|
|
||||||
|
for option_inner in option_pair.into_inner() {
|
||||||
|
match option_inner.as_rule() {
|
||||||
|
Rule::option_name => {
|
||||||
|
option_name = Some(option_inner.as_str().to_string());
|
||||||
|
}
|
||||||
|
Rule::option_value => {
|
||||||
|
option_value = Some(parse_option_value(option_inner.as_str())?);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = option_value {
|
||||||
|
// If no name is provided, use the filter name as the key
|
||||||
|
let key = option_name.unwrap_or_else(|| name.clone());
|
||||||
|
options.insert(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filters.push(Filter { name, options });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_option_value(input: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||||
|
// 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 let Ok(boolean) = input.parse::<bool>() {
|
||||||
|
return Ok(serde_json::Value::Bool(boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat as string (remove quotes if present)
|
||||||
|
let value = if input.starts_with('"') && input.ends_with('"') {
|
||||||
|
input[1..input.len()-1].to_string()
|
||||||
|
} else if input.starts_with('\'') && input.ends_with('\'') {
|
||||||
|
input[1..input.len()-1].to_string()
|
||||||
|
} else {
|
||||||
|
input.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(serde_json::Value::String(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_simple_filter() {
|
||||||
|
let result = parse_filter_string("grep").unwrap();
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert_eq!(result[0].name, "grep");
|
||||||
|
assert!(result[0].options.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_filter_with_options() {
|
||||||
|
let result = parse_filter_string("head_lines(10)").unwrap();
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert_eq!(result[0].name, "head_lines");
|
||||||
|
assert_eq!(result[0].options["head_lines"], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_filter_with_named_options() {
|
||||||
|
let result = parse_filter_string("grep(pattern=\"error\")").unwrap();
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert_eq!(result[0].name, "grep");
|
||||||
|
assert_eq!(result[0].options["pattern"], "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_multiple_filters() {
|
||||||
|
let result = parse_filter_string("head_lines(10), grep(pattern=\"error\")").unwrap();
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
assert_eq!(result[0].name, "head_lines");
|
||||||
|
assert_eq!(result[0].options["head_lines"], 10);
|
||||||
|
assert_eq!(result[1].name, "grep");
|
||||||
|
assert_eq!(result[1].options["pattern"], "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user