Skip to main content

bnto_engine/
recipes.rs

1//! Built-in recipe catalog — engine-owned recipe definitions.
2//!
3//! Each recipe is a `.bnto.json` embedded at compile time via `include_str!()`.
4//! The engine is the source of truth for recipes. TypeScript catalogs
5//! are generated from the engine's catalog snapshot.
6
7/// A built-in recipe with metadata extracted from the definition JSON.
8pub 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
16/// All built-in recipe definition strings, embedded at compile time.
17const 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
35/// Returns all built-in recipes, embedded at compile time.
36pub 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
59/// Find a built-in recipe by slug.
60pub 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}