1use clap::Subcommand;
2use serde_json::json;
3
4use crate::util::{api_request, exit_error, read_json_from_file};
5
6#[derive(Subcommand)]
7pub enum EventCommands {
8 Create {
10 #[arg(long)]
12 event_type: String,
13 #[arg(long)]
15 timestamp: Option<String>,
16 #[arg(long, required_unless_present = "data_file")]
18 data: Option<String>,
19 #[arg(long, short = 'f', conflicts_with = "data")]
21 data_file: Option<String>,
22 #[arg(long)]
24 idempotency_key: Option<String>,
25 #[arg(long, default_value = "cli")]
27 source: String,
28 #[arg(long)]
30 agent: Option<String>,
31 },
32 List {
34 #[arg(long)]
36 event_type: Option<String>,
37 #[arg(long)]
39 since: Option<String>,
40 #[arg(long)]
42 until: Option<String>,
43 #[arg(long)]
45 limit: Option<u32>,
46 #[arg(long)]
48 cursor: Option<String>,
49 },
50 Batch {
52 #[arg(long)]
54 file: String,
55 },
56}
57
58pub async fn run(api_url: &str, token: Option<&str>, command: EventCommands) -> i32 {
59 match command {
60 EventCommands::Create {
61 event_type,
62 timestamp,
63 data,
64 data_file,
65 idempotency_key,
66 source,
67 agent,
68 } => {
69 create(
70 api_url,
71 token,
72 &event_type,
73 timestamp.as_deref(),
74 data.as_deref(),
75 data_file.as_deref(),
76 idempotency_key.as_deref(),
77 &source,
78 agent.as_deref(),
79 )
80 .await
81 }
82 EventCommands::List {
83 event_type,
84 since,
85 until,
86 limit,
87 cursor,
88 } => {
89 list(
90 api_url,
91 token,
92 event_type.as_deref(),
93 since.as_deref(),
94 until.as_deref(),
95 limit,
96 cursor.as_deref(),
97 )
98 .await
99 }
100 EventCommands::Batch { file } => batch(api_url, token, &file).await,
101 }
102}
103
104async fn create(
105 api_url: &str,
106 token: Option<&str>,
107 event_type: &str,
108 timestamp: Option<&str>,
109 data: Option<&str>,
110 data_file: Option<&str>,
111 idempotency_key: Option<&str>,
112 source: &str,
113 agent: Option<&str>,
114) -> i32 {
115 let data_value: serde_json::Value = if let Some(d) = data {
116 match serde_json::from_str(d) {
117 Ok(v) => v,
118 Err(e) => exit_error(
119 &format!("Invalid JSON in --data: {e}"),
120 Some("Provide valid JSON, e.g. --data '{\"weight_kg\":100}'"),
121 ),
122 }
123 } else if let Some(f) = data_file {
124 match read_json_from_file(f) {
125 Ok(v) => v,
126 Err(e) => exit_error(&e, Some("Provide a valid JSON file or use '-' for stdin")),
127 }
128 } else {
129 exit_error(
130 "Either --data or --data-file is required",
131 Some("Use --data '{...}' or --data-file path.json"),
132 )
133 };
134
135 let ts = match timestamp {
136 Some(t) => t.to_string(),
137 None => chrono::Utc::now().to_rfc3339(),
138 };
139
140 let idem_key = idempotency_key
141 .map(|k| k.to_string())
142 .unwrap_or_else(|| uuid::Uuid::now_v7().to_string());
143
144 let mut metadata = json!({
145 "source": source,
146 "idempotency_key": idem_key
147 });
148 if let Some(a) = agent {
149 metadata["agent"] = json!(a);
150 }
151
152 let body = json!({
153 "timestamp": ts,
154 "event_type": event_type,
155 "data": data_value,
156 "metadata": metadata
157 });
158
159 api_request(
160 api_url,
161 reqwest::Method::POST,
162 "/v1/events",
163 token,
164 Some(body),
165 &[],
166 &[],
167 false,
168 false,
169 )
170 .await
171}
172
173async fn list(
174 api_url: &str,
175 token: Option<&str>,
176 event_type: Option<&str>,
177 since: Option<&str>,
178 until: Option<&str>,
179 limit: Option<u32>,
180 cursor: Option<&str>,
181) -> i32 {
182 let mut query = Vec::new();
183 if let Some(et) = event_type {
184 query.push(("event_type".to_string(), et.to_string()));
185 }
186 if let Some(s) = since {
187 query.push(("since".to_string(), s.to_string()));
188 }
189 if let Some(u) = until {
190 query.push(("until".to_string(), u.to_string()));
191 }
192 if let Some(l) = limit {
193 query.push(("limit".to_string(), l.to_string()));
194 }
195 if let Some(c) = cursor {
196 query.push(("cursor".to_string(), c.to_string()));
197 }
198
199 api_request(
200 api_url,
201 reqwest::Method::GET,
202 "/v1/events",
203 token,
204 None,
205 &query,
206 &[],
207 false,
208 false,
209 )
210 .await
211}
212
213async fn batch(api_url: &str, token: Option<&str>, file: &str) -> i32 {
214 let body = match read_json_from_file(file) {
215 Ok(v) => v,
216 Err(e) => exit_error(&e, Some("Provide a JSON file with {\"events\": [...]}")),
217 };
218
219 api_request(
220 api_url,
221 reqwest::Method::POST,
222 "/v1/events/batch",
223 token,
224 Some(body),
225 &[],
226 &[],
227 false,
228 false,
229 )
230 .await
231}