mudra-cli 0.1.0

A robust, high-performance currency converter with caching and CLI interface
Documentation
//! Core conversion logic and types

use crate::{CurrencyError, Result};
use serde::{Deserialize, Serialize};
use std::fmt;

/// Represents a currency with validation
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Currency {
    code: String,
}

impl Currency {
    /// Create a new currency with validation
    pub fn new<S: Into<String>>(code: S) -> Result<Self> {
        let code = code.into().to_uppercase();

        if !crate::api::is_valid_currency_code(&code) {
            return Err(CurrencyError::invalid_currency(&code));
        }

        Ok(Currency { code })
    }

    /// Get the currency code
    pub fn code(&self) -> &str {
        &self.code
    }

    /// Check if this is the same currency as another
    pub fn is_same_as(&self, other: &Currency) -> bool {
        self.code == other.code
    }
}

impl fmt::Display for Currency {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.code)
    }
}

impl TryFrom<&str> for Currency {
    type Error = CurrencyError;

    fn try_from(code: &str) -> Result<Self> {
        Currency::new(code)
    }
}

impl TryFrom<String> for Currency {
    type Error = CurrencyError;

    fn try_from(code: String) -> Result<Self> {
        Currency::new(code)
    }
}

/// Represents a monetary amount with a specific currency
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Money {
    amount: f64,
    currency: Currency,
}

impl Money {
    /// Create a new Money instance
    pub fn new(amount: f64, currency: Currency) -> Result<Self> {
        Self::validate_amount(amount)?;
        Ok(Money { amount, currency })
    }

    /// Create Money from amount and currency code
    pub fn from_code<S: Into<String>>(amount: f64, currency_code: S) -> Result<Self> {
        let currency = Currency::new(currency_code)?;
        Self::new(amount, currency)
    }

    /// Get the amount
    pub fn amount(&self) -> f64 {
        self.amount
    }

    /// Get the currency
    pub fn currency(&self) -> &Currency {
        &self.currency
    }

    /// Round to specified decimal places
    pub fn round(&self, decimal_places: u32) -> Self {
        let multiplier = 10_f64.powi(decimal_places as i32);
        let rounded_amount = (self.amount * multiplier).round() / multiplier;

        Money {
            amount: rounded_amount,
            currency: self.currency.clone(),
        }
    }

    /// Validate amount for financial calculations
    fn validate_amount(amount: f64) -> Result<()> {
        if amount < 0.0 {
            return Err(CurrencyError::invalid_amount(amount));
        }
        if amount.is_nan() || amount.is_infinite() {
            return Err(CurrencyError::invalid_amount(amount));
        }
        // Reasonable upper limit for financial calculations
        if amount > 1_000_000_000_000.0 {
            return Err(CurrencyError::invalid_amount(amount));
        }
        Ok(())
    }
}

impl fmt::Display for Money {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.2} {}", self.amount, self.currency)
    }
}

/// Request for currency conversion
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversionRequest {
    /// Source money (amount + currency)
    pub from: Money,
    /// Target currency
    pub to: Currency,
}

impl ConversionRequest {
    /// Create a new conversion request
    pub fn new(from: Money, to: Currency) -> Self {
        ConversionRequest { from, to }
    }

    /// Create from individual components
    pub fn from_components(amount: f64, from_currency: &str, to_currency: &str) -> Result<Self> {
        let from = Money::from_code(amount, from_currency)?;
        let to = Currency::new(to_currency)?;
        Ok(ConversionRequest::new(from, to))
    }

    /// Check if this is a same-currency conversion
    pub fn is_same_currency(&self) -> bool {
        self.from.currency.is_same_as(&self.to)
    }
}

/// Result of a currency conversion
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversionResult {
    /// Original request
    pub request: ConversionRequest,
    /// Converted amount
    pub result: Money,
    /// Exchange rate used (from -> to)
    pub exchange_rate: f64,
    /// Timestamp of the conversion
    pub timestamp: i64,
    /// Whether this was a direct or cross-conversion
    pub conversion_type: ConversionType,
}

/// Type of conversion performed
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ConversionType {
    /// Same currency (no conversion needed)
    SameCurrency,
    /// Direct conversion (both currencies available in rates)
    Direct,
    /// Cross conversion via base currency
    Cross { via_currency: String },
}

impl ConversionResult {
    /// Create a new conversion result
    pub fn new(
        request: ConversionRequest,
        result: Money,
        exchange_rate: f64,
        conversion_type: ConversionType,
    ) -> Self {
        ConversionResult {
            request,
            result,
            exchange_rate,
            timestamp: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs() as i64,
            conversion_type,
        }
    }

    /// Get a formatted summary of the conversion
    pub fn summary(&self) -> String {
        match self.conversion_type {
            ConversionType::SameCurrency => {
                format!("{} (same currency)", self.request.from)
            }
            ConversionType::Direct => {
                format!(
                    "{}{} (rate: {:.6})",
                    self.request.from, self.result, self.exchange_rate
                )
            }
            ConversionType::Cross { ref via_currency } => {
                format!(
                    "{}{} via {} (rate: {:.6})",
                    self.request.from, self.result, via_currency, self.exchange_rate
                )
            }
        }
    }
}

impl fmt::Display for ConversionResult {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.summary())
    }
}

#[cfg(test)]
mod tests;