use clap::Parser;
use std::net::SocketAddr;
use std::str::FromStr;
use tracing::debug;
use crate::error::{CliError, CliResult};
use crate::tui::event_explorer;
use crate::CommandContext;
use theater::chain::ChainEvent;
use theater::id::TheaterId;
#[derive(Debug, Parser)]
pub struct ExploreArgs {
#[arg(required = true)]
pub actor_id: String,
#[arg(short, long, default_value = "127.0.0.1:9000")]
pub address: SocketAddr,
#[arg(short, long)]
pub live: bool,
#[arg(short, long)]
pub follow: bool,
#[arg(short = 'n', long, default_value = "1000")]
pub limit: usize,
#[arg(short = 't', long)]
pub event_type: Option<String>,
#[arg(long)]
pub from: Option<String>,
#[arg(long)]
pub to: Option<String>,
#[arg(long)]
pub search: Option<String>,
#[arg(long, default_value = None)]
pub format: Option<String>,
}
pub async fn execute_async(args: &ExploreArgs, ctx: &CommandContext) -> CliResult<()> {
debug!("Starting event explorer for actor: {}", args.actor_id);
debug!("Server address: {}", args.address);
debug!("Live mode: {}", args.live);
let actor_id = TheaterId::from_str(&args.actor_id).map_err(|_| CliError::InvalidInput {
field: "actor_id".to_string(),
value: args.actor_id.clone(),
suggestion: "Provide a valid actor ID in the correct format".to_string(),
})?;
let events = load_events(args, ctx, &actor_id).await?;
let mut app =
event_explorer::EventExplorerApp::new(args.actor_id.clone(), args.live, args.follow);
if let Some(event_type) = &args.event_type {
app.set_event_type_filter(event_type.clone());
}
if let Some(search) = &args.search {
app.set_search_query(search.clone());
}
app.load_events(events);
event_explorer::run_explorer(app, ctx, args.address).await?;
Ok(())
}
async fn load_events(
args: &ExploreArgs,
ctx: &CommandContext,
actor_id: &TheaterId,
) -> CliResult<Vec<ChainEvent>> {
debug!("Loading events for actor: {}", actor_id);
let client = ctx.create_client();
if args.live {
match client.connect().await {
Ok(_) => {
debug!("Connected to live actor");
client
.get_actor_events(&actor_id.to_string())
.await
.map_err(|e| CliError::ServerError {
message: format!("Failed to get live actor events: {}", e),
})
}
Err(e) => {
debug!(
"Failed to connect to live actor: {}, falling back to stored events",
e
);
load_stored_events(args, ctx, actor_id).await
}
}
} else {
load_stored_events(args, ctx, actor_id).await
}
}
async fn load_stored_events(
args: &ExploreArgs,
ctx: &CommandContext,
actor_id: &TheaterId,
) -> CliResult<Vec<ChainEvent>> {
debug!("Loading stored events for actor: {}", actor_id);
let events_args = crate::commands::events::EventsArgs {
actor_id: actor_id.to_string(),
address: args.address,
limit: args.limit,
event_type: args.event_type.clone(),
from: args.from.clone(),
to: args.to.clone(),
search: args.search.clone(),
sort: "chain".to_string(),
reverse: false,
detailed: false,
format: args.format.clone(),
};
let client = ctx.create_client();
client
.connect()
.await
.map_err(|e| CliError::connection_failed(args.address, e))?;
let mut events = client
.get_actor_events(&actor_id.to_string())
.await
.map_err(|e| CliError::ServerError {
message: format!("Failed to get stored actor events: {}", e),
})?;
apply_filters(&mut events, &events_args)?;
Ok(events)
}
fn apply_filters(
events: &mut Vec<ChainEvent>,
args: &crate::commands::events::EventsArgs,
) -> CliResult<()> {
if let Some(event_type) = &args.event_type {
events.retain(|e| e.event_type.contains(event_type));
}
if let Some(search_text) = &args.search {
events.retain(|e| {
if e.event_type.contains(search_text) {
return true;
}
if let Some(desc) = &e.description {
if desc.contains(search_text) {
return true;
}
}
if let Ok(data_str) = std::str::from_utf8(&e.data) {
if data_str.contains(search_text) {
return true;
}
}
false
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::output::OutputManager;
#[tokio::test]
async fn test_explore_command_invalid_actor_id() {
let args = ExploreArgs {
actor_id: "invalid-id".to_string(),
address: "127.0.0.1:9000".parse().unwrap(),
live: false,
follow: false,
limit: 100,
event_type: None,
from: None,
to: None,
search: None,
format: None,
};
let config = Config::default();
let output = OutputManager::new(config.output.clone());
let ctx = CommandContext {
config,
output,
verbose: false,
json: false,
};
let result = execute_async(&args, &ctx).await;
assert!(result.is_err());
if let Err(CliError::InvalidInput { field, .. }) = result {
assert_eq!(field, "actor_id");
} else {
panic!("Expected InvalidInput error");
}
}
}