feat: Add sal-text crate
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled

- Add a new crate `sal-text` for text manipulation utilities.
- Integrate `sal-text` into the main `sal` crate.
- Remove the previous `text` module from `sal`.  This improves
  organization and allows for independent development of the
  `sal-text` library.
This commit is contained in:
Mahmoud-Emad 2025-06-19 14:43:27 +03:00
parent 4a8d3bfd24
commit a7a7353aa1
19 changed files with 1808 additions and 369 deletions

View File

@ -11,7 +11,7 @@ categories = ["os", "filesystem", "api-bindings"]
readme = "README.md" readme = "README.md"
[workspace] [workspace]
members = [".", "vault", "git", "redisclient", "mycelium"] members = [".", "vault", "git", "redisclient", "mycelium", "text"]
[dependencies] [dependencies]
hex = "0.4" hex = "0.4"
@ -63,6 +63,7 @@ futures = "0.3.30"
sal-git = { path = "git" } sal-git = { path = "git" }
sal-redisclient = { path = "redisclient" } sal-redisclient = { path = "redisclient" }
sal-mycelium = { path = "mycelium" } sal-mycelium = { path = "mycelium" }
sal-text = { path = "text" }
# Optional features for specific OS functionality # Optional features for specific OS functionality
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]

View File

@ -45,7 +45,7 @@ pub mod postgresclient;
pub mod process; pub mod process;
pub use sal_redisclient as redisclient; pub use sal_redisclient as redisclient;
pub mod rhai; pub mod rhai;
pub mod text; pub use sal_text as text;
pub mod vault; pub mod vault;
pub mod virt; pub mod virt;
pub mod zinit_client; pub mod zinit_client;

View File

@ -14,7 +14,6 @@ mod process;
mod rfs; mod rfs;
mod screen; mod screen;
mod text;
mod vault; mod vault;
mod zinit; mod zinit;
@ -101,19 +100,7 @@ pub use zinit::register_zinit_module;
pub use sal_mycelium::rhai::register_mycelium_module; pub use sal_mycelium::rhai::register_mycelium_module;
// Re-export text module // Re-export text module
pub use text::register_text_module; pub use sal_text::rhai::register_text_module;
// Re-export text functions directly from text module
pub use crate::text::{
// Dedent functions
dedent,
// Fix functions
name_fix,
path_fix,
prefix,
};
// Re-export TextReplacer functions
pub use text::*;
// Re-export crypto module // Re-export crypto module
pub use vault::register_crypto_module; pub use vault::register_crypto_module;
@ -166,7 +153,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
sal_mycelium::rhai::register_mycelium_module(engine)?; sal_mycelium::rhai::register_mycelium_module(engine)?;
// Register Text module functions // Register Text module functions
text::register_text_module(engine)?; sal_text::rhai::register_text_module(engine)?;
// Register RFS module functions // Register RFS module functions
rfs::register(engine)?; rfs::register(engine)?;

View File

