Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,33 @@
//!
//! Use `map:{operation}` to apply string operations to each item in a list.
//!
//! ## Structured Templates (Advanced)
//!
//! **NEW in v0.13.0**: Apply multiple inputs to different template sections with individual separators.
//! This enables powerful scenarios like batch processing, command construction, and data transformation.
//!
//! ```rust
//! use string_pipeline::Template;
//!
//! // Multiple inputs per template section with different separators
//! let template = Template::parse("Users: {upper} | Files: {lower}").unwrap();
//! let result = template.format_with_inputs(&[
//! &["john doe", "jane smith"], // Multiple users for first section
//! &["FILE1.TXT", "FILE2.TXT"] // Multiple files for second section
//! ], &[" ", ","]).unwrap(); // Space separator for users, comma for files
//! assert_eq!(result, "Users: JOHN DOE JANE SMITH | Files: file1.txt,file2.txt");
//!
//! // Template introspection
//! let sections = template.get_template_sections(); // Get template section info
//! assert_eq!(sections.len(), 2); // Two template sections: {strip_ansi|lower} and {}
//! ```
//!
//! **Key Features:**
//! - **🎯 Flexible Input**: Each template section can receive multiple input values
//! - **⚙️ Custom Separators**: Individual separator for each template section
//! - **🔍 Introspection**: Examine template structure before processing
//! - **🏗️ Batch Processing**: Perfect for processing multiple items per section
//!
//! ## Error Handling
//!
//! All operations return `Result<String, String>` for comprehensive error handling:
Expand All @@ -215,6 +242,12 @@
//! let result = template.format("not_a_list");
//! assert!(result.is_err());
//! // Error: "Sort operation can only be applied to lists"
//!
//! // Structured template input count validation
//! let template = Template::parse("A: {upper} B: {lower}").unwrap();
//! let result = template.format_with_inputs(&[&["only_one"]], &[" ", " "]);
//! assert!(result.is_err());
//! // Error: "Expected 2 input slices for 2 template sections, got 1"
//! ```
//!
//! ## Performance Notes
Expand Down Expand Up @@ -245,4 +278,4 @@

mod pipeline;

pub use pipeline::{MultiTemplate, Template};
pub use pipeline::{MultiTemplate, SectionInfo, SectionType, Template};
37 changes: 10 additions & 27 deletions src/pipeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ mod template;

use dashmap::DashMap;
use memchr::memchr_iter;
use once_cell::sync::{Lazy, OnceCell};
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::time::{Duration, Instant};
use strip_ansi_escapes::strip;

pub use crate::pipeline::template::{MultiTemplate, Template};
pub use crate::pipeline::template::{MultiTemplate, SectionInfo, SectionType, Template};
pub use debug::DebugTracer;

/* ------------------------------------------------------------------------ */
Expand Down Expand Up @@ -686,10 +686,7 @@ pub enum StringOp {
/// let template = Template::parse("{split:,:..|filter:\\.txt$|join:\\n}").unwrap();
/// assert_eq!(template.format("file.txt,readme.md,data.txt").unwrap(), "file.txt\ndata.txt");
/// ```
Filter {
pattern: String,
regex: OnceCell<Regex>,
},
Filter { pattern: String },

/// Remove list items matching a regex pattern.
///
Expand Down Expand Up @@ -724,10 +721,7 @@ pub enum StringOp {
/// let template = Template::parse("{split:\\n:..|filter_not:^$|join:\\n}").unwrap();
/// assert_eq!(template.format("line1\n\nline2\n\nline3").unwrap(), "line1\nline2\nline3");
/// ```
FilterNot {
pattern: String,
regex: OnceCell<Regex>,
},
FilterNot { pattern: String },

/// Select a range of items from a list.
///
Expand Down Expand Up @@ -899,7 +893,6 @@ pub enum StringOp {
RegexExtract {
pattern: String,
group: Option<usize>,
regex: OnceCell<Regex>,
},
}

Expand Down Expand Up @@ -1360,21 +1353,17 @@ fn apply_single_operation(
StringOp::Slice { range } => {
apply_list_operation(val, |list| apply_range(&list, range), "Slice")
}
StringOp::Filter { pattern, regex } => {
let re = regex.get_or_try_init(|| {
Regex::new(pattern).map_err(|e| format!("Invalid regex: {e}"))
})?;
StringOp::Filter { pattern } => {
let re = get_cached_regex(pattern)?;
match val {
Value::List(list) => Ok(Value::List(
list.into_iter().filter(|s| re.is_match(s)).collect(),
)),
Value::Str(s) => Ok(Value::Str(if re.is_match(&s) { s } else { String::new() })),
}
}
StringOp::FilterNot { pattern, regex } => {
let re = regex.get_or_try_init(|| {
Regex::new(pattern).map_err(|e| format!("Invalid regex: {e}"))
})?;
StringOp::FilterNot { pattern } => {
let re = get_cached_regex(pattern)?;
match val {
Value::List(list) => Ok(Value::List(
list.into_iter().filter(|s| !re.is_match(s)).collect(),
Expand Down Expand Up @@ -1576,15 +1565,9 @@ fn apply_single_operation(
)
}
}
StringOp::RegexExtract {
pattern,
group,
regex,
} => {
StringOp::RegexExtract { pattern, group } => {
if let Value::Str(s) = val {
let re = regex.get_or_try_init(|| {
Regex::new(pattern).map_err(|e| format!("Invalid regex: {e}"))
})?;
let re = get_cached_regex(pattern)?;
let result = if let Some(group_idx) = group {
re.captures(&s)
.and_then(|caps| caps.get(*group_idx))
Expand Down
11 changes: 1 addition & 10 deletions src/pipeline/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
//! escape sequences, and debug flags.
//!

use once_cell::sync::OnceCell;
use pest::Parser;
use pest_derive::Parser;
use smallvec::SmallVec;
Expand Down Expand Up @@ -270,11 +269,9 @@ fn parse_operation(pair: pest::iterators::Pair<Rule>) -> Result<StringOp, String
Rule::strip_ansi => Ok(StringOp::StripAnsi),
Rule::filter => Ok(StringOp::Filter {
pattern: extract_single_arg_raw(pair)?,
regex: OnceCell::new(),
}),
Rule::filter_not => Ok(StringOp::FilterNot {
pattern: extract_single_arg_raw(pair)?,
regex: OnceCell::new(),
}),
Rule::slice => Ok(StringOp::Slice {
range: extract_range_arg(pair)?,
Expand Down Expand Up @@ -500,11 +497,7 @@ fn parse_regex_extract_operation(pair: pest::iterators::Pair<Rule>) -> Result<St
let mut parts = pair.into_inner();
let pattern = parts.next().unwrap().as_str().to_string();
let group = parts.next().and_then(|p| p.as_str().parse().ok());
Ok(StringOp::RegexExtract {
pattern,
group,
regex: OnceCell::new(),
})
Ok(StringOp::RegexExtract { pattern, group })
}

/// Parses a map operation with nested operation list.
Expand Down Expand Up @@ -610,11 +603,9 @@ fn parse_map_inner_operation(pair: pest::iterators::Pair<Rule>) -> Result<String
Rule::map_unique => Ok(StringOp::Unique),
Rule::map_filter => Ok(StringOp::Filter {
pattern: extract_single_arg_raw(pair)?,
regex: OnceCell::new(),
}),
Rule::map_filter_not => Ok(StringOp::FilterNot {
pattern: extract_single_arg_raw(pair)?,
regex: OnceCell::new(),
}),

_ => Err(format!("Unsupported map operation: {:?}", pair.as_rule())),
Expand Down
Loading
Loading