1use bnto_core::{
8 BntoError, NodeRegistry, PipelineDefinition, PipelineFile, PipelineReporter, PipelineResult,
9 ProcessContext, execute_pipeline,
10};
11
12pub fn create_default_registry() -> NodeRegistry {
18 let mut registry = NodeRegistry::new();
19
20 registry.register(
21 "image-compress",
22 Box::new(bnto_image::CompressImages::new()),
23 );
24 registry.register("image-resize", Box::new(bnto_image::ResizeImages::new()));
25 registry.register(
26 "image-convert",
27 Box::new(bnto_image::ConvertImageFormat::new()),
28 );
29 registry.register("spreadsheet-clean", Box::new(bnto_csv::CleanCsv::new()));
30 registry.register(
31 "spreadsheet-rename",
32 Box::new(bnto_csv::RenameCsvColumns::new()),
33 );
34 registry.register("file-rename", Box::new(bnto_file::RenameFiles::new()));
35 registry.register("image-strip-exif", Box::new(bnto_image::StripExif::new()));
36 registry.register("spreadsheet-convert", Box::new(bnto_csv::CsvToJson::new()));
37 registry.register("spreadsheet-merge", Box::new(bnto_csv::MergeCsv::new()));
38 registry.register("image-overlay", Box::new(bnto_image::OverlayImage::new()));
39
40 registry
41}
42
43pub fn run_pipeline(
49 definition_json: &str,
50 files: Vec<PipelineFile>,
51 reporter: &PipelineReporter,
52 ctx: &dyn ProcessContext,
53) -> Result<PipelineResult, BntoError> {
54 let definition: PipelineDefinition = serde_json::from_str(definition_json)
55 .map_err(|e| BntoError::InvalidInput(format!("Failed to parse definition: {e}")))?;
56
57 let registry = create_default_registry();
58
59 let now_ms = || {
60 std::time::SystemTime::now()
61 .duration_since(std::time::UNIX_EPOCH)
62 .unwrap_or_default()
63 .as_millis() as u64
64 };
65
66 execute_pipeline(&definition, files, ®istry, reporter, ctx, now_ms)
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use bnto_core::NoopContext;
73
74 #[test]
75 fn test_default_registry_has_all_processors() {
76 let registry = create_default_registry();
77 assert_eq!(registry.len(), 10);
78
79 let expected = [
80 "image-compress",
81 "image-resize",
82 "image-convert",
83 "image-strip-exif",
84 "image-overlay",
85 "spreadsheet-clean",
86 "spreadsheet-rename",
87 "spreadsheet-convert",
88 "spreadsheet-merge",
89 "file-rename",
90 ];
91
92 let params = serde_json::Map::new();
93 for node_type in &expected {
94 assert!(
95 registry.resolve(node_type, ¶ms).is_some(),
96 "Missing processor for {node_type}",
97 );
98 }
99 }
100
101 #[test]
102 fn test_run_pipeline_rejects_invalid_json() {
103 let reporter = PipelineReporter::new_noop();
104 let result = run_pipeline("not valid json", vec![], &reporter, &NoopContext);
105 assert!(result.is_err());
106 }
107
108 #[test]
109 fn test_run_pipeline_with_simple_definition() {
110 let json = r#"{
111 "nodes": [
112 { "id": "input", "type": "input" },
113 { "id": "compress", "type": "image-compress", "parameters": { "quality": 80 } },
114 { "id": "output", "type": "output" }
115 ]
116 }"#;
117
118 let test_image = include_bytes!("../../../../test-fixtures/images/small.jpg");
119 let files = vec![PipelineFile {
120 name: "test.jpg".to_string(),
121 data: test_image.to_vec(),
122 mime_type: "image/jpeg".to_string(),
123 metadata: serde_json::Map::new(),
124 }];
125
126 let reporter = PipelineReporter::new_noop();
127 let result =
128 run_pipeline(json, files, &reporter, &NoopContext).expect("Pipeline should succeed");
129
130 assert_eq!(result.files.len(), 1);
131 assert!(!result.files[0].data.is_empty());
132 }
133
134 #[test]
135 fn test_generated_compress_images_recipe() {
136 let json = include_str!(
137 "../../../../packages/@bnto/registry/src/recipes/generated/compress-images.bnto.json"
138 );
139 let test_image = include_bytes!("../../../../test-fixtures/images/small.jpg");
140 let files = vec![PipelineFile {
141 name: "photo.jpg".to_string(),
142 data: test_image.to_vec(),
143 mime_type: "image/jpeg".to_string(),
144 metadata: serde_json::Map::new(),
145 }];
146
147 let reporter = PipelineReporter::new_noop();
148 let result =
149 run_pipeline(json, files, &reporter, &NoopContext).expect("compress-images recipe");
150
151 assert_eq!(result.files.len(), 1);
152 assert!(!result.files[0].data.is_empty());
153 }
154
155 #[test]
156 fn test_generated_resize_images_recipe() {
157 let json = include_str!(
158 "../../../../packages/@bnto/registry/src/recipes/generated/resize-images.bnto.json"
159 );
160 let test_image = include_bytes!("../../../../test-fixtures/images/small.jpg");
161 let files = vec![PipelineFile {
162 name: "photo.jpg".to_string(),
163 data: test_image.to_vec(),
164 mime_type: "image/jpeg".to_string(),
165 metadata: serde_json::Map::new(),
166 }];
167
168 let reporter = PipelineReporter::new_noop();
169 let result =
170 run_pipeline(json, files, &reporter, &NoopContext).expect("resize-images recipe");
171
172 assert_eq!(result.files.len(), 1);
173 }
174
175 #[test]
176 fn test_generated_clean_csv_recipe() {
177 let json = include_str!(
178 "../../../../packages/@bnto/registry/src/recipes/generated/clean-csv.bnto.json"
179 );
180 let csv_data = include_bytes!("../../../../test-fixtures/csv/messy.csv");
181 let files = vec![PipelineFile {
182 name: "data.csv".to_string(),
183 data: csv_data.to_vec(),
184 mime_type: "text/csv".to_string(),
185 metadata: serde_json::Map::new(),
186 }];
187
188 let reporter = PipelineReporter::new_noop();
189 let result = run_pipeline(json, files, &reporter, &NoopContext).expect("clean-csv recipe");
190
191 assert_eq!(result.files.len(), 1);
192 }
193
194 #[test]
195 fn test_generated_rename_files_recipe() {
196 let json = include_str!(
197 "../../../../packages/@bnto/registry/src/recipes/generated/rename-files.bnto.json"
198 );
199 let files = vec![PipelineFile {
200 name: "document.txt".to_string(),
201 data: b"hello world".to_vec(),
202 mime_type: "text/plain".to_string(),
203 metadata: serde_json::Map::new(),
204 }];
205
206 let reporter = PipelineReporter::new_noop();
207 let result =
208 run_pipeline(json, files, &reporter, &NoopContext).expect("rename-files recipe");
209
210 assert_eq!(result.files.len(), 1);
211 assert!(
212 result.files[0].name.starts_with("renamed-"),
213 "Expected 'renamed-' prefix, got: {}",
214 result.files[0].name,
215 );
216 }
217
218 #[test]
219 fn test_generated_csv_to_json_recipe() {
220 let json = include_str!(
221 "../../../../packages/@bnto/registry/src/recipes/generated/csv-to-json.bnto.json"
222 );
223 let csv_data = include_bytes!("../../../../test-fixtures/csv/simple.csv");
224 let files = vec![PipelineFile {
225 name: "data.csv".to_string(),
226 data: csv_data.to_vec(),
227 mime_type: "text/csv".to_string(),
228 metadata: serde_json::Map::new(),
229 }];
230
231 let reporter = PipelineReporter::new_noop();
232 let result =
233 run_pipeline(json, files, &reporter, &NoopContext).expect("csv-to-json recipe");
234
235 assert_eq!(result.files.len(), 1);
236 assert!(result.files[0].name.ends_with(".json"));
237 }
238
239 #[test]
240 fn test_generated_merge_csv_recipe() {
241 let json = include_str!(
242 "../../../../packages/@bnto/registry/src/recipes/generated/merge-csv.bnto.json"
243 );
244 let csv_a = b"name,age\nAlice,30\n";
245 let csv_b = b"name,age\nBob,25\n";
246 let files = vec![
247 PipelineFile {
248 name: "a.csv".to_string(),
249 data: csv_a.to_vec(),
250 mime_type: "text/csv".to_string(),
251 metadata: serde_json::Map::new(),
252 },
253 PipelineFile {
254 name: "b.csv".to_string(),
255 data: csv_b.to_vec(),
256 mime_type: "text/csv".to_string(),
257 metadata: serde_json::Map::new(),
258 },
259 ];
260
261 let reporter = PipelineReporter::new_noop();
262 let result = run_pipeline(json, files, &reporter, &NoopContext).expect("merge-csv recipe");
263
264 assert_eq!(result.files.len(), 1);
265 let output = String::from_utf8_lossy(&result.files[0].data);
266 assert!(output.contains("Alice"));
267 assert!(output.contains("Bob"));
268 }
269
270 #[test]
271 fn test_generated_strip_exif_recipe() {
272 let json = include_str!(
273 "../../../../packages/@bnto/registry/src/recipes/generated/strip-exif.bnto.json"
274 );
275 let test_image = include_bytes!("../../../../test-fixtures/images/small.jpg");
276 let files = vec![PipelineFile {
277 name: "photo.jpg".to_string(),
278 data: test_image.to_vec(),
279 mime_type: "image/jpeg".to_string(),
280 metadata: serde_json::Map::new(),
281 }];
282
283 let reporter = PipelineReporter::new_noop();
284 let result = run_pipeline(json, files, &reporter, &NoopContext).expect("strip-exif recipe");
285
286 assert_eq!(result.files.len(), 1);
287 assert!(!result.files[0].data.is_empty());
288 }
289
290 #[test]
291 fn test_generated_watermark_images_recipe() {
292 let mut def: serde_json::Value = serde_json::from_str(include_str!(
294 "../../../../packages/@bnto/registry/src/recipes/generated/watermark-images.bnto.json"
295 ))
296 .unwrap();
297
298 let overlay_bytes =
300 include_bytes!("../../../../test-fixtures/images/overlays/overlay-logo.png");
301 let overlay_b64 = format!(
302 "data:image/png;base64,{}",
303 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, overlay_bytes)
304 );
305
306 let nodes = def["nodes"].as_array_mut().unwrap();
308 for node in nodes.iter_mut() {
309 if node["type"] == "image-overlay" {
310 node["parameters"]["overlay"] = serde_json::Value::String(overlay_b64.clone());
311 }
312 }
313
314 let json = serde_json::to_string(&def).unwrap();
315 let test_image = include_bytes!("../../../../test-fixtures/images/small.jpg");
316 let files = vec![PipelineFile {
317 name: "photo.jpg".to_string(),
318 data: test_image.to_vec(),
319 mime_type: "image/jpeg".to_string(),
320 metadata: serde_json::Map::new(),
321 }];
322
323 let reporter = PipelineReporter::new_noop();
324 let result =
325 run_pipeline(&json, files, &reporter, &NoopContext).expect("watermark-images recipe");
326
327 assert_eq!(result.files.len(), 1);
328 assert!(!result.files[0].data.is_empty());
329 }
330
331 #[test]
332 fn test_all_generated_recipes_parse() {
333 let recipes = [
334 include_str!(
335 "../../../../packages/@bnto/registry/src/recipes/generated/compress-images.bnto.json"
336 ),
337 include_str!(
338 "../../../../packages/@bnto/registry/src/recipes/generated/resize-images.bnto.json"
339 ),
340 include_str!(
341 "../../../../packages/@bnto/registry/src/recipes/generated/convert-image-format.bnto.json"
342 ),
343 include_str!(
344 "../../../../packages/@bnto/registry/src/recipes/generated/rename-files.bnto.json"
345 ),
346 include_str!(
347 "../../../../packages/@bnto/registry/src/recipes/generated/clean-csv.bnto.json"
348 ),
349 include_str!(
350 "../../../../packages/@bnto/registry/src/recipes/generated/rename-csv-columns.bnto.json"
351 ),
352 include_str!(
353 "../../../../packages/@bnto/registry/src/recipes/generated/optimize-images-for-web.bnto.json"
354 ),
355 include_str!(
356 "../../../../packages/@bnto/registry/src/recipes/generated/generate-thumbnails.bnto.json"
357 ),
358 include_str!(
359 "../../../../packages/@bnto/registry/src/recipes/generated/compress-and-rename.bnto.json"
360 ),
361 include_str!(
362 "../../../../packages/@bnto/registry/src/recipes/generated/standardize-csv.bnto.json"
363 ),
364 include_str!(
365 "../../../../packages/@bnto/registry/src/recipes/generated/csv-to-json.bnto.json"
366 ),
367 include_str!(
368 "../../../../packages/@bnto/registry/src/recipes/generated/strip-exif.bnto.json"
369 ),
370 include_str!(
371 "../../../../packages/@bnto/registry/src/recipes/generated/watermark-images.bnto.json"
372 ),
373 include_str!(
374 "../../../../packages/@bnto/registry/src/recipes/generated/merge-csv.bnto.json"
375 ),
376 ];
377
378 for (i, json) in recipes.iter().enumerate() {
379 let def: PipelineDefinition = serde_json::from_str(json)
380 .unwrap_or_else(|e| panic!("Recipe {i} failed to parse: {e}"));
381 assert!(
382 !def.nodes.is_empty(),
383 "Recipe {i} should have at least one node",
384 );
385 }
386 }
387}