@ -1,307 +0,0 @@
# SAL Text Module (`sal::text`)
This module provides a collection of utilities for common text processing and manipulation tasks in Rust, with bindings for Rhai scripting.
## Overview
The `sal::text` module offers functionalities for:
- **Indentation**: Removing common leading whitespace (`dedent`) and adding prefixes to lines (`prefix`).
- **Normalization**: Sanitizing strings for use as filenames (`name_fix`) or fixing filename components within paths (`path_fix`).
- **Replacement**: A powerful `TextReplacer` for performing single or multiple regex or literal text replacements in strings or files.
- **Templating**: A `TemplateBuilder` using the Tera engine to render text templates with dynamic data.
## Rust API
### 1. Text Indentation
Located in `src/text/dedent.rs` (for `dedent`) and `src/text/fix.rs` (likely contains `prefix`, though not explicitly confirmed by file view, its Rhai registration implies existence).
- **`dedent(text: &str) -> String`**: Removes common leading whitespace from a multiline string. Tabs are treated as 4 spaces. Ideal for cleaning up heredocs or indented code snippets.
```rust
use sal::text::dedent;
let indented_text = " Hello\n World";
assert_eq!(dedent(indented_text), "Hello\n World");
```
- **`prefix(text: &str, prefix_str: &str) -> String`**: Adds `prefix_str` to the beginning of each line in `text`.
```rust
use sal::text::prefix;
let text = "line1\nline2";
assert_eq!(prefix(text, "> "), "> line1\n> line2");
```
### 2. Filename and Path Normalization
Located in `src/text/fix.rs`.
- **`name_fix(text: &str) -> String`**: Sanitizes a string to be suitable as a name or filename component. It converts to lowercase, replaces whitespace and various special characters with underscores, and removes non-ASCII characters.
```rust
use sal::text::name_fix;
assert_eq!(name_fix("My File (New).txt"), "my_file_new_.txt");
assert_eq!(name_fix("Café crème.jpg"), "caf_crm.jpg");
```
- **`path_fix(text: &str) -> String`**: Applies `name_fix` to the filename component of a given path string, leaving the directory structure intact.
```rust
use sal::text::path_fix;
assert_eq!(path_fix("/some/path/My Document.docx"), "/some/path/my_document.docx");
```
### 3. Text Replacement (`TextReplacer`)
Located in `src/text/replace.rs`. Provides `TextReplacer` and `TextReplacerBuilder`.
The `TextReplacer` allows for complex, chained replacement operations on strings or file contents.
**Builder Pattern:**
```rust
use sal::text::TextReplacer;
// Example: Multiple replacements, regex and literal
let replacer = TextReplacer::builder()
.pattern(r"\d+") // Regex: match one or more digits
.replacement("NUMBER")
.regex(true)
.and() // Chain another replacement
.pattern("World") // Literal string
.replacement("Universe")
.regex(false) // Explicitly literal, though default
.build()
.expect("Failed to build replacer");
let original_text = "Hello World, item 123 and item 456.";
let modified_text = replacer.replace(original_text);
assert_eq!(modified_text, "Hello Universe, item NUMBER and item NUMBER.");
// Case-insensitive regex example
let case_replacer = TextReplacer::builder()
.pattern("apple")
.replacement("FRUIT")
.regex(true)
.case_insensitive(true)
.build()
.unwrap();
assert_eq!(case_replacer.replace("Apple and apple"), "FRUIT and FRUIT");
```
**Key `TextReplacerBuilder` methods:**
- `pattern(pat: &str)`: Sets the search pattern (string or regex).
- `replacement(rep: &str)`: Sets the replacement string.
- `regex(yes: bool)`: If `true`, treats `pattern` as a regex. Default is `false` (literal).
- `case_insensitive(yes: bool)`: If `true` (and `regex` is `true`), performs case-insensitive matching.
- `and()`: Finalizes the current replacement operation and prepares for a new one.
- `build()`: Consumes the builder and returns a `Result<TextReplacer, String>`.
**`TextReplacer` methods:**
- `replace(input: &str) -> String`: Applies all configured replacements to the input string.
- `replace_file(path: P) -> io::Result<String>`: Reads a file, applies replacements, returns the result.
- `replace_file_in_place(path: P) -> io::Result<()>`: Replaces content in the specified file directly.
- `replace_file_to(input_path: P1, output_path: P2) -> io::Result<()>`: Reads from `input_path`, applies replacements, writes to `output_path`.
### 4. Text Templating (`TemplateBuilder`)
Located in `src/text/template.rs`. Uses the Tera templating engine.
**Builder Pattern:**
```rust
use sal::text::TemplateBuilder;
use std::collections::HashMap;
// Assume "./my_template.txt" contains: "Hello, {{ name }}! You are {{ age }}."
// Create a temporary template file for the example
std::fs::write("./my_template.txt", "Hello, {{ name }}! You are {{ age }}.").unwrap();
let mut builder = TemplateBuilder::open("./my_template.txt").expect("Template not found");
// Add variables individually
builder = builder.add_var("name", "Alice").add_var("age", 30);
let rendered_string = builder.render().expect("Rendering failed");
assert_eq!(rendered_string, "Hello, Alice! You are 30.");
// Or add multiple variables from a HashMap
let mut vars = HashMap::new();
vars.insert("name", "Bob");
vars.insert("age", "25"); // Values in HashMap are typically strings or serializable types
let mut builder2 = TemplateBuilder::open("./my_template.txt").unwrap();
builder2 = builder2.add_vars(vars);
let rendered_string2 = builder2.render().unwrap();
assert_eq!(rendered_string2, "Hello, Bob! You are 25.");
// Render directly to a file
// builder.render_to_file("output.txt").expect("Failed to write to file");
// Clean up temporary file
std::fs::remove_file("./my_template.txt").unwrap();
```
**Key `TemplateBuilder` methods:**
- `open(template_path: P) -> io::Result<Self>`: Loads the template file.
- `add_var(name: S, value: V) -> Self`: Adds a single variable to the context.
- `add_vars(vars: HashMap<S, V>) -> Self`: Adds multiple variables from a HashMap.
- `render() -> Result<String, tera::Error>`: Renders the template to a string.
- `render_to_file(output_path: P) -> io::Result<()>`: Renders the template and writes it to the specified file.
## Rhai Scripting with `herodo`
The `sal::text` module's functionalities are exposed to Rhai scripts when using `herodo`.
### Direct Functions
- **`dedent(text_string)`**: Removes common leading whitespace.
- Example: `let clean_script = dedent(" if true {\n print(\"indented\");\n }");`
- **`prefix(text_string, prefix_string)`**: Adds `prefix_string` to each line of `text_string`.
- Example: `let prefixed_text = prefix("hello\nworld", "# ");`
- **`name_fix(text_string)`**: Normalizes a string for use as a filename.
- Example: `let filename = name_fix("My Document (V2).docx"); // "my_document_v2_.docx"`
- **`path_fix(path_string)`**: Normalizes the filename part of a path.
- Example: `let fixed_path = path_fix("/uploads/User Files/Report [Final].pdf");`
### TextReplacer
Provides text replacement capabilities through a builder pattern.
1. **Create a builder**: `let builder = text_replacer_new();`
2. **Configure replacements** (methods return the builder for chaining):
- `builder = builder.pattern(search_pattern_string);`
- `builder = builder.replacement(replacement_string);`
- `builder = builder.regex(is_regex_bool);` (default `false`)
- `builder = builder.case_insensitive(is_case_insensitive_bool);` (default `false`, only applies if `regex` is `true`)
- `builder = builder.and();` (to add the current replacement and start a new one)
3. **Build the replacer**: `let replacer = builder.build();`
4. **Use the replacer**:
- `let modified_text = replacer.replace(original_text_string);`
- `let modified_text_from_file = replacer.replace_file(input_filepath_string);`
- `replacer.replace_file_in_place(filepath_string);`
- `replacer.replace_file_to(input_filepath_string, output_filepath_string);`
### TemplateBuilder
Provides text templating capabilities.
1. **Open a template file**: `let tpl_builder = template_builder_open(template_filepath_string);`
2. **Add variables** (methods return the builder for chaining):
- `tpl_builder = tpl_builder.add_var(name_string, value);` (value can be string, int, float, bool, or array)
- `tpl_builder = tpl_builder.add_vars(map_object);` (map keys are variable names, values are their corresponding values)
3. **Render the template**:
- `let rendered_string = tpl_builder.render();`
- `tpl_builder.render_to_file(output_filepath_string);`
## Rhai Example
```rhai
// Create a temporary file for template demonstration
let template_content = "Report for {{user}}:\nItems processed: {{count}}.\nStatus: {{status}}.";
let template_path = "./temp_report_template.txt";
// Using file.write (assuming sal::file module is available and registered)
// For this example, we'll assume a way to write this file or that it exists.
// For a real script, ensure the file module is used or the file is pre-existing.
print(`Intending to write template to: ${template_path}`);
// In a real scenario: file.write(template_path, template_content);
// For demonstration, let's simulate it exists for the template_builder_open call.
// If file module is not used, this script part needs adjustment or pre-existing file.
// --- Text Normalization ---
let raw_filename = "User's Report [Draft 1].md";
let safe_filename = name_fix(raw_filename);
print(`Safe filename: ${safe_filename}`); // E.g., "users_report_draft_1_.md"
let raw_path = "/data/project files/Final Report (2023).pdf";
let safe_path = path_fix(raw_path);
print(`Safe path: ${safe_path}`); // E.g., "/data/project files/final_report_2023_.pdf"
// --- Dedent and Prefix ---
let script_block = "\n for item in items {\n print(item);\n }\n";
let dedented_script = dedent(script_block);
print("Dedented script:\n" + dedented_script);
let prefixed_log = prefix("Operation successful.\nDetails logged.", "LOG: ");
print(prefixed_log);
// --- TextReplacer Example ---
let text_to_modify = "The quick brown fox jumps over the lazy dog. The dog was very lazy.";
let replacer_builder = text_replacer_new()
.pattern("dog")
.replacement("cat")
.case_insensitive(true) // Replace 'dog', 'Dog', 'DOG', etc.
.and()
.pattern("lazy")
.replacement("energetic")
.regex(false); // This is the default, explicit for clarity
let replacer = replacer_builder.build();
let replaced_text = replacer.replace(text_to_modify);
print(`Replaced text: ${replaced_text}`);
// Expected: The quick brown fox jumps over the energetic cat. The cat was very energetic.
// --- TemplateBuilder Example ---
// This part assumes 'temp_report_template.txt' was successfully created with content:
// "Report for {{user}}:\nItems processed: {{count}}.\nStatus: {{status}}."
// If not, template_builder_open will fail. For a robust script, check file existence or create it.
// Create a dummy template file if it doesn't exist for the example to run
// This would typically be done using the file module, e.g. file.write()
// For simplicity here, we'll just print a message if it's missing.
// In a real script: if !file.exists(template_path) { file.write(template_path, template_content); }
// Let's try to proceed assuming the template might exist or skip if not.
// A more robust script would handle the file creation explicitly.
// For the sake of this example, let's create it directly if possible (conceptual)
// This is a placeholder for actual file writing logic.
// if (true) { // Simulate file creation for example purpose
// std.os.remove_file(template_path); // Clean up if exists
// let f = std.io.open(template_path, "w"); f.write(template_content); f.close();
// }
// Due to the sandbox, direct file system manipulation like above isn't typically done in Rhai examples
// without relying on registered SAL functions. We'll assume the file exists.
print("Attempting to use template: " + template_path);
// It's better to ensure the file exists before calling template_builder_open
// For this example, we'll proceed, but in a real script, handle file creation.
// Create a dummy file for the template example to work in isolation
// This is not ideal but helps for a self-contained example if file module isn't used prior.
// In a real SAL script, you'd use `file.write`.
let _dummy_template_file_path = "./example_template.rhai.tmp";
// file.write(_dummy_template_file_path, "Name: {{name}}, Age: {{age}}");
// Using a known, simple template string for robustness if file ops are tricky in example context
let tpl_builder = template_builder_open(_dummy_template_file_path); // Use the dummy/known file
if tpl_builder.is_ok() {
let mut template_engine = tpl_builder.unwrap();
template_engine = template_engine.add_var("user", "Jane Doe");
template_engine = template_engine.add_var("count", 150);
template_engine = template_engine.add_var("status", "Completed");
let report_output = template_engine.render();
if report_output.is_ok() {
print("Generated Report:\n" + report_output.unwrap());
} else {
print("Error rendering template: " + report_output.unwrap_err());
}
// Example: Render to file
// template_engine.render_to_file("./generated_report.txt");
// print("Report also written to ./generated_report.txt");
} else {
print("Skipping TemplateBuilder example as template file '" + _dummy_template_file_path + "' likely missing or unreadable.");
print("Error: " + tpl_builder.unwrap_err());
print("To run this part, ensure '" + _dummy_template_file_path + "' exists with content like: 'Name: {{name}}, Age: {{age}}'");
}
// Clean up dummy file
// file.remove(_dummy_template_file_path);
```
**Note on Rhai Example File Operations:** The Rhai example above includes comments about file creation for the `TemplateBuilder` part. In a real `herodo` script, you would use `sal::file` module functions (e.g., `file.write`, `file.exists`, `file.remove`) to manage the template file. For simplicity and to avoid making the example dependent on another module's full setup path, it highlights where such operations would occur. The example tries to use a dummy path and gracefully skips if the template isn't found, which is a common issue when running examples in restricted environments or without proper setup. The core logic of using `TemplateBuilder` once the template is loaded remains the same.

View File

@ -1,9 +0,0 @@
mod dedent;
mod fix;
mod replace;
mod template;
pub use dedent::*;
pub use fix::*;
pub use replace::*;
pub use template::*;

22
text/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "sal-text"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
description = "SAL Text - Text processing and manipulation utilities with regex, templating, and normalization"
repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
[dependencies]
# Regex support for text replacement
regex = "1.8.1"
# Template engine for text rendering
tera = "1.19.0"
# Serialization support for templates
serde = { version = "1.0", features = ["derive"] }
# Rhai scripting support
rhai = { version = "1.12.0", features = ["sync"] }
[dev-dependencies]
# For temporary files in tests
tempfile = "3.5"

146
text/README.md Normal file
View File

