1use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use ironflow_store::models::{ConcurrencyLimit, ProviderKind, Run, RunStatus, TriggerKind};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use uuid::Uuid;
11
12use super::{CreatedBy, StepResponse};
13
14#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
25#[derive(Debug, Serialize, Deserialize)]
26pub struct RunResponse {
27 pub id: Uuid,
29 pub workflow_name: String,
31 pub status: RunStatus,
33 pub trigger: TriggerKind,
35 pub error: Option<String>,
37 pub retry_count: u32,
39 pub max_retries: u32,
41 pub lease_recoveries: u32,
43 #[cfg_attr(feature = "openapi", schema(value_type = f64))]
45 pub cost_usd: Decimal,
46 pub duration_ms: u64,
48 pub created_at: DateTime<Utc>,
50 pub updated_at: DateTime<Utc>,
52 pub started_at: Option<DateTime<Utc>>,
54 pub completed_at: Option<DateTime<Utc>>,
56 pub handler_version: Option<String>,
58 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
60 pub labels: HashMap<String, String>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub scheduled_at: Option<DateTime<Utc>>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
68 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
69 pub capacity_wait_kind: Option<ProviderKind>,
70 pub created_by: CreatedBy,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub idempotency_key: Option<String>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub concurrency_key: Option<String>,
79 #[serde(default)]
83 pub priority: i16,
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
87 pub concurrency_limits: Vec<ConcurrencyLimit>,
88 #[cfg_attr(feature = "openapi", schema(value_type = Option<f64>))]
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub max_cost_usd: Option<Decimal>,
92 #[serde(default)]
95 pub worker_tags: Vec<String>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub output: Option<Value>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub resume_status: Option<RunStatus>,
106}
107
108impl From<Run> for RunResponse {
109 fn from(run: Run) -> Self {
110 let created_by = CreatedBy::from(&run);
111 RunResponse {
112 id: run.id,
113 workflow_name: run.workflow_name,
114 status: run.status.state,
115 trigger: run.trigger,
116 error: run.error,
117 retry_count: run.retry_count,
118 max_retries: run.max_retries,
119 lease_recoveries: run.lease_recoveries,
120 cost_usd: run.cost_usd,
121 duration_ms: run.duration_ms,
122 created_at: run.created_at,
123 updated_at: run.updated_at,
124 started_at: run.started_at,
125 completed_at: run.completed_at,
126 handler_version: run.handler_version,
127 labels: run.labels,
128 scheduled_at: run.scheduled_at,
129 capacity_wait_kind: run.capacity_wait_kind,
130 created_by,
131 idempotency_key: run.idempotency_key,
132 concurrency_key: run.concurrency_key,
133 priority: run.priority,
134 concurrency_limits: run.concurrency_limits,
135 max_cost_usd: run.max_cost_usd,
136 worker_tags: run.worker_tags,
137 output: run.output,
138 resume_status: run.resume_status,
139 }
140 }
141}
142
143#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
145#[derive(Debug, Serialize)]
146pub struct RunDetailResponse {
147 pub run: RunResponse,
149 pub steps: Vec<StepResponse>,
151 pub payload: serde_json::Value,
153 pub active_descendant_count: u64,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub worker_routing: Option<WorkerRouting>,
161}
162
163#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182pub struct WorkerRouting {
183 pub seen_workers: u32,
185 pub eligible_workers: u32,
188}
189
190#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
196#[derive(Debug, Serialize)]
197pub struct CancelRunResponse {
198 #[serde(flatten)]
200 pub run: RunResponse,
201 pub cancelled_descendants: Vec<Uuid>,
204}
205
206#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
209#[derive(Debug, Serialize)]
210pub struct PauseRunResponse {
211 #[serde(flatten)]
213 pub run: RunResponse,
214 pub paused_descendants: Vec<Uuid>,
216}
217
218#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
221#[derive(Debug, Serialize)]
222pub struct ResumeRunResponse {
223 #[serde(flatten)]
225 pub run: RunResponse,
226 pub resumed_descendants: Vec<Uuid>,
228}
229
230#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams, utoipa::ToSchema))]
232#[derive(Debug, Deserialize)]
233pub struct ListRunsQuery {
234 pub workflow: Option<String>,
236 pub status: Option<RunStatus>,
238 pub has_steps: Option<bool>,
243 pub label: Option<String>,
245 pub created_by: Option<Uuid>,
249 pub concurrency_group: Option<String>,
251 pub priority: Option<i16>,
253 pub page: Option<u32>,
255 pub per_page: Option<u32>,
257}
258
259impl ListRunsQuery {
260 pub fn parse_labels(&self) -> Option<HashMap<String, String>> {
262 parse_label_param(&self.label)
263 }
264}
265
266pub(crate) fn parse_label_param(raw: &Option<String>) -> Option<HashMap<String, String>> {
271 raw.as_ref().and_then(|raw| {
272 let mut map = HashMap::new();
273 for entry in raw.split(',') {
274 let entry = entry.trim();
275 if let Some((k, v)) = entry.split_once(':') {
276 map.insert(k.to_string(), v.to_string());
277 }
278 }
279 if map.is_empty() { None } else { Some(map) }
280 })
281}
282
283#[cfg(test)]
284mod tests {
285 use ironflow_store::memory::InMemoryStore;
286 use ironflow_store::models::NewRun;
287 use ironflow_store::store::RunStore;
288 use serde_json::{json, to_value};
289
290 use super::*;
291
292 async fn run_with_lease_recoveries(lease_recoveries: u32) -> Run {
293 let store = InMemoryStore::new();
294 let mut run = store
295 .create_run(NewRun {
296 created_by: None,
297 workflow_name: "deploy".to_string(),
298 trigger: TriggerKind::Manual,
299 payload: json!({}),
300 max_retries: 1,
301 handler_version: None,
302 labels: HashMap::new(),
303 scheduled_at: None,
304 idempotency_key: None,
305 concurrency_key: None,
306 priority: 0,
307 concurrency_limits: Vec::new(),
308 max_cost_usd: None,
309 worker_tags: Vec::new(),
310 })
311 .await
312 .expect("create run")
313 .into_run();
314 run.lease_recoveries = lease_recoveries;
315 run
316 }
317
318 #[tokio::test]
319 async fn run_response_exposes_the_lease_recoveries() {
320 let response = RunResponse::from(run_with_lease_recoveries(2).await);
321
322 assert_eq!(response.lease_recoveries, 2);
323 let value = to_value(&response).expect("serialize");
324 assert_eq!(value["lease_recoveries"], 2);
325 }
326
327 #[tokio::test]
328 async fn run_response_reports_zero_lease_recoveries_for_a_fresh_run() {
329 let response = RunResponse::from(run_with_lease_recoveries(0).await);
330
331 assert_eq!(response.lease_recoveries, 0);
332 let value = to_value(&response).expect("serialize");
333 assert_eq!(value["lease_recoveries"], 0);
334 }
335}