mudra-cli 0.1.0

A robust, high-performance currency converter with caching and CLI interface
Documentation
//! Data types for API responses and requests

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Response from the exchange rate API for latest rates
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ExchangeRateResponse {
    /// Whether the request was successful
    pub result: String,

    /// Base currency for the rates
    pub base_code: String,

    /// Last update timestamp (Unix timestamp)
    #[serde(rename = "time_last_update_unix")]
    pub last_update: i64,

    /// Next update timestamp (Unix timestamp)  
    #[serde(rename = "time_next_update_unix")]
    pub next_update: i64,

    /// Exchange rates relative to the base currency
    pub conversion_rates: HashMap<String, f64>,
}

/// Response for historical exchange rates
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct HistoricalRateResponse {
    /// Whether the request was successful
    pub result: String,

    /// Base currency for the rates
    pub base_code: String,

    /// Date for these historical rates (YYYY-MM-DD)
    pub year: i32,
    pub month: i32,
    pub day: i32,

    /// Exchange rates for the specified date
    pub conversion_rates: HashMap<String, f64>,
}

/// Response for a specific currency pair conversion
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ConversionResponse {
    /// Whether the request was successful
    pub result: String,

    /// Source currency code
    pub base_code: String,

    /// Target currency code
    pub target_code: String,

    /// Conversion rate from base to target
    pub conversion_rate: f64,

    /// The converted amount
    pub conversion_result: f64,
}

/// Error response from the API
#[derive(Debug, Deserialize, Serialize)]
pub struct ApiErrorResponse {
    /// Error type
    pub result: String,

    /// Error code
    #[serde(rename = "error-type")]
    pub error_type: String,

    /// Additional info about the error
    #[serde(default)]
    pub extra_info: Option<String>,
}

/// Supported currencies response
#[derive(Debug, Deserialize, Serialize)]
pub struct SupportedCurrenciesResponse {
    /// Whether the request was successful
    pub result: String,

    /// Map of currency code to currency name
    pub supported_codes: Vec<(String, String)>,
}

/// Historical conversion request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoricalConversionRequest {
    /// Amount to convert
    pub amount: f64,
    /// Source currency
    pub from: String,
    /// Target currency
    pub to: String,
    /// Date for historical rates (YYYY-MM-DD)
    pub date: String,
}

impl ExchangeRateResponse {
    /// Check if the API response indicates success
    pub fn is_success(&self) -> bool {
        self.result == "success"
    }

    /// Get the exchange rate for a specific currency
    pub fn get_rate(&self, currency: &str) -> Option<f64> {
        self.conversion_rates.get(currency).copied()
    }

    /// Get all available currency codes
    pub fn available_currencies(&self) -> Vec<String> {
        self.conversion_rates.keys().cloned().collect()
    }

    /// Get the number of supported currencies
    pub fn currency_count(&self) -> usize {
        self.conversion_rates.len()
    }

    /// Convert to historical response format
    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 {
    /// Check if the API response indicates success
    pub fn is_success(&self) -> bool {
        self.result == "success"
    }

    /// Get the exchange rate for a specific currency
    pub fn get_rate(&self, currency: &str) -> Option<f64> {
        self.conversion_rates.get(currency).copied()
    }

    /// Get the date as a formatted string
    pub fn get_date_string(&self) -> String {
        format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
    }

    /// Convert to standard exchange rate response
    pub fn to_standard(&self) -> ExchangeRateResponse {
        ExchangeRateResponse {
            result: self.result.clone(),
            base_code: self.base_code.clone(),
            last_update: 0, // Historical data doesn't have update times
            next_update: 0,
            conversion_rates: self.conversion_rates.clone(),
        }
    }
}

impl ConversionResponse {
    /// Check if the API response indicates success
    pub fn is_success(&self) -> bool {
        self.result == "success"
    }
}

/// Validation for currency codes (ISO 4217 format)
pub fn is_valid_currency_code(code: &str) -> bool {
    // Basic validation: 3 uppercase letters
    code.len() == 3 && code.chars().all(|c| c.is_ascii_uppercase())
}

/// Validate date format (YYYY-MM-DD)
pub fn is_valid_date_format(date: &str) -> bool {
    let parts: Vec<&str> = date.split('-').collect();
    if parts.len() != 3 {
        return false;
    }

    // Check that all parts are numeric and in valid ranges
    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
    }
}

/// Common currency codes for validation and suggestions
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",
];

/// Check if a currency code is commonly used
pub fn is_common_currency(code: &str) -> bool {
    COMMON_CURRENCIES.contains(&code)
}