@ -0,0 +1,146 @@
# SAL Text - Text Processing and Manipulation Utilities
SAL Text provides a comprehensive collection of text processing utilities for both Rust applications and Rhai scripting environments.
## Features
- **Text Indentation**: Remove common leading whitespace (`dedent`) and add prefixes (`prefix`)
- **String Normalization**: Sanitize strings for filenames (`name_fix`) and paths (`path_fix`)
- **Text Replacement**: Powerful `TextReplacer` for regex and literal replacements
- **Template Rendering**: `TemplateBuilder` using Tera engine for dynamic text generation
## Rust API
### Text Indentation
```rust
use sal_text::{dedent, prefix};
// Remove common indentation
let indented = " line 1\n line 2\n line 3";
let dedented = dedent(indented);
assert_eq!(dedented, "line 1\nline 2\n line 3");
// Add prefix to each line
let text = "line 1\nline 2";
let prefixed = prefix(text, "> ");
assert_eq!(prefixed, "> line 1\n> line 2");
```
### String Normalization
```rust
use sal_text::{name_fix, path_fix};
// Sanitize filename
let unsafe_name = "User's File [Draft].txt";
let safe_name = name_fix(unsafe_name);
assert_eq!(safe_name, "user_s_file_draft_.txt");
// Sanitize path (preserves directory structure)
let unsafe_path = "/path/to/User's File.txt";
let safe_path = path_fix(unsafe_path);
assert_eq!(safe_path, "/path/to/user_s_file.txt");
```
### Text Replacement
```rust
use sal_text::TextReplacer;
// Simple literal replacement
let replacer = TextReplacer::builder()
.pattern("hello")
.replacement("hi")
.build()
.expect("Failed to build replacer");
let result = replacer.replace("hello world, hello universe");
assert_eq!(result, "hi world, hi universe");
// Regex replacement
let replacer = TextReplacer::builder()
.pattern(r"\d+")
.replacement("NUMBER")
.regex(true)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("There are 123 items");
assert_eq!(result, "There are NUMBER items");
// Chained operations
let replacer = TextReplacer::builder()
.pattern("world")
.replacement("universe")
.and()
.pattern(r"\d+")
.replacement("NUMBER")
.regex(true)
.build()
.expect("Failed to build replacer");
```
### Template Rendering
```rust
use sal_text::TemplateBuilder;
let result = TemplateBuilder::open("template.txt")
.expect("Failed to open template")
.add_var("name", "World")
.add_var("count", 42)
.render()
.expect("Failed to render template");
```
## Rhai Scripting
All functionality is available in Rhai scripts when using `herodo`:
```rhai
// Text indentation
let dedented = dedent(" hello\n world");
let prefixed = prefix("line1\nline2", "> ");
// String normalization
let safe_name = name_fix("User's File [Draft].txt");
let safe_path = path_fix("/path/to/User's File.txt");
// Text replacement
let builder = text_replacer_new();
builder = pattern(builder, "hello");
builder = replacement(builder, "hi");
builder = regex(builder, false);
let replacer = build(builder);
let result = replace(replacer, "hello world");
// Template rendering
let template = template_builder_open("template.txt");
template = add_var(template, "name", "World");
let result = render(template);
```
## Testing
Run the comprehensive test suite:
```bash
# Unit tests
cargo test
# Rhai integration tests
cargo run --bin herodo tests/rhai/run_all_tests.rhai
```
## Dependencies
- `regex`: For regex-based text replacement
- `tera`: For template rendering
- `serde`: For template variable serialization
- `rhai`: For Rhai scripting integration
## License
Apache-2.0

59
text/src/lib.rs Normal file
View File

@ -0,0 +1,59 @@
//! SAL Text - Text processing and manipulation utilities
//!
//! This crate provides a comprehensive collection of text processing utilities including:
//! - **Text indentation**: Remove common leading whitespace (`dedent`) and add prefixes (`prefix`)
//! - **String normalization**: Sanitize strings for filenames (`name_fix`) and paths (`path_fix`)
//! - **Text replacement**: Powerful `TextReplacer` for regex and literal replacements
//! - **Template rendering**: `TemplateBuilder` using Tera engine for dynamic text generation
//!
//! All functionality is available in both Rust and Rhai scripting environments.
//!
//! # Examples
//!
//! ## Text Indentation
//!
//! ```rust
//! use sal_text::dedent;
//!
//! let indented = " line 1\n line 2\n line 3";
//! let dedented = dedent(indented);
//! assert_eq!(dedented, "line 1\nline 2\n line 3");
//! ```
//!
//! ## String Normalization
//!
//! ```rust
//! use sal_text::name_fix;
//!
//! let unsafe_name = "User's File [Draft].txt";
//! let safe_name = name_fix(unsafe_name);
//! assert_eq!(safe_name, "users_file_draft_.txt");
//! ```
//!
//! ## Text Replacement
//!
//! ```rust
//! use sal_text::TextReplacer;
//!
//! let replacer = TextReplacer::builder()
//! .pattern(r"\d+")
//! .replacement("NUMBER")
//! .regex(true)
//! .build()
//! .expect("Failed to build replacer");
//!
//! let result = replacer.replace("There are 123 items");
//! assert_eq!(result, "There are NUMBER items");
//! ```
mod dedent;
mod fix;
mod replace;
mod template;
pub mod rhai;
pub use dedent::*;
pub use fix::*;
pub use replace::*;
pub use template::*;

View File

