Skip to main content

mudra_cli/api/
types.rs

1//! Data types for API responses and requests
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Response from the exchange rate API for latest rates
7#[derive(Debug, Deserialize, Serialize, Clone)]
8pub struct ExchangeRateResponse {
9    /// Whether the request was successful
10    pub result: String,
11
12    /// Base currency for the rates
13    pub base_code: String,
14
15    /// Last update timestamp (Unix timestamp)
16    #[serde(rename = "time_last_update_unix")]
17    pub last_update: i64,
18
19    /// Next update timestamp (Unix timestamp)  
20    #[serde(rename = "time_next_update_unix")]
21    pub next_update: i64,
22
23    /// Exchange rates relative to the base currency
24    pub conversion_rates: HashMap<String, f64>,
25}
26
27/// Response for historical exchange rates
28#[derive(Debug, Deserialize, Serialize, Clone)]
29pub struct HistoricalRateResponse {
30    /// Whether the request was successful
31    pub result: String,
32
33    /// Base currency for the rates
34    pub base_code: String,
35
36    /// Date for these historical rates (YYYY-MM-DD)
37    pub year: i32,
38    pub month: i32,
39    pub day: i32,
40
41    /// Exchange rates for the specified date
42    pub conversion_rates: HashMap<String, f64>,
43}
44
45/// Response for a specific currency pair conversion
46#[derive(Debug, Deserialize, Serialize, Clone)]
47pub struct ConversionResponse {
48    /// Whether the request was successful
49    pub result: String,
50
51    /// Source currency code
52    pub base_code: String,
53
54    /// Target currency code
55    pub target_code: String,
56
57    /// Conversion rate from base to target
58    pub conversion_rate: f64,
59
60    /// The converted amount
61    pub conversion_result: f64,
62}
63
64/// Error response from the API
65#[derive(Debug, Deserialize, Serialize)]
66pub struct ApiErrorResponse {
67    /// Error type
68    pub result: String,
69
70    /// Error code
71    #[serde(rename = "error-type")]
72    pub error_type: String,
73
74    /// Additional info about the error
75    #[serde(default)]
76    pub extra_info: Option<String>,
77}
78
79/// Supported currencies response
80#[derive(Debug, Deserialize, Serialize)]
81pub struct SupportedCurrenciesResponse {
82    /// Whether the request was successful
83    pub result: String,
84
85    /// Map of currency code to currency name
86    pub supported_codes: Vec<(String, String)>,
87}
88
89/// Historical conversion request
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct HistoricalConversionRequest {
92    /// Amount to convert
93    pub amount: f64,
94    /// Source currency
95    pub from: String,
96    /// Target currency
97    pub to: String,
98    /// Date for historical rates (YYYY-MM-DD)
99    pub date: String,
100}
101
102impl ExchangeRateResponse {
103    /// Check if the API response indicates success
104    pub fn is_success(&self) -> bool {
105        self.result == "success"
106    }
107
108    /// Get the exchange rate for a specific currency
109    pub fn get_rate(&self, currency: &str) -> Option<f64> {
110        self.conversion_rates.get(currency).copied()
111    }
112
113    /// Get all available currency codes
114    pub fn available_currencies(&self) -> Vec<String> {
115        self.conversion_rates.keys().cloned().collect()
116    }
117
118    /// Get the number of supported currencies
119    pub fn currency_count(&self) -> usize {
120        self.conversion_rates.len()
121    }
122
123    /// Convert to historical response format
124    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    /// Check if the API response indicates success
149    pub fn is_success(&self) -> bool {
150        self.result == "success"
151    }
152
153    /// Get the exchange rate for a specific currency
154    pub fn get_rate(&self, currency: &str) -> Option<f64> {
155        self.conversion_rates.get(currency).copied()
156    }
157
158    /// Get the date as a formatted string
159    pub fn get_date_string(&self) -> String {
160        format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
161    }
162
163    /// Convert to standard exchange rate response
164    pub fn to_standard(&self) -> ExchangeRateResponse {
165        ExchangeRateResponse {
166            result: self.result.clone(),
167            base_code: self.base_code.clone(),
168            last_update: 0, // Historical data doesn't have update times
169            next_update: 0,
170            conversion_rates: self.conversion_rates.clone(),
171        }
172    }
173}
174
175impl ConversionResponse {
176    /// Check if the API response indicates success
177    pub fn is_success(&self) -> bool {
178        self.result == "success"
179    }
180}
181
182/// Validation for currency codes (ISO 4217 format)
183pub fn is_valid_currency_code(code: &str) -> bool {
184    // Basic validation: 3 uppercase letters
185    code.len() == 3 && code.chars().all(|c| c.is_ascii_uppercase())
186}
187
188/// Validate date format (YYYY-MM-DD)
189pub 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    // Check that all parts are numeric and in valid ranges
196    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
207/// Common currency codes for validation and suggestions
208pub 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
213/// Check if a currency code is commonly used
214pub fn is_common_currency(code: &str) -> bool {
215    COMMON_CURRENCIES.contains(&code)
216}