1pub struct BuiltinRecipe {
9 pub slug: String,
10 pub name: String,
11 pub description: String,
12 pub category: String,
13 pub definition_json: &'static str,
14}
15
16const RECIPE_DEFINITIONS: &[&str] = &[
18 include_str!("../recipes/compress-images.bnto.json"),
19 include_str!("../recipes/resize-images.bnto.json"),
20 include_str!("../recipes/convert-image-format.bnto.json"),
21 include_str!("../recipes/rename-files.bnto.json"),
22 include_str!("../recipes/clean-csv.bnto.json"),
23 include_str!("../recipes/rename-csv-columns.bnto.json"),
24 include_str!("../recipes/csv-to-json.bnto.json"),
25 include_str!("../recipes/merge-csv.bnto.json"),
26 include_str!("../recipes/optimize-images-for-web.bnto.json"),
27 include_str!("../recipes/generate-thumbnails.bnto.json"),
28 include_str!("../recipes/compress-and-rename.bnto.json"),
29 include_str!("../recipes/standardize-csv.bnto.json"),
30 include_str!("../recipes/strip-exif.bnto.json"),
31 include_str!("../recipes/watermark-images.bnto.json"),
32 include_str!("../recipes/download-video.bnto.json"),
33];
34
35pub fn builtin_recipes() -> Vec<BuiltinRecipe> {
37 RECIPE_DEFINITIONS
38 .iter()
39 .map(|json| {
40 let val: serde_json::Value =
41 serde_json::from_str(json).expect("built-in recipe JSON must be valid");
42 BuiltinRecipe {
43 slug: val["id"].as_str().unwrap_or_default().to_string(),
44 name: val["name"].as_str().unwrap_or_default().to_string(),
45 description: val["metadata"]["description"]
46 .as_str()
47 .unwrap_or_default()
48 .to_string(),
49 category: val["metadata"]["category"]
50 .as_str()
51 .unwrap_or_default()
52 .to_string(),
53 definition_json: json,
54 }
55 })
56 .collect()
57}
58
59pub fn builtin_recipe_by_slug(slug: &str) -> Option<BuiltinRecipe> {
61 builtin_recipes().into_iter().find(|r| r.slug == slug)
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use bnto_core::PipelineDefinition;
68
69 #[test]
70 fn test_builtin_recipes_count() {
71 assert_eq!(builtin_recipes().len(), 15);
72 }
73
74 #[test]
75 fn test_builtin_recipes_all_have_slugs() {
76 for recipe in builtin_recipes() {
77 assert!(!recipe.slug.is_empty(), "Recipe has empty slug");
78 }
79 }
80
81 #[test]
82 fn test_builtin_recipes_all_have_categories() {
83 for recipe in builtin_recipes() {
84 assert!(
85 !recipe.category.is_empty(),
86 "Recipe '{}' has empty category",
87 recipe.slug,
88 );
89 }
90 }
91
92 #[test]
93 fn test_builtin_recipes_all_parse_as_pipelines() {
94 for recipe in builtin_recipes() {
95 let _def: PipelineDefinition = serde_json::from_str(recipe.definition_json)
96 .unwrap_or_else(|e| panic!("Recipe '{}' failed to parse: {e}", recipe.slug));
97 }
98 }
99
100 #[test]
101 fn test_builtin_recipe_by_slug() {
102 let recipe =
103 builtin_recipe_by_slug("compress-images").expect("compress-images should exist");
104 assert_eq!(recipe.name, "Compress Images");
105 assert_eq!(recipe.category, "image");
106 }
107
108 #[test]
109 fn test_builtin_recipe_by_slug_not_found() {
110 assert!(builtin_recipe_by_slug("nonexistent").is_none());
111 }
112
113 #[test]
114 fn test_builtin_recipes_unique_slugs() {
115 let recipes = builtin_recipes();
116 let mut slugs: Vec<&str> = recipes.iter().map(|r| r.slug.as_str()).collect();
117 slugs.sort();
118 slugs.dedup();
119 assert_eq!(slugs.len(), recipes.len(), "Duplicate slugs found");
120 }
121
122 #[test]
123 fn test_builtin_recipes_expected_categories() {
124 let recipes = builtin_recipes();
125 let mut categories: Vec<&str> = recipes.iter().map(|r| r.category.as_str()).collect();
126 categories.sort();
127 categories.dedup();
128 assert_eq!(categories, ["file", "image", "spreadsheet", "video"]);
129 }
130}