use anyhow::{Context, Result};
use glob::Pattern;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct PackagerConfig {
pub input_dir: String,
pub output_file: String,
pub extra_files: Vec<String>,
pub ignore_patterns: Vec<String>,
}
impl Default for PackagerConfig {
fn default() -> Self {
Self {
input_dir: "src".to_string(),
output_file: "src_code.txt".to_string(),
extra_files: Vec::new(),
ignore_patterns: Vec::new(),
}
}
}
pub fn parse_rule_string(rule_string: &str, separator: &str) -> Result<(Vec<String>, Vec<String>)> {
let mut extra_files = Vec::new();
let mut ignore_patterns = Vec::new();
for item in rule_string.split(separator) {
let trimmed = item.trim();
if trimmed.is_empty() {
continue;
}
if let Some(ignore_pattern) = trimmed.strip_prefix('!') {
let pattern = ignore_pattern.trim().to_string();
if !pattern.is_empty() {
ignore_patterns.push(pattern);
}
} else {
extra_files.push(trimmed.to_string());
}
}
Ok((extra_files, ignore_patterns))
}
pub fn merge_rule_config(
rule_extra: Vec<String>,
rule_ignore: Vec<String>,
cli_extra: Vec<String>,
cli_ignore: Vec<String>,
) -> (Vec<String>, Vec<String>) {
let mut extra_files = rule_extra;
let mut ignore_patterns = rule_ignore;
extra_files.extend(cli_extra);
ignore_patterns.extend(cli_ignore);
(extra_files, ignore_patterns)
}
pub fn package_code(config: &PackagerConfig) -> Result<()> {
let compiled_ignores: Result<Vec<Pattern>> = config
.ignore_patterns
.iter()
.map(|p| Pattern::new(p).context(format!("Invalid ignore pattern: {}", p)))
.collect();
let compiled_ignores = compiled_ignores?;
let mut output = File::create(&config.output_file).context(format!(
"Failed to create output file: {}",
config.output_file
))?;
for file_pattern in &config.extra_files {
let matches =
glob::glob(file_pattern).context(format!("Invalid file pattern: {}", file_pattern))?;
for entry in matches {
let path = entry.context("Failed to parse file path")?;
if path.exists() {
if path.is_dir() {
process_directory(
&path.to_string_lossy(),
&mut output,
&compiled_ignores,
&path.to_string_lossy(), )
.context(format!(
"Failed to process extra directory: {}",
path.display()
))?;
} else if path.is_file() {
write_file_to_output(&path.to_string_lossy(), &mut output)
.context(format!("Failed to process extra file: {}", path.display()))?;
}
}
}
}
if Path::new(&config.input_dir).exists() && config.input_dir != "." {
process_directory(
&config.input_dir,
&mut output,
&compiled_ignores,
&config.input_dir,
)
.context("Failed to process input directory")?;
}
Ok(())
}
fn process_directory(
dir_path: &str,
output: &mut File,
ignore_patterns: &[Pattern],
base_dir: &str,
) -> Result<()> {
let entries =
fs::read_dir(dir_path).context(format!("Failed to read directory: {}", dir_path))?;
for entry in entries {
let entry = entry.context("Failed to read directory entry")?;
let path = entry.path();
let path_str = path.to_string_lossy();
if should_ignore(&path, ignore_patterns, base_dir) {
continue;
}
if path.is_dir() {
process_directory(&path_str, output, ignore_patterns, base_dir)?;
} else if path.is_file() {
write_file_to_output(&path_str, output)
.context(format!("Failed to process file: {}", path_str))?;
}
}
Ok(())
}
fn should_ignore(path: &Path, ignore_patterns: &[Pattern], base_dir: &str) -> bool {
let path_str = path.to_string_lossy();
for pattern in ignore_patterns {
if pattern.matches(&path_str) {
return true;
}
if let Ok(relative_path) = path.strip_prefix(base_dir) {
let relative_str = relative_path.to_string_lossy();
if pattern.matches(&relative_str) {
return true;
}
}
}
false
}
fn write_file_to_output(file_path: &str, output: &mut File) -> Result<()> {
let content =
fs::read_to_string(file_path).context(format!("Failed to read file: {}", file_path))?;
writeln!(output, "```{}", file_path)?;
write!(output, "{}", content)?;
if !content.ends_with('\n') {
writeln!(output)?;
}
writeln!(output, "```")?;
writeln!(output)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_parse_rule_string_basic() {
let rule = "Cargo.toml + src + !target";
let (extra, ignore) = parse_rule_string(rule, " + ").unwrap();
assert_eq!(extra, vec!["Cargo.toml", "src"]);
assert_eq!(ignore, vec!["target"]);
}
#[test]
fn test_parse_rule_string_complex() {
let rule = "Cargo.toml + src + !src/nodes + src/nodes/mod.rs + !src/bin";
let (extra, ignore) = parse_rule_string(rule, " + ").unwrap();
assert_eq!(extra, vec!["Cargo.toml", "src", "src/nodes/mod.rs"]);
assert_eq!(ignore, vec!["src/nodes", "src/bin"]);
}
#[test]
fn test_parse_rule_string_with_whitespace() {
let rule = " file1.txt + ! pattern/* + dir/ + ! *.tmp ";
let (extra, ignore) = parse_rule_string(rule, " + ").unwrap();
assert_eq!(extra, vec!["file1.txt", "dir/"]);
assert_eq!(ignore, vec!["pattern/*", "*.tmp"]);
}
#[test]
fn test_parse_rule_string_empty_and_blank() {
let rule = " + file.txt + + !pattern + ";
let (extra, ignore) = parse_rule_string(rule, " + ").unwrap();
assert_eq!(extra, vec!["file.txt"]);
assert_eq!(ignore, vec!["pattern"]);
}
#[test]
fn test_parse_rule_string_custom_separator() {
let rule = "file.txt | src | !target";
let (extra, ignore) = parse_rule_string(rule, " | ").unwrap();
assert_eq!(extra, vec!["file.txt", "src"]);
assert_eq!(ignore, vec!["target"]);
}
#[test]
fn test_parse_rule_string_only_ignores() {
let rule = "!target + !*.tmp + !node_modules";
let (extra, ignore) = parse_rule_string(rule, " + ").unwrap();
assert!(extra.is_empty());
assert_eq!(ignore, vec!["target", "*.tmp", "node_modules"]);
}
#[test]
fn test_parse_rule_string_only_extras() {
let rule = "src + Cargo.toml + README.md";
let (extra, ignore) = parse_rule_string(rule, " + ").unwrap();
assert_eq!(extra, vec!["src", "Cargo.toml", "README.md"]);
assert!(ignore.is_empty());
}
#[test]
fn test_merge_rule_config() {
let rule_extra = vec!["src".to_string(), "docs".to_string()];
let rule_ignore = vec!["target".to_string(), "*.tmp".to_string()];
let cli_extra = vec!["Cargo.toml".to_string()];
let cli_ignore = vec!["node_modules".to_string()];
let (merged_extra, merged_ignore) =
merge_rule_config(rule_extra, rule_ignore, cli_extra, cli_ignore);
assert_eq!(merged_extra, vec!["src", "docs", "Cargo.toml"]);
assert_eq!(merged_ignore, vec!["target", "*.tmp", "node_modules"]);
}
#[test]
fn test_merge_rule_config_empty() {
let (merged_extra, merged_ignore) =
merge_rule_config(Vec::new(), Vec::new(), Vec::new(), Vec::new());
assert!(merged_extra.is_empty());
assert!(merged_ignore.is_empty());
}
#[test]
fn test_packager_config_default() {
let config = PackagerConfig::default();
assert_eq!(config.input_dir, "src");
assert_eq!(config.output_file, "src_code.txt");
assert!(config.extra_files.is_empty());
assert!(config.ignore_patterns.is_empty());
}
#[test]
fn test_should_ignore() {
let patterns = vec![
Pattern::new("*.tmp").unwrap(),
Pattern::new("target/*").unwrap(),
];
let base_dir = "/project";
let path = Path::new("/project/src/main.rs");
assert!(!should_ignore(path, &patterns, base_dir));
let ignore_path = Path::new("/project/test.tmp");
assert!(should_ignore(ignore_path, &patterns, base_dir));
}
#[test]
fn test_write_file_to_output() -> Result<()> {
let temp_dir = TempDir::new()?;
let output_path = temp_dir.path().join("src_output.txt");
let test_file_path = temp_dir.path().join("test.rs");
let test_content = "fn main() {\n println!(\"Hello\");\n}";
fs::write(&test_file_path, test_content)?;
let mut output_file = File::create(&output_path)?;
write_file_to_output(&test_file_path.to_string_lossy(), &mut output_file)?;
let output_content = fs::read_to_string(&output_path)?;
assert!(output_content.contains("```"));
assert!(output_content.contains("fn main()"));
assert!(output_content.contains("Hello"));
Ok(())
}
#[test]
fn test_write_file_to_output_with_trailing_newline() -> Result<()> {
let temp_dir = TempDir::new()?;
let output_path = temp_dir.path().join("src_output.txt");
let test_file_path = temp_dir.path().join("test.rs");
let test_content = "fn main() {\n println!(\"Hello\");\n}";
fs::write(&test_file_path, test_content)?;
let mut output_file = File::create(&output_path)?;
write_file_to_output(&test_file_path.to_string_lossy(), &mut output_file)?;
let output_content = fs::read_to_string(&output_path)?;
assert!(output_content.ends_with("```\n\n"));
Ok(())
}
#[test]
fn test_package_code_with_invalid_config() {
let config = PackagerConfig {
input_dir: "/nonexistent/directory".to_string(),
output_file: "src_output.txt".to_string(),
extra_files: vec![],
ignore_patterns: vec![],
};
let result = package_code(&config);
assert!(result.is_ok());
}
#[test]
fn test_package_code_integration() -> Result<()> {
let temp_dir = TempDir::new()?;
let src_dir = temp_dir.path().join("src");
fs::create_dir(&src_dir)?;
let main_rs = src_dir.join("main.rs");
fs::write(&main_rs, "fn main() { println!(\"Hello\"); }")?;
let lib_rs = src_dir.join("lib.rs");
fs::write(&lib_rs, "pub fn add(a: i32, b: i32) -> i32 { a + b }")?;
let cargo_toml = temp_dir.path().join("Cargo.toml");
fs::write(
&cargo_toml,
"[package]\nname = \"test\"\nversion = \"0.1.0\"",
)?;
let output_path = temp_dir.path().join("src_output.txt");
let config = PackagerConfig {
input_dir: temp_dir.path().to_string_lossy().to_string(),
output_file: output_path.to_string_lossy().to_string(),
extra_files: vec!["Cargo.toml".to_string(), "src/*.rs".to_string()],
ignore_patterns: vec![],
};
package_code(&config)?;
assert!(output_path.exists());
let output_content = fs::read_to_string(&output_path)?;
assert!(output_content.contains("Cargo.toml"));
assert!(output_content.contains("main.rs"));
assert!(output_content.contains("lib.rs"));
assert!(output_content.contains("Hello"));
Ok(())
}
}