use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ExchangeRateResponse {
pub result: String,
pub base_code: String,
#[serde(rename = "time_last_update_unix")]
pub last_update: i64,
#[serde(rename = "time_next_update_unix")]
pub next_update: i64,
pub conversion_rates: HashMap<String, f64>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct HistoricalRateResponse {
pub result: String,
pub base_code: String,
pub year: i32,
pub month: i32,
pub day: i32,
pub conversion_rates: HashMap<String, f64>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ConversionResponse {
pub result: String,
pub base_code: String,
pub target_code: String,
pub conversion_rate: f64,
pub conversion_result: f64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ApiErrorResponse {
pub result: String,
#[serde(rename = "error-type")]
pub error_type: String,
#[serde(default)]
pub extra_info: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SupportedCurrenciesResponse {
pub result: String,
pub supported_codes: Vec<(String, String)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoricalConversionRequest {
pub amount: f64,
pub from: String,
pub to: String,
pub date: String,
}
impl ExchangeRateResponse {
pub fn is_success(&self) -> bool {
self.result == "success"
}
pub fn get_rate(&self, currency: &str) -> Option<f64> {
self.conversion_rates.get(currency).copied()
}
pub fn available_currencies(&self) -> Vec<String> {
self.conversion_rates.keys().cloned().collect()
}
pub fn currency_count(&self) -> usize {
self.conversion_rates.len()
}
pub fn to_historical(&self, date: &str) -> HistoricalRateResponse {
let parts: Vec<&str> = date.split('-').collect();
let (year, month, day) = if parts.len() == 3 {
(
parts[0].parse().unwrap_or(2024),
parts[1].parse().unwrap_or(1),
parts[2].parse().unwrap_or(1),
)
} else {
(2024, 1, 1)
};
HistoricalRateResponse {
result: self.result.clone(),
base_code: self.base_code.clone(),
year,
month,
day,
conversion_rates: self.conversion_rates.clone(),
}
}
}
impl HistoricalRateResponse {
pub fn is_success(&self) -> bool {
self.result == "success"
}
pub fn get_rate(&self, currency: &str) -> Option<f64> {
self.conversion_rates.get(currency).copied()
}
pub fn get_date_string(&self) -> String {
format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
pub fn to_standard(&self) -> ExchangeRateResponse {
ExchangeRateResponse {
result: self.result.clone(),
base_code: self.base_code.clone(),
last_update: 0, next_update: 0,
conversion_rates: self.conversion_rates.clone(),
}
}
}
impl ConversionResponse {
pub fn is_success(&self) -> bool {
self.result == "success"
}
}
pub fn is_valid_currency_code(code: &str) -> bool {
code.len() == 3 && code.chars().all(|c| c.is_ascii_uppercase())
}
pub fn is_valid_date_format(date: &str) -> bool {
let parts: Vec<&str> = date.split('-').collect();
if parts.len() != 3 {
return false;
}
if let (Ok(year), Ok(month), Ok(day)) = (
parts[0].parse::<i32>(),
parts[1].parse::<i32>(),
parts[2].parse::<i32>(),
) {
year >= 1999 && year <= 2030 && month >= 1 && month <= 12 && day >= 1 && day <= 31
} else {
false
}
}
pub const COMMON_CURRENCIES: &[&str] = &[
"USD", "EUR", "GBP", "JPY", "AUD", "CAD", "CHF", "CNY", "SEK", "NZD", "MXN", "SGD", "HKD",
"NOK", "KRW", "TRY", "RUB", "INR", "BRL", "ZAR",
];
pub fn is_common_currency(code: &str) -> bool {
COMMON_CURRENCIES.contains(&code)
}