backbone_bucket/application/workflows/
media_processing_workflow.rs1use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11use chrono;
12use backbone_core::flow::{WorkflowStep, WorkflowContext};
13
14#[derive(Debug, Clone, thiserror::Error)]
16pub enum FlowError {
17 #[error("No current step to execute")]
18 NoCurrentStep,
19
20 #[error("Step execution failed: {0}")]
21 StepFailed(String),
22
23 #[error("Condition evaluation failed: {0}")]
24 ConditionFailed(String),
25
26 #[error("Compensation failed: {0}")]
27 CompensationFailed(String),
28
29 #[error("Flow timed out")]
30 Timeout,
31
32 #[error("Flow cancelled")]
33 Cancelled,
34
35 #[error("Invalid state transition: {from} -> {to}")]
36 InvalidTransition { from: String, to: String },
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum MediaProcessingFlowStatus {
43 Pending,
45 Running,
47 Waiting,
49 Completed,
51 Failed,
53 Cancelled,
55 Compensating,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum MediaProcessingFlowStep {
63 CheckMediaType,
65 CreateImageThumbnail,
67 CreateVideoThumbnail,
69 CreateDocumentPreview,
71 NoProcessingNeeded,
73 Complete,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct MediaProcessingFlowInstance {
80 pub id: String,
82 pub status: MediaProcessingFlowStatus,
84 pub current_step: Option<MediaProcessingFlowStep>,
86 pub context: serde_json::Value,
88 pub completed_steps: Vec<MediaProcessingFlowStep>,
90 pub error: Option<String>,
92 pub created_at: chrono::DateTime<chrono::Utc>,
94 pub updated_at: chrono::DateTime<chrono::Utc>,
96}
97
98impl MediaProcessingFlowInstance {
99 pub fn new(id: impl Into<String>) -> Self {
101 let now = chrono::Utc::now();
102 Self {
103 id: id.into(),
104 status: MediaProcessingFlowStatus::Pending,
105 current_step: None,
106 context: serde_json::json!({}),
107 completed_steps: Vec::new(),
108 error: None,
109 created_at: now,
110 updated_at: now,
111 }
112 }
113
114 pub fn is_complete(&self) -> bool {
116 matches!(
117 self.status,
118 MediaProcessingFlowStatus::Completed | MediaProcessingFlowStatus::Failed | MediaProcessingFlowStatus::Cancelled
119 )
120 }
121
122 pub fn is_running(&self) -> bool {
124 matches!(
125 self.status,
126 MediaProcessingFlowStatus::Running | MediaProcessingFlowStatus::Waiting
127 )
128 }
129
130 pub fn set_context(&mut self, key: &str, value: serde_json::Value) {
132 if let serde_json::Value::Object(ref mut map) = self.context {
133 map.insert(key.to_string(), value);
134 }
135 self.updated_at = chrono::Utc::now();
136 }
137
138 pub fn get_context(&self, key: &str) -> Option<&serde_json::Value> {
140 if let serde_json::Value::Object(ref map) = self.context {
141 map.get(key)
142 } else {
143 None
144 }
145 }
146}
147
148#[allow(unused_variables)]
152impl WorkflowContext<MediaProcessingFlowInstance> for MediaProcessingFlowInstance {
153 fn entity(&self) -> &MediaProcessingFlowInstance { self }
154 fn set_var(&mut self, key: &str, value: serde_json::Value) {
155 self.set_context(key, value);
156 }
157 fn get_var(&self, key: &str) -> Option<&serde_json::Value> {
158 self.get_context(key)
159 }
160}
161
162#[async_trait::async_trait]
164pub trait MediaProcessingStepHandler: Send + Sync {
165 async fn evaluate_check_media_type(
167 &self,
168 instance: &MediaProcessingFlowInstance,
169 ) -> Result<MediaProcessingFlowStep, FlowError>;
170
171 async fn handle_create_image_thumbnail(
173 &self,
174 instance: &mut MediaProcessingFlowInstance,
175 ) -> Result<Option<MediaProcessingFlowStep>, FlowError>;
176
177 async fn handle_create_video_thumbnail(
179 &self,
180 instance: &mut MediaProcessingFlowInstance,
181 ) -> Result<Option<MediaProcessingFlowStep>, FlowError>;
182
183 async fn handle_create_document_preview(
185 &self,
186 instance: &mut MediaProcessingFlowInstance,
187 ) -> Result<Option<MediaProcessingFlowStep>, FlowError>;
188
189 async fn handle_no_processing_needed(
191 &self,
192 instance: &mut MediaProcessingFlowInstance,
193 ) -> Result<(), FlowError>;
194
195 async fn handle_complete(
197 &self,
198 instance: &mut MediaProcessingFlowInstance,
199 ) -> Result<(), FlowError>;
200
201}
202
203pub struct MediaProcessingFlowExecutor<H: MediaProcessingStepHandler> {
205 handler: Arc<H>,
206}
207
208impl<H: MediaProcessingStepHandler> MediaProcessingFlowExecutor<H> {
209 pub fn new(handler: Arc<H>) -> Self {
211 Self { handler }
212 }
213
214 pub async fn start(&self, instance_id: impl Into<String>) -> Result<MediaProcessingFlowInstance, FlowError> {
216 let mut instance = MediaProcessingFlowInstance::new(instance_id);
217 instance.status = MediaProcessingFlowStatus::Running;
218 instance.current_step = Some(MediaProcessingFlowStep::CheckMediaType);
219 Ok(instance)
220 }
221
222 pub async fn execute_step(&self, instance: &mut MediaProcessingFlowInstance) -> Result<(), FlowError> {
224 let current_step = match instance.current_step {
225 Some(step) => step,
226 None => return Err(FlowError::NoCurrentStep),
227 };
228
229 let next_step = match current_step {
230 MediaProcessingFlowStep::CheckMediaType => {
231 let next = self.handler.evaluate_check_media_type(instance).await?;
232 Some(next)
233 }
234 MediaProcessingFlowStep::CreateImageThumbnail => {
235 self.handler.handle_create_image_thumbnail(instance).await?
236 }
237 MediaProcessingFlowStep::CreateVideoThumbnail => {
238 self.handler.handle_create_video_thumbnail(instance).await?
239 }
240 MediaProcessingFlowStep::CreateDocumentPreview => {
241 self.handler.handle_create_document_preview(instance).await?
242 }
243 MediaProcessingFlowStep::NoProcessingNeeded => {
244 self.handler.handle_no_processing_needed(instance).await?;
245 None }
247 MediaProcessingFlowStep::Complete => {
248 self.handler.handle_complete(instance).await?;
249 None }
251 };
252
253 instance.completed_steps.push(current_step);
255
256 match next_step {
258 Some(next) => {
259 instance.current_step = Some(next);
260 }
261 None => {
262 instance.current_step = None;
263 instance.status = MediaProcessingFlowStatus::Completed;
264 }
265 }
266
267 instance.updated_at = chrono::Utc::now();
268 Ok(())
269 }
270
271 pub async fn run(&self, instance: &mut MediaProcessingFlowInstance) -> Result<(), FlowError> {
273 while !instance.is_complete() && instance.status != MediaProcessingFlowStatus::Waiting {
274 self.execute_step(instance).await?;
275 }
276 Ok(())
277 }
278
279 pub fn cancel(&self, instance: &mut MediaProcessingFlowInstance) {
281 instance.status = MediaProcessingFlowStatus::Cancelled;
282 instance.updated_at = chrono::Utc::now();
283 }
284
285 pub fn fail(&self, instance: &mut MediaProcessingFlowInstance, error: impl Into<String>) {
287 instance.status = MediaProcessingFlowStatus::Failed;
288 instance.error = Some(error.into());
289 instance.updated_at = chrono::Utc::now();
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_flow_instance_creation() {
299 let instance = MediaProcessingFlowInstance::new("test-1");
300 assert_eq!(instance.id, "test-1");
301 assert_eq!(instance.status, MediaProcessingFlowStatus::Pending);
302 assert!(!instance.is_complete());
303 }
304
305 #[test]
306 fn test_flow_context() {
307 let mut instance = MediaProcessingFlowInstance::new("test-2");
308 instance.set_context("key", serde_json::json!("value"));
309 let value = instance.get_context("key");
310 assert_eq!(value, Some(&serde_json::json!("value")));
311 }
312
313 #[test]
314 fn test_flow_status_transitions() {
315 let mut instance = MediaProcessingFlowInstance::new("test-3");
316 assert!(!instance.is_running());
317
318 instance.status = MediaProcessingFlowStatus::Running;
319 assert!(instance.is_running());
320 assert!(!instance.is_complete());
321
322 instance.status = MediaProcessingFlowStatus::Completed;
323 assert!(instance.is_complete());
324 assert!(!instance.is_running());
325 }
326}