@ -2,12 +2,9 @@
//! //!
//! This module provides Rhai wrappers for the functions in the Text module. //! This module provides Rhai wrappers for the functions in the Text module.
use rhai::{Engine, EvalAltResult, Array, Map, Position}; use crate::{TemplateBuilder, TextReplacer, TextReplacerBuilder};
use rhai::{Array, Engine, EvalAltResult, Map, Position};
use std::collections::HashMap; use std::collections::HashMap;
use crate::text::{
TextReplacer, TextReplacerBuilder,
TemplateBuilder
};
/// Register Text module functions with the Rhai engine /// Register Text module functions with the Rhai engine
/// ///
@ -21,10 +18,10 @@ use crate::text::{
pub fn register_text_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { pub fn register_text_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register types // Register types
register_text_types(engine)?; register_text_types(engine)?;
// Register TextReplacer constructor // Register TextReplacer constructor
engine.register_fn("text_replacer_new", text_replacer_new); engine.register_fn("text_replacer_new", text_replacer_new);
// Register TextReplacerBuilder instance methods // Register TextReplacerBuilder instance methods
engine.register_fn("pattern", pattern); engine.register_fn("pattern", pattern);
engine.register_fn("replacement", replacement); engine.register_fn("replacement", replacement);
@ -32,16 +29,16 @@ pub fn register_text_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult
engine.register_fn("case_insensitive", case_insensitive); engine.register_fn("case_insensitive", case_insensitive);
engine.register_fn("and", and); engine.register_fn("and", and);
engine.register_fn("build", build); engine.register_fn("build", build);
// Register TextReplacer instance methods // Register TextReplacer instance methods
engine.register_fn("replace", replace); engine.register_fn("replace", replace);
engine.register_fn("replace_file", replace_file); engine.register_fn("replace_file", replace_file);
engine.register_fn("replace_file_in_place", replace_file_in_place); engine.register_fn("replace_file_in_place", replace_file_in_place);
engine.register_fn("replace_file_to", replace_file_to); engine.register_fn("replace_file_to", replace_file_to);
// Register TemplateBuilder constructor // Register TemplateBuilder constructor
engine.register_fn("template_builder_open", template_builder_open); engine.register_fn("template_builder_open", template_builder_open);
// Register TemplateBuilder instance methods // Register TemplateBuilder instance methods
engine.register_fn("add_var", add_var_string); engine.register_fn("add_var", add_var_string);
engine.register_fn("add_var", add_var_int); engine.register_fn("add_var", add_var_int);
@ -51,15 +48,15 @@ pub fn register_text_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult
engine.register_fn("add_vars", add_vars); engine.register_fn("add_vars", add_vars);
engine.register_fn("render", render); engine.register_fn("render", render);
engine.register_fn("render_to_file", render_to_file); engine.register_fn("render_to_file", render_to_file);
// Register Fix functions directly from text module // Register Fix functions directly from text module
engine.register_fn("name_fix", crate::text::name_fix); engine.register_fn("name_fix", crate::name_fix);
engine.register_fn("path_fix", crate::text::path_fix); engine.register_fn("path_fix", crate::path_fix);
// Register Dedent functions directly from text module // Register Dedent functions directly from text module
engine.register_fn("dedent", crate::text::dedent); engine.register_fn("dedent", crate::dedent);
engine.register_fn("prefix", crate::text::prefix); engine.register_fn("prefix", crate::prefix);
Ok(()) Ok(())
} }
@ -67,13 +64,13 @@ pub fn register_text_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult
fn register_text_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { fn register_text_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register TextReplacerBuilder type // Register TextReplacerBuilder type
engine.register_type_with_name::<TextReplacerBuilder>("TextReplacerBuilder"); engine.register_type_with_name::<TextReplacerBuilder>("TextReplacerBuilder");
// Register TextReplacer type // Register TextReplacer type
engine.register_type_with_name::<TextReplacer>("TextReplacer"); engine.register_type_with_name::<TextReplacer>("TextReplacer");
// Register TemplateBuilder type // Register TemplateBuilder type
engine.register_type_with_name::<TemplateBuilder>("TemplateBuilder"); engine.register_type_with_name::<TemplateBuilder>("TemplateBuilder");
Ok(()) Ok(())
} }
@ -82,7 +79,7 @@ fn io_error_to_rhai_error<T>(result: std::io::Result<T>) -> Result<T, Box<EvalAl
result.map_err(|e| { result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime( Box::new(EvalAltResult::ErrorRuntime(
format!("IO error: {}", e).into(), format!("IO error: {}", e).into(),
Position::NONE Position::NONE,
)) ))
}) })
} }
@ -91,18 +88,13 @@ fn tera_error_to_rhai_error<T>(result: Result<T, tera::Error>) -> Result<T, Box<
result.map_err(|e| { result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime( Box::new(EvalAltResult::ErrorRuntime(
format!("Template error: {}", e).into(), format!("Template error: {}", e).into(),
Position::NONE Position::NONE,
)) ))
}) })
} }
fn string_error_to_rhai_error<T>(result: Result<T, String>) -> Result<T, Box<EvalAltResult>> { fn string_error_to_rhai_error<T>(result: Result<T, String>) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| { result.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), Position::NONE)))
Box::new(EvalAltResult::ErrorRuntime(
e.into(),
Position::NONE
))
})
} }
// TextReplacer implementation // TextReplacer implementation
@ -153,12 +145,19 @@ pub fn replace_file(replacer: &mut TextReplacer, path: &str) -> Result<String, B
} }
/// Reads a file, applies all replacements, and writes the result back to the file /// Reads a file, applies all replacements, and writes the result back to the file
pub fn replace_file_in_place(replacer: &mut TextReplacer, path: &str) -> Result<(), Box<EvalAltResult>> { pub fn replace_file_in_place(
replacer: &mut TextReplacer,
path: &str,
) -> Result<(), Box<EvalAltResult>> {
io_error_to_rhai_error(replacer.replace_file_in_place(path)) io_error_to_rhai_error(replacer.replace_file_in_place(path))
} }
/// Reads a file, applies all replacements, and writes the result to a new file /// Reads a file, applies all replacements, and writes the result to a new file
pub fn replace_file_to(replacer: &mut TextReplacer, input_path: &str, output_path: &str) -> Result<(), Box<EvalAltResult>> { pub fn replace_file_to(
replacer: &mut TextReplacer,
input_path: &str,
output_path: &str,
) -> Result<(), Box<EvalAltResult>> {
io_error_to_rhai_error(replacer.replace_file_to(input_path, output_path)) io_error_to_rhai_error(replacer.replace_file_to(input_path, output_path))
} }
@ -192,10 +191,11 @@ pub fn add_var_bool(builder: TemplateBuilder, name: &str, value: bool) -> Templa
/// Adds an array variable to the template context /// Adds an array variable to the template context
pub fn add_var_array(builder: TemplateBuilder, name: &str, array: Array) -> TemplateBuilder { pub fn add_var_array(builder: TemplateBuilder, name: &str, array: Array) -> TemplateBuilder {
// Convert Rhai Array to Vec<String> // Convert Rhai Array to Vec<String>
let vec: Vec<String> = array.iter() let vec: Vec<String> = array
.iter()
.filter_map(|v| v.clone().into_string().ok()) .filter_map(|v| v.clone().into_string().ok())
.collect(); .collect();
builder.add_var(name, vec) builder.add_var(name, vec)
} }
@ -203,13 +203,13 @@ pub fn add_var_array(builder: TemplateBuilder, name: &str, array: Array) -> Temp
pub fn add_vars(builder: TemplateBuilder, vars: Map) -> TemplateBuilder { pub fn add_vars(builder: TemplateBuilder, vars: Map) -> TemplateBuilder {
// Convert Rhai Map to Rust HashMap // Convert Rhai Map to Rust HashMap
let mut hash_map = HashMap::new(); let mut hash_map = HashMap::new();
for (key, value) in vars.iter() { for (key, value) in vars.iter() {
if let Ok(val_str) = value.clone().into_string() { if let Ok(val_str) = value.clone().into_string() {
hash_map.insert(key.to_string(), val_str); hash_map.insert(key.to_string(), val_str);
} }
} }
// Add the variables // Add the variables
builder.add_vars(hash_map) builder.add_vars(hash_map)
} }
@ -220,6 +220,9 @@ pub fn render(builder: &mut TemplateBuilder) -> Result<String, Box<EvalAltResult
} }
/// Renders the template and writes the result to a file /// Renders the template and writes the result to a file
pub fn render_to_file(builder: &mut TemplateBuilder, output_path: &str) -> Result<(), Box<EvalAltResult>> { pub fn render_to_file(
builder: &mut TemplateBuilder,
output_path: &str,
) -> Result<(), Box<EvalAltResult>> {
io_error_to_rhai_error(builder.render_to_file(output_path)) io_error_to_rhai_error(builder.render_to_file(output_path))
} }

View File

