use crate::analytics::{
compare::{compare_topics, CompareConfig},
entities::{extract_entities, EntitiesConfig, EntityType},
report::{generate_report, ReportConfig, ReportFormat},
sentiment::{analyze_sentiment, SentimentConfig, SentimentDimension},
trends::{analyze_trends, Granularity, TrendsConfig},
};
use crate::cli::args::{
AnalyticsCommand, AnalyticsCompareArgs, AnalyticsEntitiesArgs, AnalyticsSentimentArgs,
AnalyticsTrendsArgs, EntityTypeArg, GlobalArgs, Granularity as ArgGranularity,
ReportArgs, ReportCommand, ReportCustomArgs, SentimentDimension as ArgSentimentDimension,
};
use crate::cli::output::OutputWriter;
use crate::db::AnalyticsDb;
use crate::error::{ExitStatus, GdeltError, Result};
use chrono::{Duration, Utc};
pub async fn handle_analytics(cmd: AnalyticsCommand, global: &GlobalArgs) -> Result<ExitStatus> {
match cmd {
AnalyticsCommand::Trends(args) => handle_trends(args, global).await,
AnalyticsCommand::Entities(args) => handle_entities(args, global).await,
AnalyticsCommand::Sentiment(args) => handle_sentiment(args, global).await,
AnalyticsCommand::Compare(args) => handle_compare(args, global).await,
AnalyticsCommand::Report(cmd) => handle_report(cmd, global).await,
}
}
async fn handle_trends(args: AnalyticsTrendsArgs, global: &GlobalArgs) -> Result<ExitStatus> {
if args.topics.is_empty() {
return Err(GdeltError::Validation("At least one topic is required".into()));
}
let db = AnalyticsDb::open()?;
let (start_date, end_date) = parse_timespan(&args.timespan)?;
let config = TrendsConfig {
topics: args.topics,
granularity: convert_granularity(args.granularity),
start_date: Some(start_date),
end_date: Some(end_date),
normalize: args.normalize,
detect_anomalies: args.detect_anomalies,
..Default::default()
};
let results = analyze_trends(&db, &config)?;
let writer = OutputWriter::new(global);
writer.write_value(&results)?;
Ok(ExitStatus::Success)
}
async fn handle_entities(args: AnalyticsEntitiesArgs, global: &GlobalArgs) -> Result<ExitStatus> {
let db = AnalyticsDb::open()?;
let (start_date, end_date) = parse_timespan(&args.timespan)?;
let config = EntitiesConfig {
entity_type: convert_entity_type(args.entity_type),
min_count: args.min_count,
limit: args.limit,
start_date: Some(start_date),
end_date: Some(end_date),
};
let results = extract_entities(&db, &config)?;
let writer = OutputWriter::new(global);
writer.write_value(&results)?;
Ok(ExitStatus::Success)
}
async fn handle_sentiment(args: AnalyticsSentimentArgs, global: &GlobalArgs) -> Result<ExitStatus> {
let db = AnalyticsDb::open()?;
let (start_date, end_date) = parse_timespan(&args.timespan)?;
let config = SentimentConfig {
topic: args.topic,
dimension: convert_sentiment_dimension(args.dimension),
start_date: Some(start_date),
end_date: Some(end_date),
granularity: convert_granularity(args.granularity),
compare_topic: args.compare_with,
};
let result = analyze_sentiment(&db, &config)?;
let writer = OutputWriter::new(global);
writer.write_value(&result)?;
Ok(ExitStatus::Success)
}
async fn handle_compare(args: AnalyticsCompareArgs, global: &GlobalArgs) -> Result<ExitStatus> {
let db = AnalyticsDb::open()?;
let (start_date, end_date) = parse_timespan(&args.timespan)?;
let config = CompareConfig {
topic_a: args.topic_a,
topic_b: args.topic_b,
start_date: Some(start_date),
end_date: Some(end_date),
};
let result = compare_topics(&db, &config)?;
let writer = OutputWriter::new(global);
writer.write_value(&result)?;
Ok(ExitStatus::Success)
}
async fn handle_report(cmd: ReportCommand, global: &GlobalArgs) -> Result<ExitStatus> {
match cmd {
ReportCommand::Daily(args) => handle_daily_report(args, global).await,
ReportCommand::Weekly(args) => handle_weekly_report(args, global).await,
ReportCommand::Custom(args) => handle_custom_report(args, global).await,
}
}
async fn handle_daily_report(args: ReportArgs, global: &GlobalArgs) -> Result<ExitStatus> {
let db = AnalyticsDb::open()?;
let today = Utc::now().naive_utc().date();
let yesterday = today - Duration::days(1);
let config = ReportConfig {
topics: args.topics,
start_date: yesterday.format("%Y-%m-%d").to_string(),
end_date: today.format("%Y-%m-%d").to_string(),
output_path: args.output,
format: ReportFormat::Json,
};
let report = generate_report(&db, &config)?;
if let Some(ref path) = config.output_path {
let content = match path.extension().and_then(|e| e.to_str()) {
Some("md") => report.to_markdown(),
Some("html") => report.to_html(),
_ => serde_json::to_string_pretty(&report)?,
};
std::fs::write(path, content)?;
eprintln!("Report written to: {}", path.display());
} else {
let writer = OutputWriter::new(global);
writer.write_value(&report)?;
}
Ok(ExitStatus::Success)
}
async fn handle_weekly_report(args: ReportArgs, global: &GlobalArgs) -> Result<ExitStatus> {
let db = AnalyticsDb::open()?;
let today = Utc::now().naive_utc().date();
let week_ago = today - Duration::days(7);
let config = ReportConfig {
topics: args.topics,
start_date: week_ago.format("%Y-%m-%d").to_string(),
end_date: today.format("%Y-%m-%d").to_string(),
output_path: args.output,
format: ReportFormat::Json,
};
let report = generate_report(&db, &config)?;
if let Some(ref path) = config.output_path {
let content = match path.extension().and_then(|e| e.to_str()) {
Some("md") => report.to_markdown(),
Some("html") => report.to_html(),
_ => serde_json::to_string_pretty(&report)?,
};
std::fs::write(path, content)?;
eprintln!("Report written to: {}", path.display());
} else {
let writer = OutputWriter::new(global);
writer.write_value(&report)?;
}
Ok(ExitStatus::Success)
}
async fn handle_custom_report(args: ReportCustomArgs, global: &GlobalArgs) -> Result<ExitStatus> {
let db = AnalyticsDb::open()?;
let config = ReportConfig {
topics: args.topics,
start_date: args.start,
end_date: args.end,
output_path: args.output,
format: ReportFormat::Json,
};
let report = generate_report(&db, &config)?;
if let Some(ref path) = config.output_path {
let content = match path.extension().and_then(|e| e.to_str()) {
Some("md") => report.to_markdown(),
Some("html") => report.to_html(),
_ => serde_json::to_string_pretty(&report)?,
};
std::fs::write(path, content)?;
eprintln!("Report written to: {}", path.display());
} else {
let writer = OutputWriter::new(global);
writer.write_value(&report)?;
}
Ok(ExitStatus::Success)
}
fn parse_timespan(timespan: &str) -> Result<(String, String)> {
let today = Utc::now().naive_utc().date();
let duration = if timespan.ends_with('h') {
let hours: i64 = timespan
.trim_end_matches('h')
.parse()
.map_err(|_| GdeltError::Validation(format!("Invalid timespan: {}", timespan)))?;
Duration::hours(hours)
} else if timespan.ends_with('d') {
let days: i64 = timespan
.trim_end_matches('d')
.parse()
.map_err(|_| GdeltError::Validation(format!("Invalid timespan: {}", timespan)))?;
Duration::days(days)
} else if timespan.ends_with('w') {
let weeks: i64 = timespan
.trim_end_matches('w')
.parse()
.map_err(|_| GdeltError::Validation(format!("Invalid timespan: {}", timespan)))?;
Duration::weeks(weeks)
} else if timespan.ends_with('m') {
let months: i64 = timespan
.trim_end_matches('m')
.parse()
.map_err(|_| GdeltError::Validation(format!("Invalid timespan: {}", timespan)))?;
Duration::days(months * 30) } else {
return Err(GdeltError::Validation(format!(
"Invalid timespan format: {}. Use 'Nh', 'Nd', 'Nw', or 'Nm'",
timespan
)));
};
let start = today - duration;
Ok((
start.format("%Y-%m-%d").to_string(),
today.format("%Y-%m-%d").to_string(),
))
}
fn convert_granularity(arg: ArgGranularity) -> Granularity {
match arg {
ArgGranularity::Hour => Granularity::Hour,
ArgGranularity::Day => Granularity::Day,
ArgGranularity::Week => Granularity::Week,
ArgGranularity::Month => Granularity::Month,
}
}
fn convert_entity_type(arg: EntityTypeArg) -> EntityType {
match arg {
EntityTypeArg::Person => EntityType::Person,
EntityTypeArg::Organization => EntityType::Organization,
EntityTypeArg::Location => EntityType::Location,
EntityTypeArg::Actor => EntityType::Actor,
EntityTypeArg::Theme => EntityType::Theme,
EntityTypeArg::All => EntityType::All,
}
}
fn convert_sentiment_dimension(arg: ArgSentimentDimension) -> SentimentDimension {
match arg {
ArgSentimentDimension::Time => SentimentDimension::Time,
ArgSentimentDimension::Region => SentimentDimension::Region,
ArgSentimentDimension::Source => SentimentDimension::Source,
ArgSentimentDimension::Entity => SentimentDimension::Entity,
}
}