theater_cli/commands/
events_explore.rs1use clap::Parser;
2use std::net::SocketAddr;
3use std::str::FromStr;
4use tracing::debug;
5
6use crate::error::{CliError, CliResult};
7use crate::tui::event_explorer;
8use crate::CommandContext;
9use theater::chain::ChainEvent;
10use theater::id::TheaterId;
11
12#[derive(Debug, Parser)]
14pub struct ExploreArgs {
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)]
25 pub live: bool,
26
27 #[arg(short, long)]
29 pub follow: bool,
30
31 #[arg(short = 'n', long, default_value = "1000")]
33 pub limit: usize,
34
35 #[arg(short = 't', long)]
37 pub event_type: Option<String>,
38
39 #[arg(long)]
41 pub from: Option<String>,
42
43 #[arg(long)]
45 pub to: Option<String>,
46
47 #[arg(long)]
49 pub search: Option<String>,
50
51 #[arg(long, default_value = None)]
52 pub format: Option<String>,
53}
54
55pub async fn execute_async(args: &ExploreArgs, ctx: &CommandContext) -> CliResult<()> {
57 debug!("Starting event explorer for actor: {}", args.actor_id);
58 debug!("Server address: {}", args.address);
59 debug!("Live mode: {}", args.live);
60
61 let actor_id = TheaterId::from_str(&args.actor_id).map_err(|_| CliError::InvalidInput {
63 field: "actor_id".to_string(),
64 value: args.actor_id.clone(),
65 suggestion: "Provide a valid actor ID in the correct format".to_string(),
66 })?;
67
68 let events = load_events(args, ctx, &actor_id).await?;
70
71 let mut app =
73 event_explorer::EventExplorerApp::new(args.actor_id.clone(), args.live, args.follow);
74
75 if let Some(event_type) = &args.event_type {
77 app.set_event_type_filter(event_type.clone());
78 }
79
80 if let Some(search) = &args.search {
81 app.set_search_query(search.clone());
82 }
83
84 app.load_events(events);
86
87 event_explorer::run_explorer(app, ctx, args.address).await?;
89
90 Ok(())
91}
92
93async fn load_events(
95 args: &ExploreArgs,
96 ctx: &CommandContext,
97 actor_id: &TheaterId,
98) -> CliResult<Vec<ChainEvent>> {
99 debug!("Loading events for actor: {}", actor_id);
100
101 let client = ctx.create_client();
103
104 if args.live {
105 match client.connect().await {
107 Ok(_) => {
108 debug!("Connected to live actor");
109 client
111 .get_actor_events(&actor_id.to_string())
112 .await
113 .map_err(|e| CliError::ServerError {
114 message: format!("Failed to get live actor events: {}", e),
115 })
116 }
117 Err(e) => {
118 debug!(
119 "Failed to connect to live actor: {}, falling back to stored events",
120 e
121 );
122 load_stored_events(args, ctx, actor_id).await
124 }
125 }
126 } else {
127 load_stored_events(args, ctx, actor_id).await
129 }
130}
131
132async fn load_stored_events(
134 args: &ExploreArgs,
135 ctx: &CommandContext,
136 actor_id: &TheaterId,
137) -> CliResult<Vec<ChainEvent>> {
138 debug!("Loading stored events for actor: {}", actor_id);
139
140 let events_args = crate::commands::events::EventsArgs {
142 actor_id: actor_id.to_string(),
143 address: args.address,
144 limit: args.limit,
145 event_type: args.event_type.clone(),
146 from: args.from.clone(),
147 to: args.to.clone(),
148 search: args.search.clone(),
149 sort: "chain".to_string(),
150 reverse: false,
151 detailed: false,
152 format: args.format.clone(),
153 };
154
155 let client = ctx.create_client();
157 client
158 .connect()
159 .await
160 .map_err(|e| CliError::connection_failed(args.address, e))?;
161
162 let mut events = client
163 .get_actor_events(&actor_id.to_string())
164 .await
165 .map_err(|e| CliError::ServerError {
166 message: format!("Failed to get stored actor events: {}", e),
167 })?;
168
169 apply_filters(&mut events, &events_args)?;
171
172 Ok(events)
173}
174
175fn apply_filters(
177 events: &mut Vec<ChainEvent>,
178 args: &crate::commands::events::EventsArgs,
179) -> CliResult<()> {
180 if let Some(event_type) = &args.event_type {
182 events.retain(|e| e.event_type.contains(event_type));
183 }
184
185 if let Some(search_text) = &args.search {
187 events.retain(|e| {
188 if e.event_type.contains(search_text) {
190 return true;
191 }
192
193 if let Some(desc) = &e.description {
195 if desc.contains(search_text) {
196 return true;
197 }
198 }
199
200 if let Ok(data_str) = std::str::from_utf8(&e.data) {
202 if data_str.contains(search_text) {
203 return true;
204 }
205 }
206
207 false
208 });
209 }
210
211 Ok(())
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::config::Config;
221 use crate::output::OutputManager;
222
223 #[tokio::test]
224 async fn test_explore_command_invalid_actor_id() {
225 let args = ExploreArgs {
226 actor_id: "invalid-id".to_string(),
227 address: "127.0.0.1:9000".parse().unwrap(),
228 live: false,
229 follow: false,
230 limit: 100,
231 event_type: None,
232 from: None,
233 to: None,
234 search: None,
235 format: None,
236 };
237
238 let config = Config::default();
239 let output = OutputManager::new(config.output.clone());
240 let ctx = CommandContext {
241 config,
242 output,
243 verbose: false,
244 json: false,
245 };
246
247 let result = execute_async(&args, &ctx).await;
248 assert!(result.is_err());
249 if let Err(CliError::InvalidInput { field, .. }) = result {
250 assert_eq!(field, "actor_id");
251 } else {
252 panic!("Expected InvalidInput error");
253 }
254 }
255}