@ -0,0 +1,255 @@
// Text Rhai Test Runner
//
// This script runs all Text-related Rhai tests and reports results.
print("=== Text Rhai Test Suite ===");
print("Running comprehensive tests for Text Rhai integration...\n");
let total_tests = 0;
let passed_tests = 0;
let failed_tests = 0;
// Test 1: Text Indentation Functions
print("Test 1: Text Indentation Functions");
total_tests += 1;
try {
let indented = " line 1\n line 2\n line 3";
let dedented = dedent(indented);
let text = "line 1\nline 2";
let prefixed = prefix(text, "> ");
if dedented == "line 1\nline 2\n line 3" && prefixed == "> line 1\n> line 2" {
passed_tests += 1;
print("✓ PASSED: Text indentation functions work correctly");
} else {
failed_tests += 1;
print("✗ FAILED: Text indentation functions returned unexpected results");
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: Text indentation test failed - ${err}`);
}
// Test 2: String Normalization Functions
print("\nTest 2: String Normalization Functions");
total_tests += 1;
try {
let unsafe_name = "User's File [Draft].txt";
let safe_name = name_fix(unsafe_name);
let unsafe_path = "/path/to/User's File.txt";
let safe_path = path_fix(unsafe_path);
if safe_name == "user_s_file_draft_.txt" && safe_path == "/path/to/user_s_file.txt" {
passed_tests += 1;
print("✓ PASSED: String normalization functions work correctly");
} else {
failed_tests += 1;
print(`✗ FAILED: String normalization - expected 'user_s_file_draft_.txt' and '/path/to/user_s_file.txt', got '${safe_name}' and '${safe_path}'`);
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: String normalization test failed - ${err}`);
}
// Test 3: TextReplacer Builder Pattern
print("\nTest 3: TextReplacer Builder Pattern");
total_tests += 1;
try {
let builder = text_replacer_new();
builder = pattern(builder, "hello");
builder = replacement(builder, "hi");
builder = regex(builder, false);
let replacer = build(builder);
let result = replace(replacer, "hello world, hello universe");
if result == "hi world, hi universe" {
passed_tests += 1;
print("✓ PASSED: TextReplacer builder pattern works correctly");
} else {
failed_tests += 1;
print(`✗ FAILED: TextReplacer - expected 'hi world, hi universe', got '${result}'`);
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: TextReplacer builder test failed - ${err}`);
}
// Test 4: TextReplacer with Regex
print("\nTest 4: TextReplacer with Regex");
total_tests += 1;
try {
let builder = text_replacer_new();
builder = pattern(builder, "\\d+");
builder = replacement(builder, "NUMBER");
builder = regex(builder, true);
let replacer = build(builder);
let result = replace(replacer, "There are 123 items and 456 more");
if result == "There are NUMBER items and NUMBER more" {
passed_tests += 1;
print("✓ PASSED: TextReplacer regex functionality works correctly");
} else {
failed_tests += 1;
print(`✗ FAILED: TextReplacer regex - expected 'There are NUMBER items and NUMBER more', got '${result}'`);
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: TextReplacer regex test failed - ${err}`);
}
// Test 5: TextReplacer Chained Operations
print("\nTest 5: TextReplacer Chained Operations");
total_tests += 1;
try {
let builder = text_replacer_new();
builder = pattern(builder, "world");
builder = replacement(builder, "universe");
builder = regex(builder, false);
builder = and(builder);
builder = pattern(builder, "\\d+");
builder = replacement(builder, "NUMBER");
builder = regex(builder, true);
let replacer = build(builder);
let result = replace(replacer, "Hello world, there are 123 items");
if result == "Hello universe, there are NUMBER items" {
passed_tests += 1;
print("✓ PASSED: TextReplacer chained operations work correctly");
} else {
failed_tests += 1;
print(`✗ FAILED: TextReplacer chained - expected 'Hello universe, there are NUMBER items', got '${result}'`);
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: TextReplacer chained operations test failed - ${err}`);
}
// Test 6: Error Handling - Invalid Regex
print("\nTest 6: Error Handling - Invalid Regex");
total_tests += 1;
try {
let builder = text_replacer_new();
builder = pattern(builder, "[invalid regex");
builder = replacement(builder, "test");
builder = regex(builder, true);
let replacer = build(builder);
failed_tests += 1;
print("✗ FAILED: Should have failed with invalid regex");
} catch(err) {
passed_tests += 1;
print("✓ PASSED: Invalid regex properly rejected");
}
// Test 7: Unicode Handling
print("\nTest 7: Unicode Handling");
total_tests += 1;
try {
let unicode_text = " Hello 世界\n Goodbye 世界";
let dedented = dedent(unicode_text);
let unicode_name = "Café";
let fixed_name = name_fix(unicode_name);
let unicode_prefix = prefix("Hello 世界", "🔹 ");
if dedented == "Hello 世界\nGoodbye 世界" &&
fixed_name == "caf" &&
unicode_prefix == "🔹 Hello 世界" {
passed_tests += 1;
print("✓ PASSED: Unicode handling works correctly");
} else {
failed_tests += 1;
print("✗ FAILED: Unicode handling returned unexpected results");
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: Unicode handling test failed - ${err}`);
}
// Test 8: Edge Cases
print("\nTest 8: Edge Cases");
total_tests += 1;
try {
let empty_dedent = dedent("");
let empty_prefix = prefix("test", "");
let empty_name_fix = name_fix("");
if empty_dedent == "" && empty_prefix == "test" && empty_name_fix == "" {
passed_tests += 1;
print("✓ PASSED: Edge cases handled correctly");
} else {
failed_tests += 1;
print("✗ FAILED: Edge cases returned unexpected results");
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: Edge cases test failed - ${err}`);
}
// Test 9: Complex Workflow
print("\nTest 9: Complex Text Processing Workflow");
total_tests += 1;
try {
// Normalize filename
let unsafe_filename = "User's Script [Draft].py";
let safe_filename = name_fix(unsafe_filename);
// Process code
let indented_code = " def hello():\n print('Hello World')\n return True";
let dedented_code = dedent(indented_code);
let commented_code = prefix(dedented_code, "# ");
// Replace text
let builder = text_replacer_new();
builder = pattern(builder, "Hello World");
builder = replacement(builder, "SAL Text");
builder = regex(builder, false);
let replacer = build(builder);
let final_code = replace(replacer, commented_code);
if safe_filename == "user_s_script_draft_.py" &&
final_code.contains("# def hello():") &&
final_code.contains("SAL Text") {
passed_tests += 1;
print("✓ PASSED: Complex workflow completed successfully");
} else {
failed_tests += 1;
print("✗ FAILED: Complex workflow returned unexpected results");
}
} catch(err) {
failed_tests += 1;
print(`✗ ERROR: Complex workflow test failed - ${err}`);
}
// Test 10: Template Builder Error Handling
print("\nTest 10: Template Builder Error Handling");
total_tests += 1;
try {
let builder = template_builder_open("/nonexistent/file.txt");
failed_tests += 1;
print("✗ FAILED: Should have failed with nonexistent file");
} catch(err) {
passed_tests += 1;
print("✓ PASSED: Template builder properly handles nonexistent files");
}
// Print final results
print("\n=== Test Results ===");
print(`Total Tests: ${total_tests}`);
print(`Passed: ${passed_tests}`);
print(`Failed: ${failed_tests}`);
if failed_tests == 0 {
print("\n✓ All tests passed!");
} else {
print(`\n✗ ${failed_tests} test(s) failed.`);
}
print("\n=== Text Rhai Test Suite Completed ===");

View File

@ -0,0 +1,351 @@
//! Rhai integration tests for Text module
//!
//! These tests validate the Rhai wrapper functions and ensure proper
//! integration between Rust and Rhai for text processing operations.
use rhai::{Engine, EvalAltResult};
use sal_text::rhai::*;
#[cfg(test)]
mod rhai_integration_tests {
use super::*;
fn create_test_engine() -> Engine {
let mut engine = Engine::new();
register_text_module(&mut engine).expect("Failed to register text module");
engine
}
#[test]
fn test_rhai_module_registration() {
let engine = create_test_engine();
// Test that the functions are registered by checking if they exist
let script = r#"
// Test that all text functions are available
let functions_exist = true;
functions_exist
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_dedent_function_exists() {
let engine = create_test_engine();
let script = r#"
let indented = " line 1\n line 2\n line 3";
let result = dedent(indented);
return result == "line 1\nline 2\n line 3";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_prefix_function_exists() {
let engine = create_test_engine();
let script = r#"
let text = "line 1\nline 2";
let result = prefix(text, "> ");
return result == "> line 1\n> line 2";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_name_fix_function_exists() {
let engine = create_test_engine();
let script = r#"
let unsafe_name = "User's File [Draft].txt";
let result = name_fix(unsafe_name);
return result == "users_file_draft_.txt";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_path_fix_function_exists() {
let engine = create_test_engine();
let script = r#"
let unsafe_path = "/path/to/User's File.txt";
let result = path_fix(unsafe_path);
return result == "/path/to/users_file.txt";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_text_replacer_builder_creation() {
let engine = create_test_engine();
let script = r#"
let builder = text_replacer_builder();
return type_of(builder) == "sal_text::replace::TextReplacerBuilder";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_text_replacer_workflow() {
let engine = create_test_engine();
let script = r#"
let builder = text_replacer_builder();
builder = pattern(builder, "hello");
builder = replacement(builder, "hi");
builder = regex(builder, false);
let replacer = build(builder);
let result = replace(replacer, "hello world, hello universe");
return result == "hi world, hi universe";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_text_replacer_regex_workflow() {
let engine = create_test_engine();
let script = r#"
let builder = text_replacer_builder();
builder = pattern(builder, r"\d+");
builder = replacement(builder, "NUMBER");
builder = regex(builder, true);
let replacer = build(builder);
let result = replace(replacer, "There are 123 items");
return result == "There are NUMBER items";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_text_replacer_chained_operations() {
let engine = create_test_engine();
let script = r#"
let builder = text_replacer_builder();
builder = pattern(builder, "world");
builder = replacement(builder, "universe");
builder = regex(builder, false);
builder = and(builder);
builder = pattern(builder, r"\d+");
builder = replacement(builder, "NUMBER");
builder = regex(builder, true);
let replacer = build(builder);
let result = replace(replacer, "Hello world, there are 123 items");
return result == "Hello universe, there are NUMBER items";
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_template_builder_creation() {
let engine = create_test_engine();
let script = r#"
// We can't test file operations easily in unit tests,
// but we can test that the function exists and returns the right type
try {
let builder = template_builder_open("/nonexistent/file.txt");
return false; // Should have failed
} catch(err) {
return err.to_string().contains("error"); // Expected to fail
}
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_error_handling_invalid_regex() {
let engine = create_test_engine();
let script = r#"
try {
let builder = text_replacer_builder();
builder = pattern(builder, "[invalid regex");
builder = replacement(builder, "test");
builder = regex(builder, true);
let replacer = build(builder);
return false; // Should have failed
} catch(err) {
return true; // Expected to fail
}
"#;
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_parameter_validation() {
let engine = create_test_engine();
// Test that functions handle parameter validation correctly
let script = r#"
let test_results = [];
// Test empty string handling
try {
let result = dedent("");
test_results.push(result == "");
} catch(err) {
test_results.push(false);
}
// Test empty prefix
try {
let result = prefix("test", "");
test_results.push(result == "test");
} catch(err) {
test_results.push(false);
}
// Test empty name_fix
try {
let result = name_fix("");
test_results.push(result == "");
} catch(err) {
test_results.push(false);
}
return test_results;
"#;
let result: Result<rhai::Array, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
let results = result.unwrap();
// All parameter validation tests should pass
for (i, result) in results.iter().enumerate() {
assert_eq!(
result.as_bool().unwrap_or(false),
true,
"Parameter validation test {} failed",
i
);
}
}
#[test]
fn test_unicode_handling() {
let engine = create_test_engine();
let script = r#"
let unicode_tests = [];
// Test dedent with unicode
try {
let text = " Hello 世界\n Goodbye 世界";
let result = dedent(text);
unicode_tests.push(result == "Hello 世界\nGoodbye 世界");
} catch(err) {
unicode_tests.push(false);
}
// Test name_fix with unicode (should remove non-ASCII)
try {
let result = name_fix("Café");
unicode_tests.push(result == "caf");
} catch(err) {
unicode_tests.push(false);
}
// Test prefix with unicode
try {
let result = prefix("Hello 世界", "🔹 ");
unicode_tests.push(result == "🔹 Hello 世界");
} catch(err) {
unicode_tests.push(false);
}
return unicode_tests;
"#;
let result: Result<rhai::Array, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
let results = result.unwrap();
// All unicode tests should pass
for (i, result) in results.iter().enumerate() {
assert_eq!(
result.as_bool().unwrap_or(false),
true,
"Unicode test {} failed",
i
);
}
}
#[test]
fn test_complex_text_processing_workflow() {
let engine = create_test_engine();
let script = r#"
// Simple workflow test
let unsafe_filename = "User's Script [Draft].py";
let safe_filename = name_fix(unsafe_filename);
let indented_code = " def hello():\n return True";
let dedented_code = dedent(indented_code);
let results = [];
results.push(safe_filename == "users_script_draft_.py");
results.push(dedented_code.contains("def hello():"));
return results;
"#;
let result: Result<rhai::Array, Box<EvalAltResult>> = engine.eval(script);
assert!(result.is_ok());
let results = result.unwrap();
// All workflow tests should pass
for (i, result) in results.iter().enumerate() {
assert_eq!(
result.as_bool().unwrap_or(false),
true,
"Workflow test {} failed",
i
);
}
}
}

View File

@ -0,0 +1,174 @@
//! Unit tests for string normalization functionality
//!
//! These tests validate the name_fix and path_fix functions including:
//! - Filename sanitization for safe filesystem usage
//! - Path normalization preserving directory structure
//! - Special character handling and replacement
//! - Unicode character removal and ASCII conversion
use sal_text::{name_fix, path_fix};
#[test]
fn test_name_fix_basic() {
assert_eq!(name_fix("Hello World"), "hello_world");
assert_eq!(name_fix("File-Name.txt"), "file_name.txt");
}
#[test]
fn test_name_fix_special_characters() {
assert_eq!(name_fix("Test!@#$%^&*()"), "test_");
assert_eq!(name_fix("Space, Tab\t, Comma,"), "space_tab_comma_");
assert_eq!(name_fix("Quotes\"'"), "quotes_");
assert_eq!(name_fix("Brackets[]<>"), "brackets_");
assert_eq!(name_fix("Operators=+-"), "operators_");
}
#[test]
fn test_name_fix_unicode_removal() {
assert_eq!(name_fix("Café"), "caf");
assert_eq!(name_fix("Résumé"), "rsum");
assert_eq!(name_fix("Über"), "ber");
assert_eq!(name_fix("Naïve"), "nave");
assert_eq!(name_fix("Piñata"), "piata");
}
#[test]
fn test_name_fix_case_conversion() {
assert_eq!(name_fix("UPPERCASE"), "uppercase");
assert_eq!(name_fix("MixedCase"), "mixedcase");
assert_eq!(name_fix("camelCase"), "camelcase");
assert_eq!(name_fix("PascalCase"), "pascalcase");
}
#[test]
fn test_name_fix_consecutive_underscores() {
assert_eq!(name_fix("Multiple Spaces"), "multiple_spaces");
assert_eq!(name_fix("Special!!!Characters"), "special_characters");
assert_eq!(name_fix("Mixed-_-Separators"), "mixed_separators");
}
#[test]
fn test_name_fix_file_extensions() {
assert_eq!(name_fix("Document.PDF"), "document.pdf");
assert_eq!(name_fix("Image.JPEG"), "image.jpeg");
assert_eq!(name_fix("Archive.tar.gz"), "archive.tar.gz");
assert_eq!(name_fix("Config.json"), "config.json");
}
#[test]
fn test_name_fix_empty_and_edge_cases() {
assert_eq!(name_fix(""), "");
assert_eq!(name_fix(" "), "_");
assert_eq!(name_fix("!!!"), "_");
assert_eq!(name_fix("___"), "_");
}
#[test]
fn test_name_fix_real_world_examples() {
assert_eq!(name_fix("User's Report [Draft 1].md"), "users_report_draft_1_.md");
assert_eq!(name_fix("Meeting Notes (2023-12-01).txt"), "meeting_notes_2023_12_01_.txt");
assert_eq!(name_fix("Photo #123 - Vacation!.jpg"), "photo_123_vacation_.jpg");
assert_eq!(name_fix("Project Plan v2.0 FINAL.docx"), "project_plan_v2.0_final.docx");
}
#[test]
fn test_path_fix_directory_paths() {
assert_eq!(path_fix("/path/to/directory/"), "/path/to/directory/");
assert_eq!(path_fix("./relative/path/"), "./relative/path/");
assert_eq!(path_fix("../parent/path/"), "../parent/path/");
}
#[test]
fn test_path_fix_single_filename() {
assert_eq!(path_fix("filename.txt"), "filename.txt");
assert_eq!(path_fix("UPPER-file.md"), "upper_file.md");
assert_eq!(path_fix("Special!File.pdf"), "special_file.pdf");
}
#[test]
fn test_path_fix_absolute_paths() {
assert_eq!(path_fix("/path/to/File Name.txt"), "/path/to/file_name.txt");
assert_eq!(path_fix("/absolute/path/to/DOCUMENT-123.pdf"), "/absolute/path/to/document_123.pdf");
assert_eq!(path_fix("/home/user/Résumé.doc"), "/home/user/rsum.doc");
}
#[test]
fn test_path_fix_relative_paths() {
assert_eq!(path_fix("./relative/path/to/Document.PDF"), "./relative/path/to/document.pdf");
assert_eq!(path_fix("../parent/Special File.txt"), "../parent/special_file.txt");
assert_eq!(path_fix("subfolder/User's File.md"), "subfolder/users_file.md");
}
#[test]
fn test_path_fix_special_characters_in_filename() {
assert_eq!(path_fix("/path/with/[special]<chars>.txt"), "/path/with/_special_chars_.txt");
assert_eq!(path_fix("./folder/File!@#.pdf"), "./folder/file_.pdf");
assert_eq!(path_fix("/data/Report (Final).docx"), "/data/report_final_.docx");
}
#[test]
fn test_path_fix_preserves_path_structure() {
assert_eq!(path_fix("/very/long/path/to/some/Deep File.txt"), "/very/long/path/to/some/deep_file.txt");
assert_eq!(path_fix("./a/b/c/d/e/Final Document.pdf"), "./a/b/c/d/e/final_document.pdf");
}
#[test]
fn test_path_fix_windows_style_paths() {
// Note: These tests assume Unix-style path handling
// In a real implementation, you might want to handle Windows paths differently
assert_eq!(path_fix("C:\\Users\\Name\\Document.txt"), "c_users_name_document.txt");
}
#[test]
fn test_path_fix_edge_cases() {
assert_eq!(path_fix(""), "");
assert_eq!(path_fix("/"), "/");
assert_eq!(path_fix("./"), "./");
assert_eq!(path_fix("../"), "../");
}
#[test]
fn test_path_fix_unicode_in_filename() {
assert_eq!(path_fix("/path/to/Café.txt"), "/path/to/caf.txt");
assert_eq!(path_fix("./folder/Naïve Document.pdf"), "./folder/nave_document.pdf");
assert_eq!(path_fix("/home/user/Piñata Party.jpg"), "/home/user/piata_party.jpg");
}
#[test]
fn test_path_fix_complex_real_world_examples() {
assert_eq!(
path_fix("/Users/john/Documents/Project Files/Final Report (v2.1) [APPROVED].docx"),
"/Users/john/Documents/Project Files/final_report_v2.1_approved_.docx"
);
assert_eq!(
path_fix("./assets/images/Photo #123 - Vacation! (2023).jpg"),
"./assets/images/photo_123_vacation_2023_.jpg"
);
assert_eq!(
path_fix("/var/log/Application Logs/Error Log [2023-12-01].txt"),
"/var/log/Application Logs/error_log_2023_12_01_.txt"
);
}
#[test]
fn test_name_fix_and_path_fix_consistency() {
let filename = "User's Report [Draft].txt";
let path = "/path/to/User's Report [Draft].txt";
let fixed_name = name_fix(filename);
let fixed_path = path_fix(path);
// The filename part should be the same in both cases
assert!(fixed_path.ends_with(&fixed_name));
assert_eq!(fixed_name, "users_report_draft_.txt");
assert_eq!(fixed_path, "/path/to/users_report_draft_.txt");
}
#[test]
fn test_normalization_preserves_dots_in_extensions() {
assert_eq!(name_fix("file.tar.gz"), "file.tar.gz");
assert_eq!(name_fix("backup.2023.12.01.sql"), "backup.2023.12.01.sql");
assert_eq!(path_fix("/path/to/archive.tar.bz2"), "/path/to/archive.tar.bz2");
}

View File

@ -0,0 +1,297 @@
//! Unit tests for template functionality
//!
//! These tests validate the TemplateBuilder including:
//! - Template loading from files
//! - Variable substitution (string, int, float, bool, array)
//! - Template rendering to string and file
//! - Error handling for missing variables and invalid templates
//! - Complex template scenarios with loops and conditionals
use sal_text::TemplateBuilder;
use std::collections::HashMap;
use std::fs;
use tempfile::NamedTempFile;
#[test]
fn test_template_builder_basic_string_variable() {
// Create a temporary template file
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Hello {{name}}!";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("name", "World")
.render()
.expect("Failed to render template");
assert_eq!(result, "Hello World!");
}
#[test]
fn test_template_builder_multiple_variables() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{{greeting}} {{name}}, you have {{count}} messages.";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("greeting", "Hello")
.add_var("name", "Alice")
.add_var("count", 5)
.render()
.expect("Failed to render template");
assert_eq!(result, "Hello Alice, you have 5 messages.");
}
#[test]
fn test_template_builder_different_types() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "String: {{text}}, Int: {{number}}, Float: {{decimal}}, Bool: {{flag}}";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("text", "hello")
.add_var("number", 42)
.add_var("decimal", 3.14)
.add_var("flag", true)
.render()
.expect("Failed to render template");
assert_eq!(result, "String: hello, Int: 42, Float: 3.14, Bool: true");
}
#[test]
fn test_template_builder_array_variable() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Items: {% for item in items %}{{item}}{% if not loop.last %}, {% endif %}{% endfor %}";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let items = vec!["apple", "banana", "cherry"];
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("items", items)
.render()
.expect("Failed to render template");
assert_eq!(result, "Items: apple, banana, cherry");
}
#[test]
fn test_template_builder_add_vars_hashmap() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{{title}}: {{description}}";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let mut vars = HashMap::new();
vars.insert("title".to_string(), "Report".to_string());
vars.insert("description".to_string(), "Monthly summary".to_string());
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_vars(vars)
.render()
.expect("Failed to render template");
assert_eq!(result, "Report: Monthly summary");
}
#[test]
fn test_template_builder_render_to_file() {
// Create template file
let mut template_file = NamedTempFile::new().expect("Failed to create template file");
let template_content = "Hello {{name}}, today is {{day}}.";
fs::write(template_file.path(), template_content).expect("Failed to write template");
// Create output file
let output_file = NamedTempFile::new().expect("Failed to create output file");
TemplateBuilder::open(template_file.path())
.expect("Failed to open template")
.add_var("name", "Bob")
.add_var("day", "Monday")
.render_to_file(output_file.path())
.expect("Failed to render to file");
let result = fs::read_to_string(output_file.path()).expect("Failed to read output file");
assert_eq!(result, "Hello Bob, today is Monday.");
}
#[test]
fn test_template_builder_conditional() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{% if show_message %}Message: {{message}}{% else %}No message{% endif %}";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
// Test with condition true
let result_true = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("show_message", true)
.add_var("message", "Hello World")
.render()
.expect("Failed to render template");
assert_eq!(result_true, "Message: Hello World");
// Test with condition false
let result_false = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("show_message", false)
.add_var("message", "Hello World")
.render()
.expect("Failed to render template");
assert_eq!(result_false, "No message");
}
#[test]
fn test_template_builder_loop_with_index() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{% for item in items %}{{loop.index}}: {{item}}\n{% endfor %}";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let items = vec!["first", "second", "third"];
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("items", items)
.render()
.expect("Failed to render template");
assert_eq!(result, "1: first\n2: second\n3: third\n");
}
#[test]
fn test_template_builder_nested_variables() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "User: {{user.name}} ({{user.email}})";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let mut user = HashMap::new();
user.insert("name".to_string(), "John Doe".to_string());
user.insert("email".to_string(), "john@example.com".to_string());
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("user", user)
.render()
.expect("Failed to render template");
assert_eq!(result, "User: John Doe (john@example.com)");
}
#[test]
fn test_template_builder_missing_variable_error() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Hello {{missing_var}}!";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.render();
assert!(result.is_err());
}
#[test]
fn test_template_builder_invalid_template_syntax() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Hello {{unclosed_var!";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.render();
assert!(result.is_err());
}
#[test]
fn test_template_builder_nonexistent_file() {
let result = TemplateBuilder::open("/nonexistent/template.txt");
assert!(result.is_err());
}
#[test]
fn test_template_builder_empty_template() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
fs::write(temp_file.path(), "").expect("Failed to write empty template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.render()
.expect("Failed to render empty template");
assert_eq!(result, "");
}
#[test]
fn test_template_builder_template_with_no_variables() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "This is a static template with no variables.";
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.render()
.expect("Failed to render template");
assert_eq!(result, template_content);
}
#[test]
fn test_template_builder_complex_report() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = r#"
# {{report_title}}
Generated on: {{date}}
## Summary
Total items: {{total_items}}
Status: {{status}}
## Items
{% for item in items %}
- {{item.name}}: {{item.value}}{% if item.important %} (IMPORTANT){% endif %}
{% endfor %}
## Footer
{% if show_footer %}
Report generated by {{generator}}
{% endif %}
"#;
fs::write(temp_file.path(), template_content).expect("Failed to write template");
let mut item1 = HashMap::new();
item1.insert("name".to_string(), "Item 1".to_string());
item1.insert("value".to_string(), "100".to_string());
item1.insert("important".to_string(), true.to_string());
let mut item2 = HashMap::new();
item2.insert("name".to_string(), "Item 2".to_string());
item2.insert("value".to_string(), "200".to_string());
item2.insert("important".to_string(), false.to_string());
let items = vec![item1, item2];
let result = TemplateBuilder::open(temp_file.path())
.expect("Failed to open template")
.add_var("report_title", "Monthly Report")
.add_var("date", "2023-12-01")
.add_var("total_items", 2)
.add_var("status", "Complete")
.add_var("items", items)
.add_var("show_footer", true)
.add_var("generator", "SAL Text")
.render()
.expect("Failed to render template");
assert!(result.contains("# Monthly Report"));
assert!(result.contains("Generated on: 2023-12-01"));
assert!(result.contains("Total items: 2"));
assert!(result.contains("- Item 1: 100"));
assert!(result.contains("- Item 2: 200"));
assert!(result.contains("Report generated by SAL Text"));
}

View File

@ -0,0 +1,159 @@
//! Unit tests for text indentation functionality
//!
//! These tests validate the dedent and prefix functions including:
//! - Common whitespace removal (dedent)
//! - Line prefix addition (prefix)
//! - Edge cases and special characters
//! - Tab handling and mixed indentation
use sal_text::{dedent, prefix};
#[test]
fn test_dedent_basic() {
let indented = " line 1\n line 2\n line 3";
let expected = "line 1\nline 2\n line 3";
assert_eq!(dedent(indented), expected);
}
#[test]
fn test_dedent_empty_lines() {
let indented = " line 1\n\n line 2\n line 3";
let expected = "line 1\n\nline 2\n line 3";
assert_eq!(dedent(indented), expected);
}
#[test]
fn test_dedent_tabs_as_spaces() {
let indented = "\t\tline 1\n\t\tline 2\n\t\t\tline 3";
let expected = "line 1\nline 2\n\tline 3";
assert_eq!(dedent(indented), expected);
}
#[test]
fn test_dedent_mixed_tabs_and_spaces() {
let indented = " \tline 1\n \tline 2\n \t line 3";
let expected = "line 1\nline 2\n line 3";
assert_eq!(dedent(indented), expected);
}
#[test]
fn test_dedent_no_common_indentation() {
let text = "line 1\n line 2\n line 3";
let expected = "line 1\n line 2\n line 3";
assert_eq!(dedent(text), expected);
}
#[test]
fn test_dedent_single_line() {
let indented = " single line";
let expected = "single line";
assert_eq!(dedent(indented), expected);
}
#[test]
fn test_dedent_empty_string() {
assert_eq!(dedent(""), "");
}
#[test]
fn test_dedent_only_whitespace() {
let whitespace = " \n \n ";
let expected = "\n\n";
assert_eq!(dedent(whitespace), expected);
}
#[test]
fn test_prefix_basic() {
let text = "line 1\nline 2\nline 3";
let expected = " line 1\n line 2\n line 3";
assert_eq!(prefix(text, " "), expected);
}
#[test]
fn test_prefix_with_symbols() {
let text = "line 1\nline 2\nline 3";
let expected = "> line 1\n> line 2\n> line 3";
assert_eq!(prefix(text, "> "), expected);
}
#[test]
fn test_prefix_empty_lines() {
let text = "line 1\n\nline 3";
let expected = ">> line 1\n>> \n>> line 3";
assert_eq!(prefix(text, ">> "), expected);
}
#[test]
fn test_prefix_single_line() {
let text = "single line";
let expected = "PREFIX: single line";
assert_eq!(prefix(text, "PREFIX: "), expected);
}
#[test]
fn test_prefix_empty_string() {
assert_eq!(prefix("", "PREFIX: "), "");
}
#[test]
fn test_prefix_empty_prefix() {
let text = "line 1\nline 2";
assert_eq!(prefix(text, ""), text);
}
#[test]
fn test_dedent_and_prefix_combination() {
let indented = " def function():\n print('hello')\n return True";
let dedented = dedent(indented);
let prefixed = prefix(&dedented, ">>> ");
let expected = ">>> def function():\n>>> print('hello')\n>>> return True";
assert_eq!(prefixed, expected);
}
#[test]
fn test_dedent_real_code_example() {
let code = r#"
if condition:
for item in items:
process(item)
return result
else:
return None"#;
let dedented = dedent(code);
let expected = "\nif condition:\n for item in items:\n process(item)\n return result\nelse:\n return None";
assert_eq!(dedented, expected);
}
#[test]
fn test_prefix_code_comment() {
let code = "function main() {\n console.log('Hello');\n}";
let commented = prefix(code, "// ");
let expected = "// function main() {\n// console.log('Hello');\n// }";
assert_eq!(commented, expected);
}
#[test]
fn test_dedent_preserves_relative_indentation() {
let text = " start\n indented more\n back to start level\n indented again";
let dedented = dedent(text);
let expected = "start\n indented more\nback to start level\n indented again";
assert_eq!(dedented, expected);
}
#[test]
fn test_prefix_with_unicode() {
let text = "Hello 世界\nGoodbye 世界";
let prefixed = prefix(text, "🔹 ");
let expected = "🔹 Hello 世界\n🔹 Goodbye 世界";
assert_eq!(prefixed, expected);
}
#[test]
fn test_dedent_with_unicode() {
let text = " Hello 世界\n Goodbye 世界\n More indented 世界";
let dedented = dedent(text);
let expected = "Hello 世界\nGoodbye 世界\n More indented 世界";
assert_eq!(dedented, expected);
}

View File

@ -0,0 +1,301 @@
//! Unit tests for text replacement functionality
//!
//! These tests validate the TextReplacer and TextReplacerBuilder including:
//! - Literal string replacement
//! - Regex pattern replacement
//! - Multiple chained replacements
//! - File operations (read, write, in-place)
//! - Error handling and edge cases
use sal_text::{TextReplacer, TextReplacerBuilder};
use std::fs;
use tempfile::NamedTempFile;
#[test]
fn test_text_replacer_literal_single() {
let replacer = TextReplacer::builder()
.pattern("hello")
.replacement("hi")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("hello world, hello universe");
assert_eq!(result, "hi world, hi universe");
}
#[test]
fn test_text_replacer_regex_single() {
let replacer = TextReplacer::builder()
.pattern(r"\d+")
.replacement("NUMBER")
.regex(true)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("There are 123 items and 456 more");
assert_eq!(result, "There are NUMBER items and NUMBER more");
}
#[test]
fn test_text_replacer_multiple_operations() {
let replacer = TextReplacer::builder()
.pattern(r"\d+")
.replacement("NUMBER")
.regex(true)
.and()
.pattern("world")
.replacement("universe")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("Hello world, there are 123 items");
assert_eq!(result, "Hello universe, there are NUMBER items");
}
#[test]
fn test_text_replacer_chained_operations() {
let replacer = TextReplacer::builder()
.pattern("cat")
.replacement("dog")
.regex(false)
.and()
.pattern("dog")
.replacement("animal")
.regex(false)
.build()
.expect("Failed to build replacer");
// Operations are applied in sequence, so "cat" -> "dog" -> "animal"
let result = replacer.replace("The cat sat on the mat");
assert_eq!(result, "The animal sat on the mat");
}
#[test]
fn test_text_replacer_regex_capture_groups() {
let replacer = TextReplacer::builder()
.pattern(r"(\d{4})-(\d{2})-(\d{2})")
.replacement("$3/$2/$1")
.regex(true)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("Date: 2023-12-01");
assert_eq!(result, "Date: 01/12/2023");
}
#[test]
fn test_text_replacer_case_sensitive() {
let replacer = TextReplacer::builder()
.pattern("Hello")
.replacement("Hi")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("Hello world, hello universe");
assert_eq!(result, "Hi world, hello universe");
}
#[test]
fn test_text_replacer_regex_case_insensitive() {
let replacer = TextReplacer::builder()
.pattern(r"(?i)hello")
.replacement("Hi")
.regex(true)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("Hello world, HELLO universe");
assert_eq!(result, "Hi world, Hi universe");
}
#[test]
fn test_text_replacer_empty_input() {
let replacer = TextReplacer::builder()
.pattern("test")
.replacement("replacement")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("");
assert_eq!(result, "");
}
#[test]
fn test_text_replacer_no_matches() {
let replacer = TextReplacer::builder()
.pattern("xyz")
.replacement("abc")
.regex(false)
.build()
.expect("Failed to build replacer");
let input = "Hello world";
let result = replacer.replace(input);
assert_eq!(result, input);
}
#[test]
fn test_text_replacer_file_operations() {
// Create a temporary file with test content
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let test_content = "Hello world, there are 123 items";
fs::write(temp_file.path(), test_content).expect("Failed to write to temp file");
let replacer = TextReplacer::builder()
.pattern(r"\d+")
.replacement("NUMBER")
.regex(true)
.and()
.pattern("world")
.replacement("universe")
.regex(false)
.build()
.expect("Failed to build replacer");
// Test replace_file
let result = replacer.replace_file(temp_file.path()).expect("Failed to replace file content");
assert_eq!(result, "Hello universe, there are NUMBER items");
// Verify original file is unchanged
let original_content = fs::read_to_string(temp_file.path()).expect("Failed to read original file");
assert_eq!(original_content, test_content);
}
#[test]
fn test_text_replacer_file_in_place() {
// Create a temporary file with test content
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
let test_content = "Hello world, there are 123 items";
fs::write(temp_file.path(), test_content).expect("Failed to write to temp file");
let replacer = TextReplacer::builder()
.pattern("world")
.replacement("universe")
.regex(false)
.build()
.expect("Failed to build replacer");
// Test replace_file_in_place
replacer.replace_file_in_place(temp_file.path()).expect("Failed to replace file in place");
// Verify file content was changed
let new_content = fs::read_to_string(temp_file.path()).expect("Failed to read modified file");
assert_eq!(new_content, "Hello universe, there are 123 items");
}
#[test]
fn test_text_replacer_file_to_file() {
// Create source file
let mut source_file = NamedTempFile::new().expect("Failed to create source file");
let test_content = "Hello world, there are 123 items";
fs::write(source_file.path(), test_content).expect("Failed to write to source file");
// Create destination file
let dest_file = NamedTempFile::new().expect("Failed to create dest file");
let replacer = TextReplacer::builder()
.pattern(r"\d+")
.replacement("NUMBER")
.regex(true)
.build()
.expect("Failed to build replacer");
// Test replace_file_to
replacer.replace_file_to(source_file.path(), dest_file.path())
.expect("Failed to replace file to destination");
// Verify source file is unchanged
let source_content = fs::read_to_string(source_file.path()).expect("Failed to read source file");
assert_eq!(source_content, test_content);
// Verify destination file has replaced content
let dest_content = fs::read_to_string(dest_file.path()).expect("Failed to read dest file");
assert_eq!(dest_content, "Hello world, there are NUMBER items");
}
#[test]
fn test_text_replacer_invalid_regex() {
let result = TextReplacer::builder()
.pattern("[invalid regex")
.replacement("test")
.regex(true)
.build();
assert!(result.is_err());
}
#[test]
fn test_text_replacer_builder_default_regex_false() {
let replacer = TextReplacer::builder()
.pattern(r"\d+")
.replacement("NUMBER")
.build()
.expect("Failed to build replacer");
// Should treat as literal since regex defaults to false
let result = replacer.replace(r"Match \d+ pattern");
assert_eq!(result, "Match NUMBER pattern");
}
#[test]
fn test_text_replacer_complex_regex() {
let replacer = TextReplacer::builder()
.pattern(r"(\w+)@(\w+\.\w+)")
.replacement("EMAIL_ADDRESS")
.regex(true)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("Contact john@example.com or jane@test.org");
assert_eq!(result, "Contact EMAIL_ADDRESS or EMAIL_ADDRESS");
}
#[test]
fn test_text_replacer_multiline_text() {
let replacer = TextReplacer::builder()
.pattern(r"^\s*//.*$")
.replacement("")
.regex(true)
.build()
.expect("Failed to build replacer");
let input = "function test() {\n // This is a comment\n return true;\n // Another comment\n}";
let result = replacer.replace(input);
// Note: This test depends on how the regex engine handles multiline mode
// The actual behavior might need adjustment based on regex flags
assert!(result.contains("function test()"));
assert!(result.contains("return true;"));
}
#[test]
fn test_text_replacer_unicode_text() {
let replacer = TextReplacer::builder()
.pattern("café")
.replacement("coffee")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace("I love café in the morning");
assert_eq!(result, "I love coffee in the morning");
}
#[test]
fn test_text_replacer_large_text() {
let large_text = "word ".repeat(10000);
let replacer = TextReplacer::builder()
.pattern("word")
.replacement("term")
.regex(false)
.build()
.expect("Failed to build replacer");
let result = replacer.replace(&large_text);
assert_eq!(result, "term ".repeat(10000));
}