1mod auto_iteration;
16mod container;
17pub mod loop_config;
18mod primitive;
19pub mod resolve;
20pub mod template;
21
22use crate::context::ProcessContext;
23use crate::errors::BntoError;
24use crate::events::{NodeInfo, PipelineEvent, PipelineReporter};
25use crate::pipeline::{
26 IterationMode, PipelineDefinition, PipelineFile, PipelineFileResult, PipelineNode,
27 PipelineResult, is_container_node, is_io_node,
28};
29use crate::registry::NodeRegistry;
30
31#[cfg(test)]
32use crate::processor::NodeInput;
33#[cfg(test)]
34use crate::progress::ProgressReporter;
35
36type PipelineNodeRef<'a> = &'a PipelineNode;
39
40struct PipelineContext<'a, F: Fn() -> u64 + Copy> {
43 registry: &'a NodeRegistry,
44 reporter: &'a PipelineReporter,
45 process_ctx: &'a dyn ProcessContext,
47 pipeline_total_files: usize,
50 now_ms: F,
52 loop_item: Option<serde_json::Map<String, serde_json::Value>>,
55 parent_node_id: Option<String>,
58 node_outputs: std::collections::BTreeMap<String, serde_json::Value>,
61}
62
63#[derive(Clone, Copy)]
68struct ChainPosition {
69 first_index: usize,
71 total_nodes: usize,
73}
74
75impl ChainPosition {
76 fn whole(total_nodes: usize) -> Self {
78 Self::starting_at(0, total_nodes)
79 }
80
81 fn starting_at(first_index: usize, total_nodes: usize) -> Self {
83 Self {
84 first_index,
85 total_nodes,
86 }
87 }
88}
89
90struct NodeExecutionResult {
92 output_files: Vec<PipelineFile>,
93 files_processed: usize,
94 warnings: Vec<String>,
96}
97
98fn filter_processing_nodes(definition: &PipelineDefinition) -> Vec<&PipelineNode> {
102 definition
103 .nodes
104 .iter()
105 .filter(|n| !is_io_node(&n.node_type))
106 .collect()
107}
108
109fn build_pipeline_result(
111 files: Vec<PipelineFile>,
112 duration_ms: u64,
113 warnings: Vec<String>,
114) -> PipelineResult {
115 let result_files = files
116 .into_iter()
117 .map(|f| PipelineFileResult {
118 name: f.name,
119 data: f.data,
120 mime_type: f.mime_type,
121 metadata: f.metadata,
122 })
123 .collect();
124 PipelineResult {
125 files: result_files,
126 duration_ms,
127 warnings,
128 }
129}
130
131pub fn execute_pipeline(
136 definition: &PipelineDefinition,
137 files: Vec<PipelineFile>,
138 registry: &NodeRegistry,
139 reporter: &PipelineReporter,
140 process_ctx: &dyn ProcessContext,
141 now_ms: impl Fn() -> u64 + Copy,
142) -> Result<PipelineResult, BntoError> {
143 let start_ms = now_ms();
144 let processing_nodes = filter_processing_nodes(definition);
145 let ctx = build_root_context(definition, &files, registry, reporter, process_ctx, now_ms);
146 emit_pipeline_started(&ctx, &processing_nodes, files.len());
147 let result = run_processing_nodes(&ctx, definition, &processing_nodes, files)?;
148 Ok(finish_pipeline(&ctx, start_ms, result))
149}
150
151fn build_root_context<'a, F: Fn() -> u64 + Copy>(
153 definition: &PipelineDefinition,
154 files: &[PipelineFile],
155 registry: &'a NodeRegistry,
156 reporter: &'a PipelineReporter,
157 process_ctx: &'a dyn ProcessContext,
158 now_ms: F,
159) -> PipelineContext<'a, F> {
160 PipelineContext {
161 registry,
162 reporter,
163 process_ctx,
164 pipeline_total_files: files.len(),
165 now_ms,
166 loop_item: None,
167 parent_node_id: None,
168 node_outputs: template::build_node_outputs_for_input(&definition.nodes, files),
169 }
170}
171
172fn emit_pipeline_started<F: Fn() -> u64 + Copy>(
173 ctx: &PipelineContext<F>,
174 processing_nodes: &[&PipelineNode],
175 total_files: usize,
176) {
177 let nodes = processing_nodes
178 .iter()
179 .map(|n| NodeInfo {
180 id: n.id.clone(),
181 node_type: n.node_type.clone(),
182 })
183 .collect();
184 ctx.reporter.emit(PipelineEvent::PipelineStarted {
185 total_nodes: processing_nodes.len(),
186 total_files,
187 nodes,
188 });
189}
190
191fn run_processing_nodes<F: Fn() -> u64 + Copy>(
193 ctx: &PipelineContext<F>,
194 definition: &PipelineDefinition,
195 processing_nodes: &[&PipelineNode],
196 files: Vec<PipelineFile>,
197) -> Result<NodeExecutionResult, BntoError> {
198 match definition.resolved_iteration() {
199 IterationMode::Explicit => {
200 let position = ChainPosition::whole(processing_nodes.len());
201 run_node_chain(ctx, processing_nodes, files, 0, position)
202 }
203 IterationMode::Auto => auto_iteration::run_auto_iteration(ctx, processing_nodes, files),
204 }
205}
206
207fn finish_pipeline<F: Fn() -> u64 + Copy>(
209 ctx: &PipelineContext<F>,
210 start_ms: u64,
211 result: NodeExecutionResult,
212) -> PipelineResult {
213 let duration_ms = (ctx.now_ms)() - start_ms;
214 ctx.reporter.emit(PipelineEvent::PipelineCompleted {
215 duration_ms,
216 total_files_processed: result.files_processed,
217 });
218 build_pipeline_result(result.output_files, duration_ms, result.warnings)
219}
220
221fn run_node_chain<F: Fn() -> u64 + Copy>(
226 ctx: &PipelineContext<F>,
227 nodes: &[&PipelineNode],
228 files: Vec<PipelineFile>,
229 file_offset: usize,
230 position: ChainPosition,
231) -> Result<NodeExecutionResult, BntoError> {
232 let mut current_files = files;
233 let mut total_files_processed: usize = 0;
234 let mut all_warnings: Vec<String> = Vec::new();
235
236 for (i, node) in nodes.iter().enumerate() {
237 let node_index = position.first_index + i;
238 let result = execute_node(
239 ctx,
240 node,
241 current_files,
242 node_index,
243 position.total_nodes,
244 file_offset,
245 )?;
246 total_files_processed += result.files_processed;
247 all_warnings.extend(result.warnings);
248 current_files = result.output_files;
249 }
250
251 Ok(NodeExecutionResult {
252 output_files: current_files,
253 files_processed: total_files_processed,
254 warnings: all_warnings,
255 })
256}
257
258fn emit_node_failure<F: Fn() -> u64 + Copy>(
262 ctx: &PipelineContext<F>,
263 node_id: &str,
264 error: BntoError,
265) -> BntoError {
266 let error_msg = error.to_string();
267 ctx.reporter.emit(PipelineEvent::NodeFailed {
268 node_id: node_id.to_string(),
269 error: error_msg.clone(),
270 parent_node_id: ctx.parent_node_id.clone(),
271 });
272 ctx.reporter.emit(PipelineEvent::PipelineFailed {
273 node_id: node_id.to_string(),
274 error: error_msg,
275 });
276 error
277}
278
279fn execute_node<F: Fn() -> u64 + Copy>(
285 ctx: &PipelineContext<F>,
286 node: &PipelineNode,
287 files: Vec<PipelineFile>,
288 node_index: usize,
289 total_nodes: usize,
290 file_offset: usize,
291) -> Result<NodeExecutionResult, BntoError> {
292 let node_start = (ctx.now_ms)();
293 emit_node_started(ctx, node, node_index, total_nodes);
294
295 let result = dispatch_node(ctx, node, files, file_offset);
296
297 match result {
298 Ok(exec_result) => {
299 emit_node_completed(ctx, &node.id, node_start, exec_result.files_processed);
300 Ok(exec_result)
301 }
302 Err(error) => Err(emit_node_failure(ctx, &node.id, error)),
303 }
304}
305
306fn emit_node_started<F: Fn() -> u64 + Copy>(
307 ctx: &PipelineContext<F>,
308 node: &PipelineNode,
309 node_index: usize,
310 total_nodes: usize,
311) {
312 ctx.reporter.emit(PipelineEvent::NodeStarted {
313 node_id: node.id.clone(),
314 node_index,
315 total_nodes,
316 node_type: node.node_type.clone(),
317 parent_node_id: ctx.parent_node_id.clone(),
318 });
319}
320
321fn emit_node_completed<F: Fn() -> u64 + Copy>(
322 ctx: &PipelineContext<F>,
323 node_id: &str,
324 node_start: u64,
325 files_processed: usize,
326) {
327 let duration_ms = (ctx.now_ms)() - node_start;
328 ctx.reporter.emit(PipelineEvent::NodeCompleted {
329 node_id: node_id.to_string(),
330 duration_ms,
331 files_processed,
332 parent_node_id: ctx.parent_node_id.clone(),
333 });
334}
335
336fn dispatch_node<F: Fn() -> u64 + Copy>(
337 ctx: &PipelineContext<F>,
338 node: &PipelineNode,
339 files: Vec<PipelineFile>,
340 file_offset: usize,
341) -> Result<NodeExecutionResult, BntoError> {
342 if is_container_node(&node.node_type) {
343 container::execute_container_node(ctx, node, files, file_offset)
344 } else {
345 primitive::execute_primitive_node(ctx, node, files, file_offset)
346 }
347}
348
349#[cfg(test)]
350mod tests;