use std::path::Path;
use super::Recipe;
use crate::recipe::compiler::CompiledRecipe;
pub fn load_from_dir(dir: &Path) -> anyhow::Result<Vec<Recipe>> {
let mut recipes = Vec::new();
if !dir.exists() {
return Ok(recipes);
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map(|e| e == "toml").unwrap_or(false) {
match std::fs::read_to_string(&path) {
Ok(content) => match toml::from_str::<Recipe>(&content) {
Ok(r) => recipes.push(r),
Err(e) => {
eprintln!("bctx recipe: skipping {:?}: {e}", path.file_name());
}
},
Err(e) => {
eprintln!("bctx recipe: cannot read {:?}: {e}", path.file_name());
}
}
}
}
Ok(recipes)
}
pub fn load_project_recipes(root: &Path) -> Vec<CompiledRecipe> {
let dir = root.join(".bctx").join("recipes");
compile_dir(&dir)
}
pub fn load_user_recipes() -> Vec<CompiledRecipe> {
let home = forge::home_dir();
if home.is_empty() {
return Vec::new();
}
let dir = std::path::PathBuf::from(home)
.join(".config")
.join("bctx")
.join("recipes");
compile_dir(&dir)
}
pub fn load_all_recipes(root: &Path) -> Vec<CompiledRecipe> {
let mut recipes = load_project_recipes(root);
recipes.extend(load_user_recipes());
recipes
}
fn compile_dir(dir: &Path) -> Vec<CompiledRecipe> {
let raw = match load_from_dir(dir) {
Ok(r) => r,
Err(e) => {
eprintln!("bctx recipe: cannot read directory {dir:?}: {e}");
return Vec::new();
}
};
let mut compiled = Vec::new();
for recipe in raw {
match CompiledRecipe::compile(recipe) {
Ok(c) => compiled.push(c),
Err(e) => eprintln!("bctx recipe: compile error: {e}"),
}
}
compiled
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
fn write_recipe(dir: &Path, name: &str, content: &str) {
let path = dir.join(name);
let mut f = std::fs::File::create(path).unwrap();
f.write_all(content.as_bytes()).unwrap();
}
#[test]
fn load_valid_toml() {
let tmp = TempDir::new().unwrap();
write_recipe(
tmp.path(),
"my-tool.toml",
"name = \"my-tool build\"\nmatch_command = \"my-tool build\"\nlens = [\"clarity\"]\nstrip_lines = [\"^Compiling.*\"]\non_empty = \"my-tool: built\"\n",
);
let recipes = load_from_dir(tmp.path()).unwrap();
assert_eq!(recipes.len(), 1);
assert_eq!(recipes[0].name, "my-tool build");
assert_eq!(recipes[0].strip_lines, vec!["^Compiling.*"]);
}
#[test]
fn skips_invalid_toml() {
let tmp = TempDir::new().unwrap();
write_recipe(tmp.path(), "bad.toml", "not valid toml [[[");
let recipes = load_from_dir(tmp.path()).unwrap();
assert!(recipes.is_empty());
}
#[test]
fn load_empty_dir() {
let tmp = TempDir::new().unwrap();
let recipes = load_from_dir(tmp.path()).unwrap();
assert!(recipes.is_empty());
}
#[test]
fn load_nonexistent_dir() {
let dir = std::path::PathBuf::from("/nonexistent/path/recipes");
let recipes = load_from_dir(&dir).unwrap();
assert!(recipes.is_empty());
}
#[test]
fn compile_dir_skips_bad_regex() {
let tmp = TempDir::new().unwrap();
write_recipe(
tmp.path(),
"bad_regex.toml",
"name = \"broken\"\nmatch_command = \"[[invalid regex\"\nstrip_lines = []\n",
);
let compiled = compile_dir(tmp.path());
assert!(compiled.is_empty());
}
#[test]
fn load_all_project_takes_priority() {
let tmp = TempDir::new().unwrap();
let bctx_dir = tmp.path().join(".bctx").join("recipes");
std::fs::create_dir_all(&bctx_dir).unwrap();
write_recipe(
&bctx_dir,
"proj.toml",
"name = \"project recipe\"\nmatch_command = \"my-tool\"\nstrip_lines = []\n",
);
let all = load_all_recipes(tmp.path());
assert!(!all.is_empty());
assert_eq!(all[0].recipe.name, "project recipe");
}
}