use std::path::Path;
use tokf::config;
pub fn cmd_eject(filter: &str, global: bool, no_cache: bool) -> i32 {
match eject(filter, global, no_cache) {
Ok(()) => 0,
Err(e) => {
eprintln!("[tokf] error: {e:#}");
1
}
}
}
fn eject(filter: &str, global: bool, no_cache: bool) -> anyhow::Result<()> {
let target_base = if global {
tokf::paths::user_dir()
.ok_or_else(|| anyhow::anyhow!("could not determine config directory"))?
.join("filters")
} else {
std::env::current_dir()?.join(".tokf/filters")
};
eject_to(filter, &target_base, no_cache)
}
fn eject_to(filter: &str, target_base: &Path, no_cache: bool) -> anyhow::Result<()> {
let filter_name = filter.strip_suffix(".toml").unwrap_or(filter);
let search_dirs = config::default_search_dirs();
let resolved = if no_cache {
config::discover_all_filters(&search_dirs)?
} else {
config::cache::discover_with_cache(&search_dirs)?
};
let found = resolved.iter().find(|f| f.matches_name(filter_name));
let resolved_filter =
found.ok_or_else(|| anyhow::anyhow!("filter not found: {filter_name}"))?;
let target_toml = target_base.join(&resolved_filter.relative_path);
if target_toml.exists() {
anyhow::bail!(
"filter already exists at {} — remove it first to re-eject",
target_toml.display()
);
}
let toml_content = if resolved_filter.priority == tokf::config::STDLIB_PRIORITY {
config::get_embedded_filter(&resolved_filter.relative_path)
.ok_or_else(|| anyhow::anyhow!("embedded filter not readable"))?
.to_string()
} else {
std::fs::read_to_string(&resolved_filter.source_path)?
};
if let Some(parent) = target_toml.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&target_toml, &toml_content)?;
eprintln!("[tokf] wrote {}", target_toml.display());
copy_test_suite(resolved_filter, target_base)?;
eprintln!("[tokf] ejected {filter_name} to {}", target_base.display());
Ok(())
}
fn copy_test_suite(
resolved_filter: &config::ResolvedFilter,
target_base: &Path,
) -> anyhow::Result<()> {
let stem = resolved_filter
.relative_path
.file_stem()
.unwrap_or_default()
.to_string_lossy();
let test_dir_name = format!("{stem}_test");
let test_dir_relative = resolved_filter
.relative_path
.parent()
.unwrap_or_else(|| Path::new(""))
.join(&test_dir_name);
let wrote = if resolved_filter.priority == tokf::config::STDLIB_PRIORITY {
copy_embedded_test_dir(&test_dir_relative, target_base)?
} else {
let source_test_dir = resolved_filter
.source_path
.parent()
.unwrap_or_else(|| Path::new("."))
.join(&test_dir_name);
if source_test_dir.is_dir() {
let target_test_dir = target_base.join(&test_dir_relative);
copy_dir_flat(&source_test_dir, &target_test_dir)?;
true
} else {
false
}
};
if wrote {
eprintln!("[tokf] copied test suite: {test_dir_name}/");
}
Ok(())
}
fn copy_embedded_test_dir(test_dir_relative: &Path, target_base: &Path) -> anyhow::Result<bool> {
let files = config::get_embedded_dir_files(test_dir_relative);
if files.is_empty() {
return Ok(false);
}
for (rel_path, content) in &files {
let dest = target_base.join(rel_path);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&dest, content)?;
eprintln!("[tokf] wrote {}", dest.display());
}
Ok(true)
}
fn copy_dir_flat(src_dir: &Path, dest_dir: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(dest_dir)?;
let entries = std::fs::read_dir(src_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let dest = dest_dir.join(entry.file_name());
std::fs::copy(&path, &dest)?;
eprintln!("[tokf] wrote {}", dest.display());
}
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn eject_builtin_filter_writes_toml() {
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("filters");
eject_to("cargo/build", &target, true).unwrap();
let toml_path = target.join("cargo/build.toml");
assert!(toml_path.exists(), "toml file should be created");
let content = std::fs::read_to_string(&toml_path).unwrap();
assert!(
content.contains("command"),
"toml should contain a command field"
);
}
#[test]
fn eject_builtin_copies_test_dir() {
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("filters");
eject_to("cargo/build", &target, true).unwrap();
let test_dir = target.join("cargo/build_test");
assert!(test_dir.is_dir(), "test directory should be created");
let entries: Vec<_> = std::fs::read_dir(&test_dir)
.unwrap()
.filter_map(Result::ok)
.collect();
assert!(!entries.is_empty(), "test directory should contain files");
}
#[test]
fn eject_refuses_if_target_exists() {
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("filters");
eject_to("cargo/build", &target, true).unwrap();
let result = eject_to("cargo/build", &target, true);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("already exists"),
"error should mention 'already exists', got: {err}"
);
}
#[test]
fn eject_nonexistent_filter_errors() {
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("filters");
let result = eject_to("nonexistent/filter", &target, true);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("not found"),
"error should mention 'not found', got: {err}"
);
}
#[test]
fn eject_strips_toml_extension() {
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("filters");
eject_to("cargo/build.toml", &target, true).unwrap();
let toml_path = target.join("cargo/build.toml");
assert!(toml_path.exists());
}
}