- Improve platform detection logic for more robust package management. - Enhance error handling and reporting in package commands. - Refactor code for better readability and maintainability. - Add comprehensive tests to cover package management functionality. - Improve test coverage for various scenarios and edge cases.
293 lines
8.6 KiB
Rust
293 lines
8.6 KiB
Rust
use regex::Regex;
|
|
use std::fs;
|
|
use std::io::{self, Read};
|
|
use std::path::Path;
|
|
|
|
/// Represents the type of replacement to perform.
|
|
#[derive(Clone)]
|
|
pub enum ReplaceMode {
|
|
/// Regex-based replacement using the `regex` crate
|
|
Regex(Regex),
|
|
/// Literal substring replacement (non-regex)
|
|
Literal(String),
|
|
}
|
|
|
|
/// A single replacement operation with a pattern and replacement text
|
|
#[derive(Clone)]
|
|
pub struct ReplacementOperation {
|
|
mode: ReplaceMode,
|
|
replacement: String,
|
|
}
|
|
|
|
impl ReplacementOperation {
|
|
/// Applies this replacement operation to the input text
|
|
fn apply(&self, input: &str) -> String {
|
|
match &self.mode {
|
|
ReplaceMode::Regex(re) => re.replace_all(input, self.replacement.as_str()).to_string(),
|
|
ReplaceMode::Literal(search) => input.replace(search, &self.replacement),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Text replacer that can perform multiple replacement operations
|
|
/// in a single pass over the input text.
|
|
#[derive(Clone)]
|
|
pub struct TextReplacer {
|
|
operations: Vec<ReplacementOperation>,
|
|
}
|
|
|
|
impl TextReplacer {
|
|
/// Creates a new builder for configuring a TextReplacer
|
|
pub fn builder() -> TextReplacerBuilder {
|
|
TextReplacerBuilder::default()
|
|
}
|
|
|
|
/// Applies all configured replacement operations to the input text
|
|
pub fn replace(&self, input: &str) -> String {
|
|
let mut result = input.to_string();
|
|
|
|
// Apply each replacement operation in sequence
|
|
for op in &self.operations {
|
|
result = op.apply(&result);
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Reads a file, applies all replacements, and returns the result as a string
|
|
pub fn replace_file<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
|
|
let mut file = fs::File::open(path)?;
|
|
let mut content = String::new();
|
|
file.read_to_string(&mut content)?;
|
|
|
|
Ok(self.replace(&content))
|
|
}
|
|
|
|
/// Reads a file, applies all replacements, and writes the result back to the file
|
|
pub fn replace_file_in_place<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
|
let content = self.replace_file(&path)?;
|
|
fs::write(path, content)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Reads a file, applies all replacements, and writes the result to a new file
|
|
pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>(
|
|
&self,
|
|
input_path: P1,
|
|
output_path: P2,
|
|
) -> io::Result<()> {
|
|
let content = self.replace_file(&input_path)?;
|
|
fs::write(output_path, content)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Builder for the TextReplacer.
|
|
#[derive(Default, Clone)]
|
|
pub struct TextReplacerBuilder {
|
|
operations: Vec<ReplacementOperation>,
|
|
pattern: Option<String>,
|
|
replacement: Option<String>,
|
|
use_regex: bool,
|
|
case_insensitive: bool,
|
|
}
|
|
|
|
impl TextReplacerBuilder {
|
|
/// Sets the pattern to search for
|
|
pub fn pattern(mut self, pat: &str) -> Self {
|
|
self.pattern = Some(pat.to_string());
|
|
self
|
|
}
|
|
|
|
/// Sets the replacement text
|
|
pub fn replacement(mut self, rep: &str) -> Self {
|
|
self.replacement = Some(rep.to_string());
|
|
self
|
|
}
|
|
|
|
/// Sets whether to use regex
|
|
pub fn regex(mut self, yes: bool) -> Self {
|
|
self.use_regex = yes;
|
|
self
|
|
}
|
|
|
|
/// Sets whether the replacement should be case-insensitive
|
|
pub fn case_insensitive(mut self, yes: bool) -> Self {
|
|
self.case_insensitive = yes;
|
|
self
|
|
}
|
|
|
|
/// Adds another replacement operation to the chain and resets the builder for a new operation
|
|
pub fn and(mut self) -> Self {
|
|
self.add_current_operation();
|
|
self
|
|
}
|
|
|
|
// Helper method to add the current operation to the list
|
|
fn add_current_operation(&mut self) -> bool {
|
|
if let Some(pattern) = self.pattern.take() {
|
|
let replacement = self.replacement.take().unwrap_or_default();
|
|
let use_regex = self.use_regex;
|
|
let case_insensitive = self.case_insensitive;
|
|
|
|
// Reset current settings
|
|
self.use_regex = false;
|
|
self.case_insensitive = false;
|
|
|
|
// Create the replacement mode
|
|
let mode = if use_regex {
|
|
let mut regex_pattern = pattern;
|
|
|
|
// If case insensitive, add the flag to the regex pattern
|
|
if case_insensitive && !regex_pattern.starts_with("(?i)") {
|
|
regex_pattern = format!("(?i){}", regex_pattern);
|
|
}
|
|
|
|
match Regex::new(®ex_pattern) {
|
|
Ok(re) => ReplaceMode::Regex(re),
|
|
Err(_) => return false, // Failed to compile regex
|
|
}
|
|
} else {
|
|
// For literal replacement, we'll handle case insensitivity differently
|
|
// since String::replace doesn't have a case-insensitive option
|
|
if case_insensitive {
|
|
return false; // Case insensitive not supported for literal
|
|
}
|
|
ReplaceMode::Literal(pattern)
|
|
};
|
|
|
|
self.operations
|
|
.push(ReplacementOperation { mode, replacement });
|
|
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Builds the TextReplacer with all configured replacement operations
|
|
pub fn build(mut self) -> Result<TextReplacer, String> {
|
|
// If there's a pending replacement operation, add it
|
|
self.add_current_operation();
|
|
|
|
// Ensure we have at least one replacement operation
|
|
if self.operations.is_empty() {
|
|
return Err("No replacement operations configured".to_string());
|
|
}
|
|
|
|
Ok(TextReplacer {
|
|
operations: self.operations,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::io::{Seek, SeekFrom, Write};
|
|
use tempfile::NamedTempFile;
|
|
|
|
#[test]
|
|
fn test_regex_replace() {
|
|
let replacer = TextReplacer::builder()
|
|
.pattern(r"\bfoo\b")
|
|
.replacement("bar")
|
|
.regex(true)
|
|
.build()
|
|
.unwrap();
|
|
|
|
let input = "foo bar foo baz";
|
|
let output = replacer.replace(input);
|
|
|
|
assert_eq!(output, "bar bar bar baz");
|
|
}
|
|
|
|
#[test]
|
|
fn test_literal_replace() {
|
|
let replacer = TextReplacer::builder()
|
|
.pattern("foo")
|
|
.replacement("qux")
|
|
.regex(false)
|
|
.build()
|
|
.unwrap();
|
|
|
|
let input = "foo bar foo baz";
|
|
let output = replacer.replace(input);
|
|
|
|
assert_eq!(output, "qux bar qux baz");
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_replacements() {
|
|
let replacer = TextReplacer::builder()
|
|
.pattern("foo")
|
|
.replacement("qux")
|
|
.and()
|
|
.pattern("bar")
|
|
.replacement("baz")
|
|
.build()
|
|
.unwrap();
|
|
|
|
let input = "foo bar foo";
|
|
let output = replacer.replace(input);
|
|
|
|
assert_eq!(output, "qux baz qux");
|
|
}
|
|
|
|
#[test]
|
|
fn test_case_insensitive_regex() {
|
|
let replacer = TextReplacer::builder()
|
|
.pattern("foo")
|
|
.replacement("bar")
|
|
.regex(true)
|
|
.case_insensitive(true)
|
|
.build()
|
|
.unwrap();
|
|
|
|
let input = "FOO foo Foo";
|
|
let output = replacer.replace(input);
|
|
|
|
assert_eq!(output, "bar bar bar");
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_operations() -> io::Result<()> {
|
|
// Create a temporary file
|
|
let mut temp_file = NamedTempFile::new()?;
|
|
writeln!(temp_file, "foo bar foo baz")?;
|
|
|
|
// Flush the file to ensure content is written
|
|
temp_file.as_file_mut().flush()?;
|
|
|
|
let replacer = TextReplacer::builder()
|
|
.pattern("foo")
|
|
.replacement("qux")
|
|
.build()
|
|
.unwrap();
|
|
|
|
// Test replace_file
|
|
let result = replacer.replace_file(temp_file.path())?;
|
|
assert_eq!(result, "qux bar qux baz\n");
|
|
|
|
// Test replace_file_in_place
|
|
replacer.replace_file_in_place(temp_file.path())?;
|
|
|
|
// Verify the file was updated - need to seek to beginning of file first
|
|
let mut content = String::new();
|
|
temp_file.as_file_mut().seek(SeekFrom::Start(0))?;
|
|
temp_file.as_file_mut().read_to_string(&mut content)?;
|
|
assert_eq!(content, "qux bar qux baz\n");
|
|
|
|
// Test replace_file_to with a new temporary file
|
|
let output_file = NamedTempFile::new()?;
|
|
replacer.replace_file_to(temp_file.path(), output_file.path())?;
|
|
|
|
// Verify the output file has the replaced content
|
|
let mut output_content = String::new();
|
|
fs::File::open(output_file.path())?.read_to_string(&mut output_content)?;
|
|
assert_eq!(output_content, "qux bar qux baz\n");
|
|
|
|
Ok(())
|
|
}
|
|
}
|