Skip to content

Commit b140f58

Browse files
authored
feat(lib): introduce structured templates (#6)
* feat(lib): introduce structured templates gives the option to pass different arguments to the different sections inside a MultiTemplate * fix(lib): remove interior mutability issue from OnceCell that prevented stable use in HashMaps/Sets * fix(lib): fix logic error where we join first then processed intead of processing then joining
1 parent 14189e8 commit b140f58

File tree

5 files changed

+707
-39
lines changed

5 files changed

+707
-39
lines changed

src/lib.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,33 @@
199199
//!
200200
//! Use `map:{operation}` to apply string operations to each item in a list.
201201
//!
202+
//! ## Structured Templates (Advanced)
203+
//!
204+
//! **NEW in v0.13.0**: Apply multiple inputs to different template sections with individual separators.
205+
//! This enables powerful scenarios like batch processing, command construction, and data transformation.
206+
//!
207+
//! ```rust
208+
//! use string_pipeline::Template;
209+
//!
210+
//! // Multiple inputs per template section with different separators
211+
//! let template = Template::parse("Users: {upper} | Files: {lower}").unwrap();
212+
//! let result = template.format_with_inputs(&[
213+
//! &["john doe", "jane smith"], // Multiple users for first section
214+
//! &["FILE1.TXT", "FILE2.TXT"] // Multiple files for second section
215+
//! ], &[" ", ","]).unwrap(); // Space separator for users, comma for files
216+
//! assert_eq!(result, "Users: JOHN DOE JANE SMITH | Files: file1.txt,file2.txt");
217+
//!
218+
//! // Template introspection
219+
//! let sections = template.get_template_sections(); // Get template section info
220+
//! assert_eq!(sections.len(), 2); // Two template sections: {strip_ansi|lower} and {}
221+
//! ```
222+
//!
223+
//! **Key Features:**
224+
//! - **🎯 Flexible Input**: Each template section can receive multiple input values
225+
//! - **⚙️ Custom Separators**: Individual separator for each template section
226+
//! - **🔍 Introspection**: Examine template structure before processing
227+
//! - **🏗️ Batch Processing**: Perfect for processing multiple items per section
228+
//!
202229
//! ## Error Handling
203230
//!
204231
//! All operations return `Result<String, String>` for comprehensive error handling:
@@ -215,6 +242,12 @@
215242
//! let result = template.format("not_a_list");
216243
//! assert!(result.is_err());
217244
//! // Error: "Sort operation can only be applied to lists"
245+
//!
246+
//! // Structured template input count validation
247+
//! let template = Template::parse("A: {upper} B: {lower}").unwrap();
248+
//! let result = template.format_with_inputs(&[&["only_one"]], &[" ", " "]);
249+
//! assert!(result.is_err());
250+
//! // Error: "Expected 2 input slices for 2 template sections, got 1"
218251
//! ```
219252
//!
220253
//! ## Performance Notes
@@ -245,4 +278,4 @@
245278
246279
mod pipeline;
247280

248-
pub use pipeline::{MultiTemplate, Template};
281+
pub use pipeline::{MultiTemplate, SectionInfo, SectionType, Template};

src/pipeline/mod.rs

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ mod template;
4747

4848
use dashmap::DashMap;
4949
use memchr::memchr_iter;
50-
use once_cell::sync::{Lazy, OnceCell};
50+
use once_cell::sync::Lazy;
5151
use std::collections::HashMap;
5252
use std::time::{Duration, Instant};
5353
use strip_ansi_escapes::strip;
5454

55-
pub use crate::pipeline::template::{MultiTemplate, Template};
55+
pub use crate::pipeline::template::{MultiTemplate, SectionInfo, SectionType, Template};
5656
pub use debug::DebugTracer;
5757

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

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

732726
/// Select a range of items from a list.
733727
///
@@ -899,7 +893,6 @@ pub enum StringOp {
899893
RegexExtract {
900894
pattern: String,
901895
group: Option<usize>,
902-
regex: OnceCell<Regex>,
903896
},
904897
}
905898

@@ -1360,21 +1353,17 @@ fn apply_single_operation(
13601353
StringOp::Slice { range } => {
13611354
apply_list_operation(val, |list| apply_range(&list, range), "Slice")
13621355
}
1363-
StringOp::Filter { pattern, regex } => {
1364-
let re = regex.get_or_try_init(|| {
1365-
Regex::new(pattern).map_err(|e| format!("Invalid regex: {e}"))
1366-
})?;
1356+
StringOp::Filter { pattern } => {
1357+
let re = get_cached_regex(pattern)?;
13671358
match val {
13681359
Value::List(list) => Ok(Value::List(
13691360
list.into_iter().filter(|s| re.is_match(s)).collect(),
13701361
)),
13711362
Value::Str(s) => Ok(Value::Str(if re.is_match(&s) { s } else { String::new() })),
13721363
}
13731364
}
1374-
StringOp::FilterNot { pattern, regex } => {
1375-
let re = regex.get_or_try_init(|| {
1376-
Regex::new(pattern).map_err(|e| format!("Invalid regex: {e}"))
1377-
})?;
1365+
StringOp::FilterNot { pattern } => {
1366+
let re = get_cached_regex(pattern)?;
13781367
match val {
13791368
Value::List(list) => Ok(Value::List(
13801369
list.into_iter().filter(|s| !re.is_match(s)).collect(),
@@ -1576,15 +1565,9 @@ fn apply_single_operation(
15761565
)
15771566
}
15781567
}
1579-
StringOp::RegexExtract {
1580-
pattern,
1581-
group,
1582-
regex,
1583-
} => {
1568+
StringOp::RegexExtract { pattern, group } => {
15841569
if let Value::Str(s) = val {
1585-
let re = regex.get_or_try_init(|| {
1586-
Regex::new(pattern).map_err(|e| format!("Invalid regex: {e}"))
1587-
})?;
1570+
let re = get_cached_regex(pattern)?;
15881571
let result = if let Some(group_idx) = group {
15891572
re.captures(&s)
15901573
.and_then(|caps| caps.get(*group_idx))

src/pipeline/parser.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
//! escape sequences, and debug flags.
99
//!
1010
11-
use once_cell::sync::OnceCell;
1211
use pest::Parser;
1312
use pest_derive::Parser;
1413
use smallvec::SmallVec;
@@ -270,11 +269,9 @@ fn parse_operation(pair: pest::iterators::Pair<Rule>) -> Result<StringOp, String
270269
Rule::strip_ansi => Ok(StringOp::StripAnsi),
271270
Rule::filter => Ok(StringOp::Filter {
272271
pattern: extract_single_arg_raw(pair)?,
273-
regex: OnceCell::new(),
274272
}),
275273
Rule::filter_not => Ok(StringOp::FilterNot {
276274
pattern: extract_single_arg_raw(pair)?,
277-
regex: OnceCell::new(),
278275
}),
279276
Rule::slice => Ok(StringOp::Slice {
280277
range: extract_range_arg(pair)?,
@@ -500,11 +497,7 @@ fn parse_regex_extract_operation(pair: pest::iterators::Pair<Rule>) -> Result<St
500497
let mut parts = pair.into_inner();
501498
let pattern = parts.next().unwrap().as_str().to_string();
502499
let group = parts.next().and_then(|p| p.as_str().parse().ok());
503-
Ok(StringOp::RegexExtract {
504-
pattern,
505-
group,
506-
regex: OnceCell::new(),
507-
})
500+
Ok(StringOp::RegexExtract { pattern, group })
508501
}
509502

510503
/// Parses a map operation with nested operation list.
@@ -610,11 +603,9 @@ fn parse_map_inner_operation(pair: pest::iterators::Pair<Rule>) -> Result<String
610603
Rule::map_unique => Ok(StringOp::Unique),
611604
Rule::map_filter => Ok(StringOp::Filter {
612605
pattern: extract_single_arg_raw(pair)?,
613-
regex: OnceCell::new(),
614606
}),
615607
Rule::map_filter_not => Ok(StringOp::FilterNot {
616608
pattern: extract_single_arg_raw(pair)?,
617-
regex: OnceCell::new(),
618609
}),
619610

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

0 commit comments

Comments
 (0)