use std::collections::HashMap;
use chrono::{DateTime, Utc};
use ironflow_store::models::{ConcurrencyLimit, ProviderKind, Run, RunStatus, TriggerKind};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use super::{CreatedBy, StepResponse};
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Serialize, Deserialize)]
pub struct RunResponse {
pub id: Uuid,
pub workflow_name: String,
pub status: RunStatus,
pub trigger: TriggerKind,
pub error: Option<String>,
pub retry_count: u32,
pub max_retries: u32,
pub lease_recoveries: u32,
#[cfg_attr(feature = "openapi", schema(value_type = f64))]
pub cost_usd: Decimal,
pub duration_ms: u64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub handler_version: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub labels: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scheduled_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
pub capacity_wait_kind: Option<ProviderKind>,
pub created_by: CreatedBy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub concurrency_key: Option<String>,
#[serde(default)]
pub priority: i16,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub concurrency_limits: Vec<ConcurrencyLimit>,
#[cfg_attr(feature = "openapi", schema(value_type = Option<f64>))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_cost_usd: Option<Decimal>,
#[serde(default)]
pub worker_tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resume_status: Option<RunStatus>,
}
impl From<Run> for RunResponse {
fn from(run: Run) -> Self {
let created_by = CreatedBy::from(&run);
RunResponse {
id: run.id,
workflow_name: run.workflow_name,
status: run.status.state,
trigger: run.trigger,
error: run.error,
retry_count: run.retry_count,
max_retries: run.max_retries,
lease_recoveries: run.lease_recoveries,
cost_usd: run.cost_usd,
duration_ms: run.duration_ms,
created_at: run.created_at,
updated_at: run.updated_at,
started_at: run.started_at,
completed_at: run.completed_at,
handler_version: run.handler_version,
labels: run.labels,
scheduled_at: run.scheduled_at,
capacity_wait_kind: run.capacity_wait_kind,
created_by,
idempotency_key: run.idempotency_key,
concurrency_key: run.concurrency_key,
priority: run.priority,
concurrency_limits: run.concurrency_limits,
max_cost_usd: run.max_cost_usd,
worker_tags: run.worker_tags,
output: run.output,
resume_status: run.resume_status,
}
}
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Serialize)]
pub struct RunDetailResponse {
pub run: RunResponse,
pub steps: Vec<StepResponse>,
pub payload: serde_json::Value,
pub active_descendant_count: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker_routing: Option<WorkerRouting>,
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerRouting {
pub seen_workers: u32,
pub eligible_workers: u32,
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Serialize)]
pub struct CancelRunResponse {
#[serde(flatten)]
pub run: RunResponse,
pub cancelled_descendants: Vec<Uuid>,
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Serialize)]
pub struct PauseRunResponse {
#[serde(flatten)]
pub run: RunResponse,
pub paused_descendants: Vec<Uuid>,
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Serialize)]
pub struct ResumeRunResponse {
#[serde(flatten)]
pub run: RunResponse,
pub resumed_descendants: Vec<Uuid>,
}
#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams, utoipa::ToSchema))]
#[derive(Debug, Deserialize)]
pub struct ListRunsQuery {
pub workflow: Option<String>,
pub status: Option<RunStatus>,
pub has_steps: Option<bool>,
pub label: Option<String>,
pub created_by: Option<Uuid>,
pub concurrency_group: Option<String>,
pub priority: Option<i16>,
pub page: Option<u32>,
pub per_page: Option<u32>,
}
impl ListRunsQuery {
pub fn parse_labels(&self) -> Option<HashMap<String, String>> {
parse_label_param(&self.label)
}
}
pub(crate) fn parse_label_param(raw: &Option<String>) -> Option<HashMap<String, String>> {
raw.as_ref().and_then(|raw| {
let mut map = HashMap::new();
for entry in raw.split(',') {
let entry = entry.trim();
if let Some((k, v)) = entry.split_once(':') {
map.insert(k.to_string(), v.to_string());
}
}
if map.is_empty() { None } else { Some(map) }
})
}
#[cfg(test)]
mod tests {
use ironflow_store::memory::InMemoryStore;
use ironflow_store::models::NewRun;
use ironflow_store::store::RunStore;
use serde_json::{json, to_value};
use super::*;
async fn run_with_lease_recoveries(lease_recoveries: u32) -> Run {
let store = InMemoryStore::new();
let mut run = store
.create_run(NewRun {
created_by: None,
workflow_name: "deploy".to_string(),
trigger: TriggerKind::Manual,
payload: json!({}),
max_retries: 1,
handler_version: None,
labels: HashMap::new(),
scheduled_at: None,
idempotency_key: None,
concurrency_key: None,
priority: 0,
concurrency_limits: Vec::new(),
max_cost_usd: None,
worker_tags: Vec::new(),
})
.await
.expect("create run")
.into_run();
run.lease_recoveries = lease_recoveries;
run
}
#[tokio::test]
async fn run_response_exposes_the_lease_recoveries() {
let response = RunResponse::from(run_with_lease_recoveries(2).await);
assert_eq!(response.lease_recoveries, 2);
let value = to_value(&response).expect("serialize");
assert_eq!(value["lease_recoveries"], 2);
}
#[tokio::test]
async fn run_response_reports_zero_lease_recoveries_for_a_fresh_run() {
let response = RunResponse::from(run_with_lease_recoveries(0).await);
assert_eq!(response.lease_recoveries, 0);
let value = to_value(&response).expect("serialize");
assert_eq!(value["lease_recoveries"], 0);
}
}