ironflow_cli/commands/
signal.rs1use std::fs::read_to_string;
4
5use anyhow::{Context, Result, bail};
6use clap::{Args, Subcommand};
7use comfy_table::{ContentArrangement, Table};
8use ironflow_sdk::IronflowClient;
9use ironflow_sdk::client::ListSignalsFilter;
10use ironflow_sdk::types::{SendSignalRequest, SignalDeliveryResponse, SignalResponse};
11use serde_json::{Value, from_str, from_value, json};
12
13use crate::output;
14
15#[derive(Debug, Args)]
17pub struct SignalArgs {
18 #[command(subcommand)]
20 pub command: SignalCommands,
21}
22
23#[derive(Debug, Subcommand)]
25pub enum SignalCommands {
26 Send {
28 name: String,
30 #[arg(long)]
32 key: String,
33 #[arg(long)]
35 payload: Option<String>,
36 #[arg(long)]
38 idempotency_id: Option<String>,
39 },
40 List {
42 #[arg(long)]
44 name: Option<String>,
45 #[arg(long)]
47 key: Option<String>,
48 #[arg(long)]
50 page: Option<u32>,
51 #[arg(long)]
53 per_page: Option<u32>,
54 },
55}
56
57fn read_payload_file(path: &str) -> Result<String> {
59 read_to_string(path).with_context(|| format!("cannot read payload file '{path}'"))
60}
61
62fn parse_payload(raw: Option<&str>) -> Result<Value> {
65 let Some(raw) = raw else {
66 return Ok(json!({}));
67 };
68 let text = match raw.strip_prefix('@') {
69 Some(path) => read_payload_file(path)?,
70 None => raw.to_string(),
71 };
72 let payload: Value = from_str(&text).context("--payload must be valid JSON")?;
73 if !payload.is_object() {
74 bail!("--payload must be a JSON object");
75 }
76 Ok(payload)
77}
78
79fn delivery_table(delivery: &SignalDeliveryResponse) -> Table {
80 let mut table = Table::new();
81 table.set_content_arrangement(ContentArrangement::Dynamic);
82 table.set_header(vec!["SIGNAL ID", "DUPLICATE", "RESUMED", "REJECTED"]);
83 table.add_row(vec![
84 delivery.signal_id.to_string(),
85 delivery.duplicate.to_string(),
86 delivery.resumed.len().to_string(),
87 delivery.rejected.len().to_string(),
88 ]);
89 table
90}
91
92fn signals_table(items: &[SignalResponse]) -> Table {
93 let mut table = Table::new();
94 table.set_content_arrangement(ContentArrangement::Dynamic);
95 table.set_header(vec!["ID", "NAME", "KEY", "RECEIVED AT"]);
96 for s in items {
97 table.add_row(vec![
98 s.id.to_string(),
99 s.name.clone(),
100 s.key.clone(),
101 s.received_at.to_string(),
102 ]);
103 }
104 table
105}
106
107pub async fn execute(client: &IronflowClient, args: &SignalArgs, json_mode: bool) -> Result<()> {
113 match &args.command {
114 SignalCommands::Send {
115 name,
116 key,
117 payload,
118 idempotency_id,
119 } => {
120 let payload = parse_payload(payload.as_deref())?;
121 let request: SendSignalRequest = from_value(json!({
122 "name": name,
123 "key": key,
124 "payload": payload,
125 "idempotency_id": idempotency_id,
126 }))
127 .context("cannot build the signal request")?;
128 let response = client.send_signal(&request).await?;
129 output::print_output(json_mode, &response, || delivery_table(&response.data))
130 }
131 SignalCommands::List {
132 name,
133 key,
134 page,
135 per_page,
136 } => {
137 let filter = ListSignalsFilter {
138 name: name.clone(),
139 key: key.clone(),
140 page: *page,
141 per_page: *per_page,
142 };
143 let response = client.list_signals(&filter).await?;
144 output::print_output(json_mode, &response, || signals_table(&response.data))
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use std::fs::write;
152
153 use tempfile::tempdir;
154
155 use super::*;
156
157 #[test]
158 fn parse_payload_defaults_to_an_empty_object() {
159 assert_eq!(parse_payload(None).unwrap(), json!({}));
160 }
161
162 #[test]
163 fn parse_payload_accepts_inline_json() {
164 let payload = parse_payload(Some(r#"{"status":"success"}"#)).unwrap();
165 assert_eq!(payload, json!({"status": "success"}));
166 }
167
168 #[test]
169 fn parse_payload_reads_a_file() {
170 let dir = tempdir().unwrap();
171 let path = dir.path().join("payload.json");
172 write(&path, r#"{"sha":"abc"}"#).unwrap();
173 let payload = parse_payload(Some(&format!("@{}", path.display()))).unwrap();
174 assert_eq!(payload, json!({"sha": "abc"}));
175 }
176
177 #[test]
178 fn parse_payload_rejects_invalid_json_and_non_objects() {
179 assert!(parse_payload(Some("{not json")).is_err());
180 assert!(parse_payload(Some("[1, 2]")).is_err());
181 assert!(parse_payload(Some("@/nonexistent/payload.json")).is_err());
182 }
183}