1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Deserialize, Serialize, Clone)]
8pub struct ExchangeRateResponse {
9 pub result: String,
11
12 pub base_code: String,
14
15 #[serde(rename = "time_last_update_unix")]
17 pub last_update: i64,
18
19 #[serde(rename = "time_next_update_unix")]
21 pub next_update: i64,
22
23 pub conversion_rates: HashMap<String, f64>,
25}
26
27#[derive(Debug, Deserialize, Serialize, Clone)]
29pub struct HistoricalRateResponse {
30 pub result: String,
32
33 pub base_code: String,
35
36 pub year: i32,
38 pub month: i32,
39 pub day: i32,
40
41 pub conversion_rates: HashMap<String, f64>,
43}
44
45#[derive(Debug, Deserialize, Serialize, Clone)]
47pub struct ConversionResponse {
48 pub result: String,
50
51 pub base_code: String,
53
54 pub target_code: String,
56
57 pub conversion_rate: f64,
59
60 pub conversion_result: f64,
62}
63
64#[derive(Debug, Deserialize, Serialize)]
66pub struct ApiErrorResponse {
67 pub result: String,
69
70 #[serde(rename = "error-type")]
72 pub error_type: String,
73
74 #[serde(default)]
76 pub extra_info: Option<String>,
77}
78
79#[derive(Debug, Deserialize, Serialize)]
81pub struct SupportedCurrenciesResponse {
82 pub result: String,
84
85 pub supported_codes: Vec<(String, String)>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct HistoricalConversionRequest {
92 pub amount: f64,
94 pub from: String,
96 pub to: String,
98 pub date: String,
100}
101
102impl ExchangeRateResponse {
103 pub fn is_success(&self) -> bool {
105 self.result == "success"
106 }
107
108 pub fn get_rate(&self, currency: &str) -> Option<f64> {
110 self.conversion_rates.get(currency).copied()
111 }
112
113 pub fn available_currencies(&self) -> Vec<String> {
115 self.conversion_rates.keys().cloned().collect()
116 }
117
118 pub fn currency_count(&self) -> usize {
120 self.conversion_rates.len()
121 }
122
123 pub fn to_historical(&self, date: &str) -> HistoricalRateResponse {
125 let parts: Vec<&str> = date.split('-').collect();
126 let (year, month, day) = if parts.len() == 3 {
127 (
128 parts[0].parse().unwrap_or(2024),
129 parts[1].parse().unwrap_or(1),
130 parts[2].parse().unwrap_or(1),
131 )
132 } else {
133 (2024, 1, 1)
134 };
135
136 HistoricalRateResponse {
137 result: self.result.clone(),
138 base_code: self.base_code.clone(),
139 year,
140 month,
141 day,
142 conversion_rates: self.conversion_rates.clone(),
143 }
144 }
145}
146
147impl HistoricalRateResponse {
148 pub fn is_success(&self) -> bool {
150 self.result == "success"
151 }
152
153 pub fn get_rate(&self, currency: &str) -> Option<f64> {
155 self.conversion_rates.get(currency).copied()
156 }
157
158 pub fn get_date_string(&self) -> String {
160 format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
161 }
162
163 pub fn to_standard(&self) -> ExchangeRateResponse {
165 ExchangeRateResponse {
166 result: self.result.clone(),
167 base_code: self.base_code.clone(),
168 last_update: 0, next_update: 0,
170 conversion_rates: self.conversion_rates.clone(),
171 }
172 }
173}
174
175impl ConversionResponse {
176 pub fn is_success(&self) -> bool {
178 self.result == "success"
179 }
180}
181
182pub fn is_valid_currency_code(code: &str) -> bool {
184 code.len() == 3 && code.chars().all(|c| c.is_ascii_uppercase())
186}
187
188pub fn is_valid_date_format(date: &str) -> bool {
190 let parts: Vec<&str> = date.split('-').collect();
191 if parts.len() != 3 {
192 return false;
193 }
194
195 if let (Ok(year), Ok(month), Ok(day)) = (
197 parts[0].parse::<i32>(),
198 parts[1].parse::<i32>(),
199 parts[2].parse::<i32>(),
200 ) {
201 year >= 1999 && year <= 2030 && month >= 1 && month <= 12 && day >= 1 && day <= 31
202 } else {
203 false
204 }
205}
206
207pub const COMMON_CURRENCIES: &[&str] = &[
209 "USD", "EUR", "GBP", "JPY", "AUD", "CAD", "CHF", "CNY", "SEK", "NZD", "MXN", "SGD", "HKD",
210 "NOK", "KRW", "TRY", "RUB", "INR", "BRL", "ZAR",
211];
212
213pub fn is_common_currency(code: &str) -> bool {
215 COMMON_CURRENCIES.contains(&code)
216}