Skip to main content

ironflow_api/entities/
run.rs

1//! Run-related DTOs and query parameters.
2
3use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use ironflow_store::models::{Run, RunStatus, TriggerKind};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use super::{CreatedBy, StepResponse};
12
13/// Run response DTO — public API representation of a run.
14///
15/// Maps from the internal [`Run`] model, exposing only necessary fields.
16///
17/// # Examples
18///
19/// ```
20/// use ironflow_store::models::{Run, RunStatus, TriggerKind};
21/// use ironflow_api::entities::RunResponse;
22/// ```
23#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
24#[derive(Debug, Serialize, Deserialize)]
25pub struct RunResponse {
26    /// Unique run identifier.
27    pub id: Uuid,
28    /// Workflow name.
29    pub workflow_name: String,
30    /// Current status.
31    pub status: RunStatus,
32    /// How the run was triggered.
33    pub trigger: TriggerKind,
34    /// Optional error message.
35    pub error: Option<String>,
36    /// Number of times retried.
37    pub retry_count: u32,
38    /// Maximum allowed retries.
39    pub max_retries: u32,
40    /// Aggregated cost in USD.
41    #[cfg_attr(feature = "openapi", schema(value_type = f64))]
42    pub cost_usd: Decimal,
43    /// Total duration in milliseconds.
44    pub duration_ms: u64,
45    /// When created.
46    pub created_at: DateTime<Utc>,
47    /// When last updated.
48    pub updated_at: DateTime<Utc>,
49    /// When execution started.
50    pub started_at: Option<DateTime<Utc>>,
51    /// When execution completed.
52    pub completed_at: Option<DateTime<Utc>>,
53    /// Version of the handler that created this run.
54    pub handler_version: Option<String>,
55    /// User-defined key-value labels.
56    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
57    pub labels: HashMap<String, String>,
58    /// Scheduled execution time. `None` means the run executed immediately.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub scheduled_at: Option<DateTime<Utc>>,
61    /// Who triggered the run. Always present.
62    pub created_by: CreatedBy,
63    /// Idempotency key that produced this run, when one was supplied.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub idempotency_key: Option<String>,
66    /// Exclusivity key held by this run until it reaches a terminal state,
67    /// when one was supplied.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub concurrency_key: Option<String>,
70    /// Cumulative cost cap for this run, in USD. `None` means no cap.
71    #[cfg_attr(feature = "openapi", schema(value_type = Option<f64>))]
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub max_cost_usd: Option<Decimal>,
74}
75
76impl From<Run> for RunResponse {
77    fn from(run: Run) -> Self {
78        let created_by = CreatedBy::from(&run);
79        RunResponse {
80            id: run.id,
81            workflow_name: run.workflow_name,
82            status: run.status.state,
83            trigger: run.trigger,
84            error: run.error,
85            retry_count: run.retry_count,
86            max_retries: run.max_retries,
87            cost_usd: run.cost_usd,
88            duration_ms: run.duration_ms,
89            created_at: run.created_at,
90            updated_at: run.updated_at,
91            started_at: run.started_at,
92            completed_at: run.completed_at,
93            handler_version: run.handler_version,
94            labels: run.labels,
95            scheduled_at: run.scheduled_at,
96            created_by,
97            idempotency_key: run.idempotency_key,
98            concurrency_key: run.concurrency_key,
99            max_cost_usd: run.max_cost_usd,
100        }
101    }
102}
103
104/// Run detail response — includes steps and payload.
105#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
106#[derive(Debug, Serialize)]
107pub struct RunDetailResponse {
108    /// The run.
109    pub run: RunResponse,
110    /// Associated steps, ordered by position.
111    pub steps: Vec<StepResponse>,
112    /// Input payload that triggered this run.
113    pub payload: serde_json::Value,
114}
115
116/// Query parameters for listing runs.
117#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams, utoipa::ToSchema))]
118#[derive(Debug, Deserialize)]
119pub struct ListRunsQuery {
120    /// Filter by workflow name.
121    pub workflow: Option<String>,
122    /// Filter by run status.
123    pub status: Option<RunStatus>,
124    /// Filter by step presence (only applies to completed/cancelled runs).
125    /// Non-terminal runs (pending, running, etc.) are always included.
126    /// When `true`, only return completed/cancelled runs that have steps.
127    /// When `false`, only return completed/cancelled runs without steps.
128    pub has_steps: Option<bool>,
129    /// Filter by labels. Comma-separated `key:value` pairs.
130    pub label: Option<String>,
131    /// Filter by author: the user ID that triggered the run.
132    ///
133    /// Also matches runs triggered by one of that user's API keys.
134    pub created_by: Option<Uuid>,
135    /// Page number (1-based).
136    pub page: Option<u32>,
137    /// Items per page.
138    pub per_page: Option<u32>,
139}
140
141impl ListRunsQuery {
142    /// Parse the comma-separated `label` param into a `HashMap`.
143    pub fn parse_labels(&self) -> Option<HashMap<String, String>> {
144        parse_label_param(&self.label)
145    }
146}
147
148/// Parse a comma-separated `key:value` label query param into a `HashMap`.
149///
150/// Entries without a `:` are ignored. Returns `None` when the param is
151/// absent or contains no valid entry.
152pub(crate) fn parse_label_param(raw: &Option<String>) -> Option<HashMap<String, String>> {
153    raw.as_ref().and_then(|raw| {
154        let mut map = HashMap::new();
155        for entry in raw.split(',') {
156            let entry = entry.trim();
157            if let Some((k, v)) = entry.split_once(':') {
158                map.insert(k.to_string(), v.to_string());
159            }
160        }
161        if map.is_empty() { None } else { Some(map) }
162    })
163}