1use clap::Parser;
2use std::net::SocketAddr;
3use std::str::FromStr;
4use tracing::debug;
5
6use crate::error::{CliError, CliResult};
7use crate::output::formatters::ActorEvents;
8use crate::CommandContext;
9use theater::chain::ChainEvent;
10use theater::id::TheaterId;
11
12#[derive(Debug, Parser)]
14pub struct EventsArgs {
15 #[arg(required = true)]
17 pub actor_id: String,
18
19 #[arg(short, long, default_value = "127.0.0.1:9000")]
21 pub address: SocketAddr,
22
23 #[arg(short, long, default_value = "0")]
25 pub limit: usize,
26
27 #[arg(short = 't', long)]
29 pub event_type: Option<String>,
30
31 #[arg(long)]
33 pub from: Option<String>,
34
35 #[arg(long)]
37 pub to: Option<String>,
38
39 #[arg(long)]
41 pub search: Option<String>,
42
43 #[arg(short, long, default_value = "chain")]
45 pub sort: String,
46
47 #[arg(short = 'r', long)]
49 pub reverse: bool,
50
51 #[arg(short = 'd', long)]
53 pub detailed: bool,
54
55 #[arg(long, short = 'f', default_value = "pretty")]
56 pub format: Option<String>,
57}
58
59pub async fn execute_async(args: &EventsArgs, ctx: &CommandContext) -> CliResult<()> {
61 debug!("Getting events for actor: {}", args.actor_id);
62 debug!("Connecting to server at: {}", args.address);
63
64 let actor_id = TheaterId::from_str(&args.actor_id).map_err(|_| CliError::InvalidInput {
66 field: "actor_id".to_string(),
67 value: args.actor_id.clone(),
68 suggestion: "Provide a valid actor ID in the correct format".to_string(),
69 })?;
70
71 let client = ctx.create_client();
73 client
74 .connect()
75 .await
76 .map_err(|e| CliError::connection_failed(args.address, e))?;
77
78 let mut events = client
80 .get_actor_events(&actor_id.to_string())
81 .await
82 .map_err(|e| CliError::ServerError {
83 message: format!("Failed to get actor events: {}", e),
84 })?;
85
86 apply_filters(&mut events, args)?;
88
89 apply_sorting(&mut events, &args.sort, args.reverse)?;
91
92 if args.limit > 0 && events.len() > args.limit {
94 events = events.into_iter().take(args.limit).collect();
95 }
96
97 let actor_events = ActorEvents {
99 actor_id: actor_id.to_string(),
100 events,
101 };
102
103 let format = if let Some(fmt) = &args.format {
105 fmt.clone()
106 } else if ctx.json {
107 "json".to_string()
108 } else {
109 "pretty".to_string()
110 };
111 ctx.output.output(&actor_events, Some(&format))?;
112
113 Ok(())
114}
115
116fn apply_filters(events: &mut Vec<ChainEvent>, args: &EventsArgs) -> CliResult<()> {
118 if let Some(event_type) = &args.event_type {
120 events.retain(|e| e.event_type.contains(event_type));
121 }
122
123 if let Some(from_str) = &args.from {
125 let from_time = parse_time_spec(from_str)?;
126 events.retain(|e| e.timestamp >= from_time);
127 }
128
129 if let Some(to_str) = &args.to {
130 let to_time = parse_time_spec(to_str)?;
131 events.retain(|e| e.timestamp <= to_time);
132 }
133
134 if let Some(search_text) = &args.search {
136 events.retain(|e| {
137 if e.event_type.contains(search_text) {
139 return true;
140 }
141
142 if let Some(desc) = &e.description {
144 if desc.contains(search_text) {
145 return true;
146 }
147 }
148
149 if let Ok(data_str) = std::str::from_utf8(&e.data) {
151 if data_str.contains(search_text) {
152 return true;
153 }
154 }
155
156 false
157 });
158 }
159
160 Ok(())
161}
162
163fn apply_sorting(events: &mut Vec<ChainEvent>, sort_type: &str, reverse: bool) -> CliResult<()> {
165 match sort_type {
166 "chain" => {
167 let ordered_events = order_events_by_chain(events, reverse);
168 *events = ordered_events;
169 }
170 "time" => {
171 if reverse {
172 events.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
173 } else {
174 events.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
175 }
176 }
177 "type" => {
178 if reverse {
179 events.sort_by(|a, b| b.event_type.cmp(&a.event_type));
180 } else {
181 events.sort_by(|a, b| a.event_type.cmp(&b.event_type));
182 }
183 }
184 "size" => {
185 if reverse {
186 events.sort_by(|a, b| a.data.len().cmp(&b.data.len()));
187 } else {
188 events.sort_by(|a, b| b.data.len().cmp(&a.data.len()));
189 }
190 }
191 _ => {
192 return Err(CliError::InvalidInput {
193 field: "sort".to_string(),
194 value: sort_type.to_string(),
195 suggestion: "Use one of: chain, time, type, size".to_string(),
196 });
197 }
198 }
199 Ok(())
200}
201
202fn parse_time_spec(spec: &str) -> CliResult<u64> {
204 if let Ok(timestamp) = spec.parse::<u64>() {
206 return Ok(timestamp);
207 }
208
209 let now = std::time::SystemTime::now()
211 .duration_since(std::time::UNIX_EPOCH)
212 .unwrap()
213 .as_secs();
214
215 let (amount_str, unit) = spec.chars().partition::<String, _>(|c| c.is_ascii_digit());
216 let amount = amount_str
217 .parse::<u64>()
218 .map_err(|_| CliError::InvalidInput {
219 field: "time".to_string(),
220 value: spec.to_string(),
221 suggestion: "Use format like '1h', '2d', '30m', or a unix timestamp".to_string(),
222 })?;
223
224 match unit.as_str() {
225 "s" => Ok(now - amount),
226 "m" => Ok(now - amount * 60),
227 "h" => Ok(now - amount * 3600),
228 "d" => Ok(now - amount * 86400),
229 "w" => Ok(now - amount * 604800),
230 _ => Err(CliError::InvalidInput {
231 field: "time_unit".to_string(),
232 value: unit,
233 suggestion: "Use time units: s (seconds), m (minutes), h (hours), d (days), w (weeks)"
234 .to_string(),
235 }),
236 }
237}
238
239fn order_events_by_chain(events: &[ChainEvent], reverse: bool) -> Vec<ChainEvent> {
241 if events.is_empty() {
242 return Vec::new();
243 }
244
245 use std::collections::HashMap;
246
247 let root = events.iter().find(|e| e.parent_hash.is_none());
249
250 let root = match root {
252 Some(r) => r,
253 None => return events.to_vec(),
254 };
255
256 let mut parent_to_children: HashMap<Vec<u8>, Vec<&ChainEvent>> = HashMap::new();
258 for event in events {
259 if let Some(parent_hash) = &event.parent_hash {
260 parent_to_children
261 .entry(parent_hash.clone())
262 .or_insert_with(Vec::new)
263 .push(event);
264 }
265 }
266
267 let mut ordered_events = Vec::new();
269
270 fn traverse_chain(
271 event: &ChainEvent,
272 parent_to_children: &HashMap<Vec<u8>, Vec<&ChainEvent>>,
273 ordered_events: &mut Vec<ChainEvent>,
274 ) {
275 ordered_events.push(event.clone());
276
277 if let Some(children) = parent_to_children.get(&event.hash) {
278 for &child in children {
279 traverse_chain(child, parent_to_children, ordered_events);
280 }
281 }
282 }
283
284 traverse_chain(root, &parent_to_children, &mut ordered_events);
286
287 if reverse {
289 ordered_events.reverse();
290 }
291
292 ordered_events
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::config::Config;
299 use crate::output::OutputManager;
300
301 #[tokio::test]
302 async fn test_events_command_invalid_actor_id() {
303 let args = EventsArgs {
304 actor_id: "invalid-id".to_string(),
305 address: "127.0.0.1:9000".parse().unwrap(),
306 limit: 0,
307 event_type: None,
308 from: None,
309 to: None,
310 search: None,
311 sort: "chain".to_string(),
312 reverse: false,
313 detailed: false,
314 format: None,
315 };
316 let config = Config::default();
317 let output = OutputManager::new(config.output.clone());
318
319 let ctx = CommandContext {
320 config,
321 output,
322 verbose: false,
323 json: false,
324 };
325
326 let result = execute_async(&args, &ctx).await;
327 assert!(result.is_err());
328 if let Err(CliError::InvalidInput { field, .. }) = result {
329 assert_eq!(field, "actor_id");
330 } else {
331 panic!("Expected InvalidInput error");
332 }
333 }
334
335 #[test]
336 fn test_parse_time_spec() {
337 assert_eq!(parse_time_spec("1000").unwrap(), 1000);
339
340 assert!(parse_time_spec("1h").is_ok());
342 assert!(parse_time_spec("2d").is_ok());
343 assert!(parse_time_spec("30m").is_ok());
344
345 assert!(parse_time_spec("invalid").is_err());
347 assert!(parse_time_spec("1x").is_err());
348 }
349}