ironflow_cli/commands/
run.rs1use std::fs;
4use std::io::Write as _;
5use std::path::PathBuf;
6use std::slice;
7
8use anyhow::{Context, Result};
9use clap::{Args, Subcommand};
10use ironflow_sdk::IronflowClient;
11use ironflow_sdk::client::ListRunsFilter;
12use ironflow_sdk::types::CreateRunRequest;
13use uuid::Uuid;
14
15use crate::output;
16
17#[derive(Debug, Args)]
19pub struct RunArgs {
20 #[command(subcommand)]
22 pub command: RunCommands,
23}
24
25#[derive(Debug, Subcommand)]
27pub enum RunCommands {
28 Create {
30 workflow: String,
32 #[arg(long, group = "payload_source")]
34 payload: Option<String>,
35 #[arg(long, group = "payload_source")]
37 payload_file: Option<PathBuf>,
38 },
39 List {
41 #[arg(long)]
43 status: Option<String>,
44 #[arg(long)]
46 workflow: Option<String>,
47 #[arg(long)]
49 page: Option<u32>,
50 #[arg(long)]
52 per_page: Option<u32>,
53 },
54 Get {
56 id: Uuid,
58 },
59 Cancel {
61 id: Uuid,
63 },
64 Approve {
66 id: Uuid,
68 },
69 Retry {
71 id: Uuid,
73 },
74}
75
76fn resolve_payload(
78 payload: Option<&str>,
79 payload_file: Option<&PathBuf>,
80) -> Result<serde_json::Value> {
81 match (payload, payload_file) {
82 (Some(raw), _) => serde_json::from_str(raw).context("invalid JSON in --payload"),
83 (_, Some(path)) => {
84 let content = fs::read_to_string(path)
85 .with_context(|| format!("cannot read payload file: {}", path.display()))?;
86 serde_json::from_str(&content)
87 .with_context(|| format!("invalid JSON in {}", path.display()))
88 }
89 (None, None) => Ok(serde_json::Value::Object(serde_json::Map::new())),
90 }
91}
92
93pub async fn execute(
99 client: &IronflowClient,
100 args: &RunArgs,
101 json_mode: bool,
102 _verbose: bool,
103) -> Result<()> {
104 match &args.command {
105 RunCommands::Create {
106 workflow,
107 payload,
108 payload_file,
109 } => {
110 let payload_value = resolve_payload(payload.as_deref(), payload_file.as_ref())?;
111 let payload_map = payload_value
112 .as_object()
113 .context("payload must be a JSON object")?
114 .clone();
115 let request: CreateRunRequest = CreateRunRequest::builder()
116 .workflow(workflow.clone())
117 .payload(Some(payload_map))
118 .try_into()
119 .context("failed to build CreateRunRequest")?;
120
121 let response = client.create_run(&request).await?;
122 output::print_output(json_mode, &response, || {
123 output::runs_table(slice::from_ref(&response.data))
124 })?;
125 }
126 RunCommands::List {
127 status,
128 workflow,
129 page,
130 per_page,
131 } => {
132 let filter = ListRunsFilter {
133 status: status.as_deref(),
134 workflow: workflow.as_deref(),
135 page: *page,
136 per_page: *per_page,
137 ..Default::default()
138 };
139 let response = client.list_runs_filtered(&filter).await?;
140 output::print_output(json_mode, &response, || output::runs_table(&response.data))?;
141 }
142 RunCommands::Get { id } => {
143 let response = client.get_run(*id).await?;
144 output::print_output(json_mode, &response, || {
145 output::run_detail_table(&response.data)
146 })?;
147
148 if !json_mode && !response.data.steps.is_empty() {
149 let mut out = std::io::stdout().lock();
150 writeln!(out)?;
151 writeln!(out, "Steps:")?;
152 writeln!(out, "{}", output::steps_table(&response.data.steps))?;
153 }
154 }
155 RunCommands::Cancel { id } => {
156 let response = client.cancel_run(*id).await?;
157 output::print_output(json_mode, &response, || {
158 output::runs_table(slice::from_ref(&response.data))
159 })?;
160 }
161 RunCommands::Approve { id } => {
162 let response = client.approve_run(*id).await?;
163 output::print_output(json_mode, &response, || {
164 output::runs_table(slice::from_ref(&response.data))
165 })?;
166 }
167 RunCommands::Retry { id } => {
168 let response = client.retry_run(*id).await?;
169 output::print_output(json_mode, &response, || {
170 output::runs_table(slice::from_ref(&response.data))
171 })?;
172 }
173 }
174 Ok(())
175}
176
177#[cfg(test)]
178mod tests {
179 use std::io::Write;
180
181 use tempfile::NamedTempFile;
182
183 use super::*;
184
185 #[test]
186 fn resolve_payload_none_returns_empty_object() {
187 let value = resolve_payload(None, None).unwrap();
188 assert!(value.is_object());
189 assert!(value.as_object().unwrap().is_empty());
190 }
191
192 #[test]
193 fn resolve_payload_inline_valid_json() {
194 let value = resolve_payload(Some(r#"{"key": "value"}"#), None).unwrap();
195 assert_eq!(value["key"], "value");
196 }
197
198 #[test]
199 fn resolve_payload_inline_invalid_json() {
200 let result = resolve_payload(Some("not json"), None);
201 assert!(result.is_err());
202 assert!(result.unwrap_err().to_string().contains("invalid JSON"));
203 }
204
205 #[test]
206 fn resolve_payload_file_valid() {
207 let mut tmp = NamedTempFile::new().unwrap();
208 write!(tmp, r#"{{"workflow": "test"}}"#).unwrap();
209 let path = tmp.path().to_path_buf();
210
211 let value = resolve_payload(None, Some(&path)).unwrap();
212 assert_eq!(value["workflow"], "test");
213 }
214
215 #[test]
216 fn resolve_payload_file_not_found() {
217 let path = PathBuf::from("/nonexistent/payload.json");
218 let result = resolve_payload(None, Some(&path));
219 assert!(result.is_err());
220 assert!(result.unwrap_err().to_string().contains("cannot read"));
221 }
222
223 #[test]
224 fn resolve_payload_file_invalid_json() {
225 let mut tmp = NamedTempFile::new().unwrap();
226 write!(tmp, "not valid json").unwrap();
227 let path = tmp.path().to_path_buf();
228
229 let result = resolve_payload(None, Some(&path));
230 assert!(result.is_err());
231 assert!(result.unwrap_err().to_string().contains("invalid JSON"));
232 }
233}