use clap::{Parser, Subcommand};
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use mudra_cli::{ConversionRequest, ConversionType, CurrencyConverter, CurrencyError, Result};
use std::time::Duration;
#[derive(Parser)]
#[command(
name = "mudra",
about = "A robust currency converter using real-time exchange rates",
version = "0.1.0",
author = "Your Name"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(long, global = true)]
pub no_color: bool,
}
#[derive(Subcommand)]
pub enum Commands {
Convert {
amount: f64,
from: String,
to: String,
#[arg(short, long, default_value = "2")]
precision: u32,
#[arg(long)]
date: Option<String>,
},
List {
#[arg(short, long)]
extended: bool,
#[arg(short, long)]
filter: Option<String>,
},
Rates {
base: String,
#[arg(short, long)]
currencies: Option<String>,
#[arg(short, long)]
limit: Option<usize>,
#[arg(long)]
date: Option<String>,
},
Compare {
amount: f64,
from: String,
to: String,
#[arg(long)]
date: Option<String>,
},
Cache {
#[command(subcommand)]
action: CacheAction,
},
Interactive,
}
#[derive(Subcommand)]
pub enum CacheAction {
Stats,
Clear,
Cleanup,
}
pub struct CliHandler {
converter: CurrencyConverter,
verbose: bool,
use_color: bool,
}
impl CliHandler {
pub fn new(converter: CurrencyConverter, verbose: bool, use_color: bool) -> Self {
Self {
converter,
verbose,
use_color,
}
}
pub async fn handle_command(&self, command: Commands) -> Result<()> {
match command {
Commands::Convert {
amount,
from,
to,
precision,
date,
} => {
if let Some(historical_date) = date {
self.handle_historical_convert(amount, &from, &to, &historical_date, precision)
.await
} else {
self.handle_convert(amount, &from, &to, precision).await
}
}
Commands::List { extended, filter } => {
self.handle_list(extended, filter.as_deref()).await
}
Commands::Rates {
base,
currencies,
limit,
date,
} => {
if let Some(historical_date) = date {
self.handle_historical_rates(
&base,
currencies.as_deref(),
limit,
&historical_date,
)
.await
} else {
self.handle_rates(&base, currencies.as_deref(), limit).await
}
}
Commands::Compare {
amount,
from,
to,
date,
} => {
if let Some(historical_date) = date {
self.handle_historical_compare(amount, &from, &to, &historical_date)
.await
} else {
self.handle_compare(amount, &from, &to).await
}
}
Commands::Cache { action } => self.handle_cache_command(action).await,
Commands::Interactive => self.handle_interactive().await,
}
}
async fn handle_convert(
&self,
amount: f64,
from: &str,
to: &str,
precision: u32,
) -> Result<()> {
let spinner = self.create_spinner("Converting currency...");
let request = ConversionRequest::from_components(amount, from, to)?;
match self.converter.convert(request).await {
Ok(result) => {
spinner.finish_and_clear();
let rounded_result = result.result.round(precision);
println!("{}", self.format_header("💱 Currency Conversion"));
println!();
println!(
" {} {}",
self.colorize("From:", "cyan"),
self.colorize(&format!("{}", result.request.from), "white")
);
println!(
" {} {}",
self.colorize("To:", "cyan"),
self.colorize(&format!("{}", rounded_result), "green")
);
println!(
" {} {}",
self.colorize("Rate:", "cyan"),
self.colorize(&format!("{:.6}", result.exchange_rate), "yellow")
);
println!(
" {} {}",
self.colorize("Type:", "cyan"),
self.format_conversion_type(&result.conversion_type)
);
if self.verbose {
println!(
" {} {}",
self.colorize("Timestamp:", "cyan"),
self.format_timestamp(result.timestamp)
);
}
Ok(())
}
Err(e) => {
spinner.finish_and_clear();
self.print_error(&format!("Conversion failed: {}", e));
Err(e)
}
}
}
async fn handle_list(&self, extended: bool, filter: Option<&str>) -> Result<()> {
let spinner = self.create_spinner("Fetching supported currencies...");
match self.converter.get_available_currencies().await {
Ok(mut currencies) => {
spinner.finish_and_clear();
if let Some(filter_pattern) = filter {
let pattern = filter_pattern.to_uppercase();
currencies.retain(|c| c.contains(&pattern));
}
currencies.sort();
println!("{}", self.format_header("🌍 Supported Currencies"));
if extended {
self.print_currencies_extended(¤cies);
} else {
self.print_currencies_compact(¤cies);
}
println!();
println!(
"{} {} currencies found",
self.colorize("Total:", "cyan"),
self.colorize(¤cies.len().to_string(), "green")
);
Ok(())
}
Err(e) => {
spinner.finish_and_clear();
self.print_error(&format!("Failed to fetch currencies: {}", e));
Err(e)
}
}
}
async fn handle_rates(
&self,
base: &str,
currencies: Option<&str>,
limit: Option<usize>,
) -> Result<()> {
let spinner = self.create_spinner(&format!(
"Fetching {} exchange rates...",
base.to_uppercase()
));
match self.converter.exchange_service.get_latest_rates(base).await {
Ok(rates_response) => {
spinner.finish_and_clear();
let mut rates: Vec<(String, f64)> =
rates_response.conversion_rates.into_iter().collect();
rates.sort_by(|a, b| a.0.cmp(&b.0));
if let Some(currency_list) = currencies {
let requested_currencies: Vec<String> = currency_list
.split(',')
.map(|s| s.trim().to_uppercase())
.collect();
rates.retain(|(code, _)| requested_currencies.contains(code));
}
if let Some(limit_count) = limit {
rates.truncate(limit_count);
}
println!(
"{}",
self.format_header(&format!(
"📊 Exchange Rates (Base: {})",
base.to_uppercase()
))
);
println!();
for (code, rate) in rates {
println!(
" {} {} = {} {}",
self.colorize("1", "white"),
self.colorize(&base.to_uppercase(), "cyan"),
self.colorize(&format!("{:.6}", rate), "green"),
self.colorize(&code, "yellow")
);
}
Ok(())
}
Err(e) => {
spinner.finish_and_clear();
self.print_error(&format!("Failed to fetch rates: {}", e));
Err(e)
}
}
}
async fn handle_historical_convert(
&self,
amount: f64,
from: &str,
to: &str,
date: &str,
precision: u32,
) -> Result<()> {
let spinner = self.create_spinner(&format!("Converting currency for {}...", date));
let request = mudra_cli::api::types::HistoricalConversionRequest {
amount,
from: from.to_string(),
to: to.to_string(),
date: date.to_string(),
};
match self
.converter
.exchange_service
.convert_historical(request)
.await
{
Ok(result) => {
spinner.finish_and_clear();
println!(
"{}",
self.format_header(&format!("💱 Historical Currency Conversion ({})", date))
);
println!();
println!(
" {} {} {} on {}",
self.colorize("From:", "cyan"),
self.colorize(&format!("{} {}", amount, from.to_uppercase()), "white"),
self.colorize("on", "cyan"),
self.colorize(date, "white")
);
let rounded_amount = (result.conversion_result * 10_f64.powi(precision as i32))
.round()
/ 10_f64.powi(precision as i32);
println!(
" {} {} {}",
self.colorize("To:", "cyan"),
self.colorize(
&format!(
"{:.prec$} {}",
rounded_amount,
result.target_code,
prec = precision as usize
),
"green"
),
self.colorize(
&format!("(historical rate: {:.6})", result.conversion_rate),
"yellow"
)
);
Ok(())
}
Err(e) => {
spinner.finish_and_clear();
self.print_error(&format!("Historical conversion failed: {}", e));
Err(e)
}
}
}
async fn handle_historical_rates(
&self,
base: &str,
currencies: Option<&str>,
limit: Option<usize>,
date: &str,
) -> Result<()> {
let spinner = self.create_spinner(&format!(
"Fetching {} exchange rates for {}...",
base.to_uppercase(),
date
));
match self
.converter
.exchange_service
.get_historical_rates(base, date)
.await
{
Ok(rates_response) => {
spinner.finish_and_clear();
let mut rates: Vec<(String, f64)> =
rates_response.conversion_rates.into_iter().collect();
rates.sort_by(|a, b| a.0.cmp(&b.0));
if let Some(currency_list) = currencies {
let requested_currencies: Vec<String> = currency_list
.split(',')
.map(|s| s.trim().to_uppercase())
.collect();
rates.retain(|(code, _)| requested_currencies.contains(code));
}
if let Some(limit_count) = limit {
rates.truncate(limit_count);
}
println!(
"{}",
self.format_header(&format!(
"📊 Historical Exchange Rates (Base: {}, Date: {})",
base.to_uppercase(),
date
))
);
println!();
for (code, rate) in rates {
println!(
" {} {} = {} {} {}",
self.colorize("1", "white"),
self.colorize(&base.to_uppercase(), "cyan"),
self.colorize(&format!("{:.6}", rate), "green"),
self.colorize(&code, "yellow"),
self.colorize(&format!("(on {})", date), "white")
);
}
Ok(())
}
Err(e) => {
spinner.finish_and_clear();
self.print_error(&format!("Failed to fetch historical rates: {}", e));
Err(e)
}
}
}
async fn handle_historical_compare(
&self,
amount: f64,
from: &str,
to_list: &str,
date: &str,
) -> Result<()> {
let currencies: Vec<&str> = to_list.split(',').map(|s| s.trim()).collect();
let spinner = self.create_spinner(&format!("Comparing currencies for {}...", date));
match self
.converter
.exchange_service
.get_historical_rates(from, date)
.await
{
Ok(rates) => {
spinner.finish_and_clear();
println!(
"{}",
self.format_header(&format!(
"🔄 Historical Currency Comparison ({} {} on {})",
amount,
from.to_uppercase(),
date
))
);
println!();
for currency in currencies {
let currency = currency.to_uppercase();
match rates.get_rate(¤cy) {
Some(rate) => {
let converted = amount * rate;
println!(
" {} {} {} (rate: {:.6})",
self.colorize("→", "cyan"),
self.colorize(&format!("{:.2} {}", converted, currency), "green"),
self.colorize(&format!("on {}", date), "white"),
self.colorize(&format!("{:.6}", rate), "yellow")
);
}
None => {
println!(
" {} {} ({})",
self.colorize("→", "red"),
self.colorize(¤cy, "red"),
self.colorize("rate not available", "red")
);
}
}
}
Ok(())
}
Err(e) => {
spinner.finish_and_clear();
self.print_error(&format!("Failed to fetch historical rates: {}", e));
Err(e)
}
}
}
async fn handle_cache_command(&self, action: CacheAction) -> Result<()> {
match action {
CacheAction::Stats => {
let stats = self.converter.exchange_service.get_cache_stats();
println!("{}", self.format_header("📈 Cache Statistics"));
println!();
println!(
" {} {}",
self.colorize("Total Requests:", "cyan"),
self.colorize(&stats.total_requests.to_string(), "white")
);
println!(
" {} {} ({:.1}%)",
self.colorize("Cache Hits:", "cyan"),
self.colorize(&stats.hits.to_string(), "green"),
self.colorize(&format!("{:.1}", stats.hit_rate), "green")
);
println!(
" {} {}",
self.colorize("Cache Misses:", "cyan"),
self.colorize(&stats.misses.to_string(), "yellow")
);
println!(
" {} {}",
self.colorize("Expired Entries:", "cyan"),
self.colorize(&stats.expired.to_string(), "red")
);
println!(
" {} {}",
self.colorize("Cached Entries:", "cyan"),
self.colorize(&stats.cached_entries.to_string(), "white")
);
println!(
" {} {} bytes",
self.colorize("Cache Size:", "cyan"),
self.colorize(&stats.weighted_size.to_string(), "white")
);
Ok(())
}
CacheAction::Clear => {
println!("{}", self.colorize("🗑️ Clearing cache...", "yellow"));
self.converter.exchange_service.clear_cache().await;
println!(
"{}",
self.colorize("✅ Cache cleared successfully", "green")
);
Ok(())
}
CacheAction::Cleanup => {
println!(
"{}",
self.colorize("🧹 Cleaning up expired cache entries...", "yellow")
);
self.converter.exchange_service.cleanup_cache().await;
println!("{}", self.colorize("✅ Cache cleanup completed", "green"));
Ok(())
}
}
}
async fn handle_compare(&self, amount: f64, from: &str, to_list: &str) -> Result<()> {
let currencies: Vec<&str> = to_list.split(',').map(|s| s.trim()).collect();
let spinner = self.create_spinner("Comparing across currencies...");
let mut requests = Vec::new();
for currency in ¤cies {
match ConversionRequest::from_components(amount, from, currency) {
Ok(req) => requests.push(req),
Err(e) => {
spinner.finish_and_clear();
self.print_error(&format!("Invalid currency {}: {}", currency, e));
return Err(e);
}
}
}
let results = self.converter.convert_batch(requests).await;
spinner.finish_and_clear();
println!(
"{}",
self.format_header(&format!(
"🔄 Currency Comparison ({} {})",
amount,
from.to_uppercase()
))
);
println!();
for (i, result) in results.iter().enumerate() {
match result {
Ok(conversion) => {
let rounded = conversion.result.round(2);
println!(
" {} {}",
self.colorize("→", "cyan"),
self.colorize(&format!("{}", rounded), "green")
);
}
Err(e) => {
println!(
" {} {} ({})",
self.colorize("→", "red"),
self.colorize(currencies[i], "red"),
self.colorize("failed", "red")
);
if self.verbose {
println!(" {}", self.colorize(&e.to_string(), "red"));
}
}
}
}
Ok(())
}
async fn handle_interactive(&self) -> Result<()> {
println!(
"{}",
self.format_header("🎯 Interactive Currency Converter")
);
println!("Type 'help' for commands or 'quit' to exit");
println!();
loop {
print!("{} ", self.colorize("mudra>", "cyan"));
use std::io::{self, Write};
io::stdout().flush().unwrap();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_err() {
break;
}
let input = input.trim();
if input.is_empty() {
continue;
}
match input {
"quit" | "exit" | "q" => {
println!("{}", self.colorize("Goodbye! 👋", "green"));
break;
}
"help" | "h" => {
self.print_interactive_help();
}
cmd => {
if let Err(e) = self.handle_interactive_command(cmd).await {
self.print_error(&e.to_string());
}
}
}
}
Ok(())
}
async fn handle_interactive_command(&self, input: &str) -> Result<()> {
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.is_empty() {
return Ok(());
}
if parts.len() == 3 {
if let Ok(amount) = parts[0].parse::<f64>() {
return self.handle_convert(amount, parts[1], parts[2], 2).await;
}
}
if parts.len() == 4 && parts[0] == "convert" {
if let Ok(amount) = parts[1].parse::<f64>() {
return self.handle_convert(amount, parts[2], parts[3], 2).await;
}
}
if parts.len() == 2 && parts[0] == "rates" {
return self.handle_rates(parts[1], None, Some(10)).await;
}
if parts.len() == 1 && parts[0] == "list" {
return self.handle_list(false, None).await;
}
Err(CurrencyError::conversion(format!(
"Invalid command: '{}'. Type 'help' for usage.",
input
)))
}
fn create_spinner(&self, message: &str) -> ProgressBar {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap(),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(Duration::from_millis(100));
pb
}
fn format_header(&self, title: &str) -> String {
if self.use_color {
title.bold().blue().to_string()
} else {
title.to_string()
}
}
fn colorize(&self, text: &str, color: &str) -> String {
if !self.use_color {
return text.to_string();
}
match color {
"red" => text.red().to_string(),
"green" => text.green().to_string(),
"yellow" => text.yellow().to_string(),
"blue" => text.blue().to_string(),
"cyan" => text.cyan().to_string(),
"white" => text.white().to_string(),
_ => text.to_string(),
}
}
fn print_error(&self, message: &str) {
println!(
"{} {}",
self.colorize("❌ Error:", "red"),
self.colorize(message, "red")
);
}
fn format_conversion_type(&self, conv_type: &ConversionType) -> String {
match conv_type {
ConversionType::SameCurrency => self.colorize("Same Currency", "green"),
ConversionType::Direct => self.colorize("Direct", "green"),
ConversionType::Cross { via_currency } => {
self.colorize(&format!("Cross (via {})", via_currency), "yellow")
}
}
}
fn format_timestamp(&self, timestamp: i64) -> String {
use chrono::DateTime;
DateTime::from_timestamp(timestamp, 0)
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
.unwrap_or_else(|| format!("{}", timestamp))
}
fn print_currencies_compact(&self, currencies: &[String]) {
const COLUMNS: usize = 6;
for chunk in currencies.chunks(COLUMNS) {
let line = chunk
.iter()
.map(|c| format!("{:>4}", self.colorize(c, "green")))
.collect::<Vec<_>>()
.join(" ");
println!(" {}", line);
}
}
fn print_currencies_extended(&self, currencies: &[String]) {
for currency in currencies {
println!(
" {} {}",
self.colorize("•", "cyan"),
self.colorize(currency, "green")
);
}
}
fn print_interactive_help(&self) {
println!();
println!("{}", self.format_header("📖 Interactive Commands"));
println!();
println!(
" {} {} Convert currency",
self.colorize("100 USD EUR", "green"),
self.colorize("→", "cyan")
);
println!(
" {} {} Convert currency",
self.colorize("convert 100 USD EUR", "green"),
self.colorize("→", "cyan")
);
println!(
" {} {} Show exchange rates",
self.colorize("rates USD", "green"),
self.colorize("→", "cyan")
);
println!(
" {} {} List currencies",
self.colorize("list", "green"),
self.colorize("→", "cyan")
);
println!(
" {} {} Show this help",
self.colorize("help", "green"),
self.colorize("→", "cyan")
);
println!(
" {} {} Exit",
self.colorize("quit", "green"),
self.colorize("→", "cyan")
);
println!();
}
}