1use std::collections::HashMap;
48use std::future::Future;
49use std::pin::Pin;
50
51use schemars::JsonSchema;
52use serde::Serialize;
53use serde_json::Value;
54
55use crate::context::WorkflowContext;
56use crate::error::EngineError;
57
58pub fn input_schema_for<T: JsonSchema>() -> Value {
81 let schema = schemars::schema_for!(T);
82 serde_json::to_value(schema).expect("schema serialization cannot fail")
83}
84
85pub type HandlerFuture<'a> = Pin<Box<dyn Future<Output = Result<(), EngineError>> + Send + 'a>>;
87
88#[derive(Debug, Clone, Serialize)]
93pub struct WorkflowInfo {
94 pub description: String,
96 pub source_code: Option<String>,
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
100 pub sub_workflows: Vec<String>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub category: Option<String>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub version: Option<String>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub input_schema: Option<Value>,
116 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
118 pub default_labels: HashMap<String, String>,
119}
120
121pub trait WorkflowHandler: Send + Sync {
133 fn name(&self) -> &str;
135
136 fn version(&self) -> Option<&str> {
141 None
142 }
143
144 fn category(&self) -> Option<&str> {
153 None
154 }
155
156 fn input_schema(&self) -> Option<Value> {
162 None
163 }
164
165 fn default_labels(&self) -> HashMap<String, String> {
170 HashMap::new()
171 }
172
173 fn describe(&self) -> WorkflowInfo {
181 WorkflowInfo {
182 description: String::new(),
183 source_code: None,
184 sub_workflows: Vec::new(),
185 category: self.category().map(str::to_string),
186 version: self.version().map(str::to_string),
187 input_schema: self.input_schema(),
188 default_labels: self.default_labels(),
189 }
190 }
191
192 fn execute<'a>(&'a self, ctx: &'a mut WorkflowContext) -> HandlerFuture<'a>;
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use serde::{Deserialize, Serialize};
209
210 #[derive(Debug, Serialize, Deserialize, JsonSchema)]
211 struct TestInput {
212 environment: String,
213 #[serde(default)]
214 dry_run: bool,
215 }
216
217 struct MinimalHandler;
218
219 impl WorkflowHandler for MinimalHandler {
220 fn name(&self) -> &str {
221 "minimal"
222 }
223
224 fn execute<'a>(&'a self, _ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
225 Box::pin(async { Ok(()) })
226 }
227 }
228
229 struct FullFeaturedHandler;
230
231 impl WorkflowHandler for FullFeaturedHandler {
232 fn name(&self) -> &str {
233 "full"
234 }
235
236 fn version(&self) -> Option<&str> {
237 Some("1.2.0")
238 }
239
240 fn category(&self) -> Option<&str> {
241 Some("data/etl")
242 }
243
244 fn input_schema(&self) -> Option<Value> {
245 Some(input_schema_for::<TestInput>())
246 }
247
248 fn default_labels(&self) -> HashMap<String, String> {
249 HashMap::from([
250 ("team".to_string(), "platform".to_string()),
251 ("env".to_string(), "prod".to_string()),
252 ])
253 }
254
255 fn describe(&self) -> WorkflowInfo {
256 WorkflowInfo {
257 description: "Full-featured test handler".to_string(),
258 source_code: Some("fn test() {}".to_string()),
259 sub_workflows: vec!["helper".to_string()],
260 category: self.category().map(str::to_string),
261 version: self.version().map(str::to_string),
262 input_schema: self.input_schema(),
263 default_labels: self.default_labels(),
264 }
265 }
266
267 fn execute<'a>(&'a self, _ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
268 Box::pin(async { Ok(()) })
269 }
270 }
271
272 #[test]
273 fn minimal_handler_has_required_name() {
274 let handler = MinimalHandler;
275 assert_eq!(handler.name(), "minimal");
276 }
277
278 #[test]
279 fn minimal_handler_defaults_to_no_version() {
280 let handler = MinimalHandler;
281 assert_eq!(handler.version(), None);
282 }
283
284 #[test]
285 fn minimal_handler_defaults_to_no_category() {
286 let handler = MinimalHandler;
287 assert_eq!(handler.category(), None);
288 }
289
290 #[test]
291 fn minimal_handler_defaults_to_no_schema() {
292 let handler = MinimalHandler;
293 assert_eq!(handler.input_schema(), None);
294 }
295
296 #[test]
297 fn minimal_handler_defaults_to_empty_labels() {
298 let handler = MinimalHandler;
299 let labels = handler.default_labels();
300 assert!(labels.is_empty());
301 }
302
303 #[test]
304 fn minimal_handler_describe_reflects_defaults() {
305 let handler = MinimalHandler;
306 let info = handler.describe();
307 assert_eq!(info.description, "");
308 assert_eq!(info.source_code, None);
309 assert_eq!(info.sub_workflows, Vec::<String>::new());
310 assert_eq!(info.category, None);
311 assert_eq!(info.version, None);
312 assert_eq!(info.input_schema, None);
313 assert!(info.default_labels.is_empty());
314 }
315
316 #[test]
317 fn full_handler_returns_all_metadata() {
318 let handler = FullFeaturedHandler;
319 assert_eq!(handler.name(), "full");
320 assert_eq!(handler.version(), Some("1.2.0"));
321 assert_eq!(handler.category(), Some("data/etl"));
322 assert!(handler.input_schema().is_some());
323 }
324
325 #[test]
326 fn full_handler_default_labels_are_set() {
327 let handler = FullFeaturedHandler;
328 let labels = handler.default_labels();
329 assert_eq!(labels.get("team"), Some(&"platform".to_string()));
330 assert_eq!(labels.get("env"), Some(&"prod".to_string()));
331 }
332
333 #[test]
334 fn full_handler_describe_includes_all_fields() {
335 let handler = FullFeaturedHandler;
336 let info = handler.describe();
337 assert_eq!(info.description, "Full-featured test handler");
338 assert_eq!(info.source_code, Some("fn test() {}".to_string()));
339 assert_eq!(info.sub_workflows, vec!["helper".to_string()]);
340 assert_eq!(info.category, Some("data/etl".to_string()));
341 assert_eq!(info.version, Some("1.2.0".to_string()));
342 assert!(info.input_schema.is_some());
343 assert_eq!(info.default_labels.len(), 2);
344 }
345
346 #[test]
347 fn input_schema_for_generates_json_schema() {
348 let schema = input_schema_for::<TestInput>();
349 assert_eq!(schema["type"], "object");
350 assert!(schema["properties"]["environment"].is_object());
351 assert!(schema["properties"]["dry_run"].is_object());
352 }
353
354 #[test]
355 fn input_schema_for_preserves_serde_attributes() {
356 let schema = input_schema_for::<TestInput>();
357 let properties = &schema["properties"];
358 assert!(properties.is_object());
359 assert!(properties.get("environment").is_some());
360 assert!(properties.get("dry_run").is_some());
361 }
362
363 #[test]
364 fn workflow_info_serializes_with_skip_empty() {
365 let info = WorkflowInfo {
366 description: "test".to_string(),
367 source_code: None,
368 sub_workflows: Vec::new(),
369 category: None,
370 version: None,
371 input_schema: None,
372 default_labels: HashMap::new(),
373 };
374
375 let json = serde_json::to_value(&info).expect("serialize");
376 assert_eq!(json["description"], "test");
377 assert!(json.is_object());
380 }
381
382 #[test]
383 fn workflow_info_serializes_with_values() {
384 let info = WorkflowInfo {
385 description: "test".to_string(),
386 source_code: Some("code".to_string()),
387 sub_workflows: vec!["sub".to_string()],
388 category: Some("cat".to_string()),
389 version: Some("1.0.0".to_string()),
390 input_schema: Some(serde_json::json!({"type": "object"})),
391 default_labels: HashMap::from([("key".to_string(), "value".to_string())]),
392 };
393
394 let json = serde_json::to_value(&info).expect("serialize");
395 assert_eq!(json["description"], "test");
396 assert_eq!(json["source_code"], "code");
397 assert_eq!(json["sub_workflows"][0], "sub");
398 assert_eq!(json["category"], "cat");
399 assert_eq!(json["version"], "1.0.0");
400 assert_eq!(json["default_labels"]["key"], "value");
401 }
402}