cooklang_find/fetcher/
mod.rs1use crate::model::{RecipeEntry, RecipeEntryError};
8use camino::{Utf8Path, Utf8PathBuf};
9use thiserror::Error;
10
11#[derive(Error, Debug)]
13pub enum FetchError {
14 #[error("Failed to read recipe file: {0}")]
15 IoError(#[from] std::io::Error),
16
17 #[error("Failed to parse recipe: {0}")]
18 RecipeEntryError(#[from] RecipeEntryError),
19
20 #[error("Invalid recipe path: {0}")]
21 InvalidPath(Utf8PathBuf),
22}
23
24pub fn get_recipe<P: AsRef<Utf8Path>>(
53 base_dirs: impl IntoIterator<Item = P>,
54 name: P,
55) -> Result<RecipeEntry, FetchError> {
56 let name = name.as_ref();
57
58 for base_dir in base_dirs {
59 if name.extension().is_some() {
60 let recipe_path = base_dir.as_ref().join(name);
62 if recipe_path.exists() {
63 return RecipeEntry::from_path(recipe_path).map_err(FetchError::RecipeEntryError);
64 }
65 } else {
66 let cook_path = base_dir.as_ref().join(format!("{name}.cook"));
68 if cook_path.exists() {
69 return RecipeEntry::from_path(cook_path).map_err(FetchError::RecipeEntryError);
70 }
71
72 let menu_path = base_dir.as_ref().join(format!("{name}.menu"));
73 if menu_path.exists() {
74 return RecipeEntry::from_path(menu_path).map_err(FetchError::RecipeEntryError);
75 }
76 }
77 }
78
79 Err(FetchError::InvalidPath(name.to_path_buf()))
80}
81
82pub fn get_recipe_str(
102 base_dirs: impl IntoIterator<Item = impl AsRef<str>>,
103 name: &str,
104) -> Result<RecipeEntry, FetchError> {
105 let base_dirs: Vec<Utf8PathBuf> = base_dirs
106 .into_iter()
107 .map(|s| Utf8PathBuf::from(s.as_ref()))
108 .collect();
109 let name = Utf8PathBuf::from(name);
110 get_recipe(base_dirs, name)
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use indoc::indoc;
117 use std::fs;
118 use tempfile::TempDir;
119
120 fn create_test_recipe(dir: &Utf8Path, name: &str, content: &str) -> Utf8PathBuf {
121 let path = if name.ends_with(".cook") {
122 dir.join(name)
123 } else {
124 dir.join(format!("{name}.cook"))
125 };
126 fs::write(&path, content).unwrap();
127 path
128 }
129
130 #[test]
131 fn test_get_recipe_found() {
132 let temp_dir = TempDir::new().unwrap();
133 let temp_dir_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
134 create_test_recipe(
135 &temp_dir_path,
136 "pancakes",
137 indoc! {r#"
138 ---
139 servings: 4
140 ---
141
142 Make pancakes"#},
143 );
144
145 let result = get_recipe([&temp_dir_path], &Utf8PathBuf::from("pancakes")).unwrap();
146 assert_eq!(result.name().as_ref().unwrap(), "pancakes");
147 }
148
149 #[test]
150 fn test_get_recipe_not_found() {
151 let temp_dir = TempDir::new().unwrap();
152 let temp_dir_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
153 let result = get_recipe([&temp_dir_path], &Utf8PathBuf::from("nonexistent"));
154 assert!(matches!(result, Err(FetchError::InvalidPath(_))));
155 }
156
157 #[test]
158 fn test_get_recipe_multiple_directories() {
159 let dir1 = TempDir::new().unwrap();
160 let dir2 = TempDir::new().unwrap();
161 let dir1_path = Utf8PathBuf::from_path_buf(dir1.path().to_path_buf()).unwrap();
162 let dir2_path = Utf8PathBuf::from_path_buf(dir2.path().to_path_buf()).unwrap();
163
164 create_test_recipe(
165 &dir2_path,
166 "pancakes",
167 indoc! {r#"
168 ---
169 servings: 4
170 ---
171
172 Make pancakes"#},
173 );
174
175 let result = get_recipe([&dir1_path, &dir2_path], &Utf8PathBuf::from("pancakes")).unwrap();
176 assert_eq!(result.name().as_ref().unwrap(), "pancakes");
177 }
178
179 #[test]
180 fn test_get_recipe_first_directory_priority() {
181 let dir1 = TempDir::new().unwrap();
182 let dir2 = TempDir::new().unwrap();
183 let dir1_path = Utf8PathBuf::from_path_buf(dir1.path().to_path_buf()).unwrap();
184 let dir2_path = Utf8PathBuf::from_path_buf(dir2.path().to_path_buf()).unwrap();
185
186 create_test_recipe(
187 &dir1_path,
188 "pancakes",
189 indoc! {r#"
190 ---
191 servings: 2
192 ---
193
194 Dir1 pancakes"#},
195 );
196 create_test_recipe(
197 &dir2_path,
198 "pancakes",
199 indoc! {r#"
200 ---
201 servings: 4
202 ---
203
204 Dir2 pancakes"#},
205 );
206
207 let result = get_recipe([&dir1_path, &dir2_path], &Utf8PathBuf::from("pancakes")).unwrap();
208 assert_eq!(result.name().as_ref().unwrap(), "pancakes");
209 assert!(result.path().unwrap().starts_with(&dir1_path)); }
211
212 #[test]
213 fn test_get_recipe_invalid_directory() {
214 let result = get_recipe(
215 [Utf8PathBuf::from("/nonexistent/directory")],
216 Utf8PathBuf::from("recipe"),
217 );
218 assert!(matches!(result, Err(FetchError::InvalidPath(_))));
219 }
220
221 #[test]
222 fn test_get_recipe_empty_directories() {
223 let result = get_recipe(
224 std::iter::empty::<Utf8PathBuf>(),
225 Utf8PathBuf::from("recipe"),
226 );
227 assert!(matches!(result, Err(FetchError::InvalidPath(_))));
228 }
229
230 #[test]
231 fn test_get_recipe_with_subdirectories() {
232 let temp_dir = TempDir::new().unwrap();
233 let temp_dir_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
234 let sub_dir = temp_dir_path.join("breakfast");
235 fs::create_dir_all(&sub_dir).unwrap();
236
237 create_test_recipe(
238 &sub_dir,
239 "pancakes",
240 indoc! {r#"
241 ---
242 servings: 4
243 ---
244
245 Make pancakes"#},
246 );
247
248 let result = get_recipe([&temp_dir_path], &Utf8PathBuf::from("pancakes"));
250 assert!(matches!(result, Err(FetchError::InvalidPath(_))));
251
252 let result = get_recipe([&sub_dir], &Utf8PathBuf::from("pancakes")).unwrap();
254 assert_eq!(result.name().as_ref().unwrap(), "pancakes");
255 }
256
257 #[test]
258 fn test_get_recipe_with_existing_extension() {
259 let temp_dir = TempDir::new().unwrap();
260 let temp_dir_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
261 create_test_recipe(
262 &temp_dir_path,
263 "pancakes.cook",
264 indoc! {r#"
265 ---
266 servings: 4
267 ---
268
269 Make pancakes"#},
270 );
271
272 let result = get_recipe([&temp_dir_path], &Utf8PathBuf::from("pancakes.cook")).unwrap();
274 assert_eq!(result.name().as_ref().unwrap(), "pancakes");
275 }
276
277 #[test]
278 fn test_get_recipe_with_menu_extension() {
279 let temp_dir = TempDir::new().unwrap();
280 let temp_dir_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
281
282 let menu_path = temp_dir_path.join("weekly.menu");
284 fs::write(
285 &menu_path,
286 indoc! {r#"
287 ---
288 title: Weekly Menu
289 ---
290
291 Menu content here"#},
292 )
293 .unwrap();
294
295 let result = get_recipe([&temp_dir_path], &Utf8PathBuf::from("weekly.menu")).unwrap();
297 assert_eq!(result.path(), Some(&menu_path));
298 }
299}