1use chrono::{DateTime, Utc};
4use ironflow_store::models::{Step, StepKind, StepStatus};
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use uuid::Uuid;
9
10use super::ArtifactResponse;
11
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
21#[derive(Debug, Serialize, Deserialize)]
22pub struct StepResponse {
23 pub id: Uuid,
25 pub trace_id: Uuid,
27 pub run_id: Uuid,
29 pub name: String,
31 #[cfg_attr(feature = "openapi", schema(value_type = String))]
33 pub kind: StepKind,
34 pub position: u32,
36 pub status: StepStatus,
38 pub attempt: u32,
43 #[cfg_attr(feature = "openapi", schema(value_type = Option<std::collections::HashMap<String, serde_json::Value>>))]
45 pub input: Option<Value>,
46 #[cfg_attr(feature = "openapi", schema(value_type = Option<std::collections::HashMap<String, serde_json::Value>>))]
48 pub output: Option<Value>,
49 pub error: Option<String>,
51 pub duration_ms: u64,
53 #[cfg_attr(feature = "openapi", schema(value_type = f64))]
55 pub cost_usd: Decimal,
56 pub input_tokens: Option<u64>,
58 pub output_tokens: Option<u64>,
60 pub created_at: DateTime<Utc>,
62 pub updated_at: DateTime<Utc>,
64 pub started_at: Option<DateTime<Utc>>,
66 pub completed_at: Option<DateTime<Utc>>,
68 pub dependencies: Vec<Uuid>,
70 #[cfg_attr(feature = "openapi", schema(value_type = Option<serde_json::Value>))]
74 pub debug_messages: Option<Value>,
75 #[serde(default)]
80 pub artifacts: Vec<ArtifactResponse>,
81}
82
83impl StepResponse {
84 pub fn with_dependencies(step: Step, dependencies: Vec<Uuid>) -> Self {
90 Self::with_dependencies_and_artifacts(step, dependencies, Vec::new())
91 }
92
93 pub fn with_dependencies_and_artifacts(
95 step: Step,
96 dependencies: Vec<Uuid>,
97 artifacts: Vec<ArtifactResponse>,
98 ) -> Self {
99 StepResponse {
100 id: step.id,
101 trace_id: step.trace_id,
102 run_id: step.run_id,
103 name: step.name,
104 kind: step.kind,
105 position: step.position,
106 status: step.status.state,
107 attempt: step.attempt,
108 input: step.input,
109 output: step.output,
110 error: step.error,
111 duration_ms: step.duration_ms,
112 cost_usd: step.cost_usd,
113 input_tokens: step.input_tokens,
114 output_tokens: step.output_tokens,
115 created_at: step.created_at,
116 updated_at: step.updated_at,
117 started_at: step.started_at,
118 completed_at: step.completed_at,
119 dependencies,
120 debug_messages: step.debug_messages,
121 artifacts,
122 }
123 }
124}
125
126impl From<Step> for StepResponse {
127 fn from(step: Step) -> Self {
128 Self::with_dependencies(step, Vec::new())
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use std::collections::HashMap;
135
136 use ironflow_store::memory::InMemoryStore;
137 use ironflow_store::models::{NewRun, NewStep, TriggerKind, step_trace_id};
138 use ironflow_store::store::RunStore;
139 use serde_json::json;
140
141 use super::*;
142
143 async fn step() -> Step {
146 let store = InMemoryStore::new();
147 let run = store
148 .create_run(NewRun {
149 created_by: None,
150 workflow_name: "test".to_string(),
151 trigger: TriggerKind::Manual,
152 payload: json!({}),
153 max_retries: 0,
154 handler_version: None,
155 labels: HashMap::new(),
156 scheduled_at: None,
157 idempotency_key: None,
158 max_cost_usd: None,
159 })
160 .await
161 .expect("create run")
162 .into_run();
163
164 store
165 .create_step(NewStep {
166 run_id: run.id,
167 trace_id: step_trace_id(run.id, "build", 0),
168 name: "build".to_string(),
169 kind: StepKind::Shell,
170 position: 0,
171 input: None,
172 is_error_handler: false,
173 })
174 .await
175 .expect("create step")
176 }
177
178 #[tokio::test]
179 async fn a_step_without_artifacts_exposes_an_empty_list() {
180 let response = StepResponse::from(step().await);
181 assert!(response.artifacts.is_empty());
182 }
183
184 #[tokio::test]
185 async fn artifacts_are_carried_through() {
186 let step = step().await;
187 let artifact = ArtifactResponse {
188 id: Uuid::now_v7(),
189 step_id: step.id,
190 name: "report.html".to_string(),
191 content_type: "text/html".to_string(),
192 size_bytes: 1,
193 sha256: "0".repeat(64),
194 created_at: Utc::now(),
195 };
196
197 let response =
198 StepResponse::with_dependencies_and_artifacts(step, Vec::new(), vec![artifact]);
199
200 assert_eq!(response.artifacts.len(), 1);
201 assert_eq!(response.artifacts[0].name, "report.html");
202 }
203
204 #[tokio::test]
205 async fn artifacts_serialize_as_a_json_array() {
206 let body = serde_json::to_value(StepResponse::from(step().await)).expect("serialize");
207 assert!(body["artifacts"].is_array());
208 }
209
210 #[tokio::test]
211 async fn trace_id_is_exposed_in_step_response() {
212 let s = step().await;
213 let expected_trace_id = s.trace_id;
214 let response = StepResponse::from(s);
215
216 assert_eq!(response.trace_id, expected_trace_id);
217
218 let body = serde_json::to_value(&response).expect("serialize");
219 assert!(body["trace_id"].is_string());
220 }
221}