ghost_core/recipe/
store.rs1use anyhow::{Context, Result};
4use std::path::PathBuf;
5
6use super::types::Recipe;
7
8pub struct RecipeStore {
10 dir: PathBuf,
11}
12
13impl RecipeStore {
14 pub fn open() -> Result<Self> {
19 let base = match std::env::var_os("GHOST_DATA_DIR") {
20 Some(dir) => PathBuf::from(dir),
21 None => dirs::home_dir()
22 .ok_or_else(|| anyhow::anyhow!("Cannot determine home directory"))?
23 .join(".ghost"),
24 };
25 let dir = base.join("recipes");
26 std::fs::create_dir_all(&dir)
27 .with_context(|| format!("Failed to create recipe dir: {}", dir.display()))?;
28 Ok(Self { dir })
29 }
30
31 pub fn open_at(dir: PathBuf) -> Result<Self> {
33 std::fs::create_dir_all(&dir)?;
34 Ok(Self { dir })
35 }
36
37 pub fn list(&self) -> Result<Vec<Recipe>> {
39 let mut recipes = vec![];
40 for entry in std::fs::read_dir(&self.dir)? {
41 let entry = entry?;
42 let path = entry.path();
43 if path.extension().and_then(|e| e.to_str()) == Some("json") {
44 if let Ok(r) = self.load_file(&path) {
45 recipes.push(r);
46 }
47 }
48 }
49 recipes.sort_by(|a, b| a.name.cmp(&b.name));
50 Ok(recipes)
51 }
52
53 pub fn get(&self, name: &str) -> Result<Recipe> {
55 let path = self.recipe_path(name);
56 self.load_file(&path)
57 .with_context(|| format!("Recipe '{}' not found", name))
58 }
59
60 pub fn save(&self, recipe: &Recipe) -> Result<()> {
62 let path = self.recipe_path(&recipe.name);
63 let json = serde_json::to_string_pretty(recipe)?;
64 std::fs::write(&path, json)
65 .with_context(|| format!("Failed to write recipe to {}", path.display()))
66 }
67
68 pub fn save_json(&self, json: &str) -> Result<Recipe> {
70 let recipe: Recipe = serde_json::from_str(json).context("Invalid recipe JSON")?;
71 self.save(&recipe)?;
72 Ok(recipe)
73 }
74
75 pub fn delete(&self, name: &str) -> Result<()> {
77 let path = self.recipe_path(name);
78 if path.exists() {
79 std::fs::remove_file(&path)
80 .with_context(|| format!("Failed to delete recipe '{}'", name))
81 } else {
82 Err(anyhow::anyhow!("Recipe '{}' not found", name))
83 }
84 }
85
86 fn recipe_path(&self, name: &str) -> PathBuf {
87 let safe = name.replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], "_");
88 self.dir.join(format!("{safe}.json"))
89 }
90
91 fn load_file(&self, path: &PathBuf) -> Result<Recipe> {
92 let data = std::fs::read_to_string(path)?;
93 serde_json::from_str(&data).with_context(|| format!("Invalid JSON in {}", path.display()))
94 }
95}