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