//! Execution DTOs for API requests and responses use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use utoipa::{IntoParams, ToSchema}; use attune_common::models::enums::ExecutionStatus; use attune_common::models::execution::WorkflowTaskMetadata; use attune_common::repositories::execution::ExecutionWithRefs; /// Request DTO for creating a manual execution #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct CreateExecutionRequest { /// Action reference to execute #[schema(example = "slack.post_message")] pub action_ref: String, /// Execution parameters/configuration #[schema(value_type = Object, example = json!({"channel": "#alerts", "message": "Manual test"}))] pub parameters: Option, /// Environment variables for this execution #[schema(value_type = Object, example = json!({"DEBUG": "true", "LOG_LEVEL": "info"}))] pub env_vars: Option, } /// Response DTO for execution information #[derive(Debug, Clone, Serialize, ToSchema)] pub struct ExecutionResponse { /// Execution ID #[schema(example = 1)] pub id: i64, /// Action ID (optional, may be null for ad-hoc executions) #[schema(example = 1)] pub action: Option, /// Action reference #[schema(example = "slack.post_message")] pub action_ref: String, /// Execution configuration/parameters #[schema(value_type = Object, example = json!({"channel": "#alerts", "message": "System error detected"}))] pub config: Option, /// Parent execution ID (for nested/child executions) #[schema(example = 1)] pub parent: Option, /// Enforcement ID (rule enforcement that triggered this) #[schema(example = 1)] pub enforcement: Option, /// Identity ID that initiated this execution #[schema(example = 1)] pub executor: Option, /// Worker ID currently assigned to this execution #[schema(example = 1)] pub worker: Option, /// Execution status #[schema(example = "succeeded")] pub status: ExecutionStatus, /// Execution result/output #[schema(value_type = Object, example = json!({"message_id": "1234567890.123456"}))] pub result: Option, /// When the execution actually started running (worker picked it up). /// Null if the execution hasn't started running yet. #[serde(skip_serializing_if = "Option::is_none")] #[schema(example = "2024-01-13T10:31:00Z", nullable = true)] pub started_at: Option>, /// Workflow task metadata (only populated for workflow task executions) #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = Option, nullable = true)] pub workflow_task: Option, /// Creation timestamp #[schema(example = "2024-01-13T10:30:00Z")] pub created: DateTime, /// Last update timestamp #[schema(example = "2024-01-13T10:35:00Z")] pub updated: DateTime, } /// Simplified execution response (for list endpoints) #[derive(Debug, Clone, Serialize, ToSchema)] pub struct ExecutionSummary { /// Execution ID #[schema(example = 1)] pub id: i64, /// Action reference #[schema(example = "slack.post_message")] pub action_ref: String, /// Execution status #[schema(example = "succeeded")] pub status: ExecutionStatus, /// Parent execution ID #[schema(example = 1)] pub parent: Option, /// Enforcement ID #[schema(example = 1)] pub enforcement: Option, /// Rule reference (if triggered by a rule) #[schema(example = "core.on_timer")] pub rule_ref: Option, /// Trigger reference (if triggered by a trigger) #[schema(example = "core.timer")] pub trigger_ref: Option, /// When the execution actually started running (worker picked it up). /// Null if the execution hasn't started running yet. #[serde(skip_serializing_if = "Option::is_none")] #[schema(example = "2024-01-13T10:31:00Z", nullable = true)] pub started_at: Option>, /// Workflow task metadata (only populated for workflow task executions) #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = Option, nullable = true)] pub workflow_task: Option, /// Creation timestamp #[schema(example = "2024-01-13T10:30:00Z")] pub created: DateTime, /// Last update timestamp #[schema(example = "2024-01-13T10:35:00Z")] pub updated: DateTime, } /// Query parameters for filtering executions #[derive(Debug, Clone, Deserialize, IntoParams)] pub struct ExecutionQueryParams { /// Filter by execution status #[param(example = "succeeded")] pub status: Option, /// Filter by action reference #[param(example = "slack.post_message")] pub action_ref: Option, /// Filter by pack name #[param(example = "core")] pub pack_name: Option, /// Filter by rule reference #[param(example = "core.on_timer")] pub rule_ref: Option, /// Filter by trigger reference #[param(example = "core.timer")] pub trigger_ref: Option, /// Filter by executor ID #[param(example = 1)] pub executor: Option, /// Search in result JSON (case-insensitive substring match) #[param(example = "error")] pub result_contains: Option, /// Filter by enforcement ID #[param(example = 1)] pub enforcement: Option, /// Filter by parent execution ID #[param(example = 1)] pub parent: Option, /// If true, only return top-level executions (those without a parent). /// Useful for the "By Workflow" view where child tasks are loaded separately. #[serde(default)] #[param(example = false)] pub top_level_only: Option, /// Page number (for pagination) #[serde(default = "default_page")] #[param(example = 1, minimum = 1)] pub page: u32, /// Items per page (for pagination) #[serde(default = "default_per_page")] #[param(example = 50, minimum = 1, maximum = 100)] pub per_page: u32, } impl ExecutionQueryParams { /// Get the SQL offset value pub fn offset(&self) -> u32 { (self.page.saturating_sub(1)) * self.per_page } /// Get the limit value (with max cap) pub fn limit(&self) -> u32 { self.per_page.min(100) } } /// Convert from Execution model to ExecutionResponse impl From for ExecutionResponse { fn from(execution: attune_common::models::execution::Execution) -> Self { Self { id: execution.id, action: execution.action, action_ref: execution.action_ref, config: execution .config .map(|c| serde_json::to_value(c).unwrap_or(JsonValue::Null)), parent: execution.parent, enforcement: execution.enforcement, executor: execution.executor, worker: execution.worker, status: execution.status, result: execution .result .map(|r| serde_json::to_value(r).unwrap_or(JsonValue::Null)), started_at: execution.started_at, workflow_task: execution.workflow_task, created: execution.created, updated: execution.updated, } } } /// Convert from Execution model to ExecutionSummary impl From for ExecutionSummary { fn from(execution: attune_common::models::execution::Execution) -> Self { Self { id: execution.id, action_ref: execution.action_ref, status: execution.status, parent: execution.parent, enforcement: execution.enforcement, rule_ref: None, // Populated separately via enforcement lookup trigger_ref: None, // Populated separately via enforcement lookup started_at: execution.started_at, workflow_task: execution.workflow_task, created: execution.created, updated: execution.updated, } } } /// Convert from the joined query result (execution + enforcement refs). /// `rule_ref` and `trigger_ref` are already populated from the SQL JOIN. impl From for ExecutionSummary { fn from(row: ExecutionWithRefs) -> Self { Self { id: row.id, action_ref: row.action_ref, status: row.status, parent: row.parent, enforcement: row.enforcement, rule_ref: row.rule_ref, trigger_ref: row.trigger_ref, started_at: row.started_at, workflow_task: row.workflow_task, created: row.created, updated: row.updated, } } } fn default_page() -> u32 { 1 } fn default_per_page() -> u32 { 20 } #[cfg(test)] mod tests { use super::*; #[test] fn test_query_params_defaults() { let json = r#"{}"#; let params: ExecutionQueryParams = serde_json::from_str(json).unwrap(); assert_eq!(params.page, 1); assert_eq!(params.per_page, 20); assert!(params.status.is_none()); } #[test] fn test_query_params_with_filters() { let json = r#"{ "status": "completed", "action_ref": "test.action", "page": 2, "per_page": 50 }"#; let params: ExecutionQueryParams = serde_json::from_str(json).unwrap(); assert_eq!(params.page, 2); assert_eq!(params.per_page, 50); assert_eq!(params.status, Some(ExecutionStatus::Completed)); assert_eq!(params.action_ref, Some("test.action".to_string())); } #[test] fn test_query_params_offset() { let params = ExecutionQueryParams { status: None, action_ref: None, enforcement: None, parent: None, top_level_only: None, pack_name: None, rule_ref: None, trigger_ref: None, executor: None, result_contains: None, page: 3, per_page: 20, }; assert_eq!(params.offset(), 40); // (3-1) * 20 } #[test] fn test_query_params_limit_cap() { let params = ExecutionQueryParams { status: None, action_ref: None, enforcement: None, parent: None, top_level_only: None, pack_name: None, rule_ref: None, trigger_ref: None, executor: None, result_contains: None, page: 1, per_page: 200, // Exceeds max }; assert_eq!(params.limit(), 100); // Capped at 100 } }