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 #[cfg_attr(feature = "openapi", schema(value_type = f64))]
43 pub cost_usd: Decimal,
44 pub duration_ms: u64,
46 pub created_at: DateTime<Utc>,
48 pub updated_at: DateTime<Utc>,
50 pub started_at: Option<DateTime<Utc>>,
52 pub completed_at: Option<DateTime<Utc>>,
54 pub handler_version: Option<String>,
56 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
58 pub labels: HashMap<String, String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub scheduled_at: Option<DateTime<Utc>>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
66 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
67 pub capacity_wait_kind: Option<ProviderKind>,
68 pub created_by: CreatedBy,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub idempotency_key: Option<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub concurrency_key: Option<String>,
77 #[serde(default, skip_serializing_if = "Vec::is_empty")]
80 pub concurrency_limits: Vec<ConcurrencyLimit>,
81 #[cfg_attr(feature = "openapi", schema(value_type = Option<f64>))]
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub max_cost_usd: Option<Decimal>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub output: Option<Value>,
91}
92
93impl From<Run> for RunResponse {
94 fn from(run: Run) -> Self {
95 let created_by = CreatedBy::from(&run);
96 RunResponse {
97 id: run.id,
98 workflow_name: run.workflow_name,
99 status: run.status.state,
100 trigger: run.trigger,
101 error: run.error,
102 retry_count: run.retry_count,
103 max_retries: run.max_retries,
104 cost_usd: run.cost_usd,
105 duration_ms: run.duration_ms,
106 created_at: run.created_at,
107 updated_at: run.updated_at,
108 started_at: run.started_at,
109 completed_at: run.completed_at,
110 handler_version: run.handler_version,
111 labels: run.labels,
112 scheduled_at: run.scheduled_at,
113 capacity_wait_kind: run.capacity_wait_kind,
114 created_by,
115 idempotency_key: run.idempotency_key,
116 concurrency_key: run.concurrency_key,
117 concurrency_limits: run.concurrency_limits,
118 max_cost_usd: run.max_cost_usd,
119 output: run.output,
120 }
121 }
122}
123
124#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
126#[derive(Debug, Serialize)]
127pub struct RunDetailResponse {
128 pub run: RunResponse,
130 pub steps: Vec<StepResponse>,
132 pub payload: serde_json::Value,
134 pub active_descendant_count: u64,
137}
138
139#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
145#[derive(Debug, Serialize)]
146pub struct CancelRunResponse {
147 #[serde(flatten)]
149 pub run: RunResponse,
150 pub cancelled_descendants: Vec<Uuid>,
153}
154
155#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams, utoipa::ToSchema))]
157#[derive(Debug, Deserialize)]
158pub struct ListRunsQuery {
159 pub workflow: Option<String>,
161 pub status: Option<RunStatus>,
163 pub has_steps: Option<bool>,
168 pub label: Option<String>,
170 pub created_by: Option<Uuid>,
174 pub concurrency_group: Option<String>,
176 pub page: Option<u32>,
178 pub per_page: Option<u32>,
180}
181
182impl ListRunsQuery {
183 pub fn parse_labels(&self) -> Option<HashMap<String, String>> {
185 parse_label_param(&self.label)
186 }
187}
188
189pub(crate) fn parse_label_param(raw: &Option<String>) -> Option<HashMap<String, String>> {
194 raw.as_ref().and_then(|raw| {
195 let mut map = HashMap::new();
196 for entry in raw.split(',') {
197 let entry = entry.trim();
198 if let Some((k, v)) = entry.split_once(':') {
199 map.insert(k.to_string(), v.to_string());
200 }
201 }
202 if map.is_empty() { None } else { Some(map) }
203 })
204}