diff --git a/Cargo.lock b/Cargo.lock index 45271e5..cf19590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,6 +40,10 @@ name = "bnum" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f781dba93de3a5ef6dc5b17c9958b208f6f3f021623b360fb605ea51ce443f10" +dependencies = [ + "serde", + "serde-big-array", +] [[package]] name = "bumpalo" @@ -166,6 +170,7 @@ dependencies = [ "bnum", "num-integer", "num-traits", + "serde", ] [[package]] @@ -925,6 +930,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index 3a81672..a567df4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ categories = [ crate-type = ["cdylib", "rlib"] [dependencies] -fastnum = "0.7" +fastnum = { version = "0.7", features = ["serde"] } unicode-segmentation = "1.13" web-time = "1.1.0" reqwest = { version = "0.12", features = ["blocking", "json"] } diff --git a/src/currency.rs b/src/currency.rs index 1a6739d..339b94b 100644 --- a/src/currency.rs +++ b/src/currency.rs @@ -2,6 +2,8 @@ use crate::units::Unit; use fastnum::D128; +use fastnum::decimal::Context; +use serde::Deserialize; use std::collections::HashMap; use std::sync::RwLock; @@ -15,27 +17,47 @@ static CURRENCY_CACHE: RwLock>> = RwLock::new(None); /// The base currency for all exchange rates (EUR) pub const BASE_CURRENCY: Unit = Unit::EUR; -/// Initialize the currency cache by fetching rates from the API -pub fn initialize_currency_cache() -> Result<(), String> { - if CURRENCY_CACHE.read().unwrap().is_some() { - return Ok(()); // Already initialized - } +#[derive(Deserialize, Debug)] +struct CurrencyRate { + #[allow(dead_code)] + date: String, + base: String, + quote: String, + rate: serde_json::Number, +} - let rates = fetch_currency_rates()?; - let mut cache = HashMap::new(); +fn set_currency_cache(rates: Vec) -> Result<(), String> { + let mut cache = HashMap::with_capacity(rates.len() + 1); - // Store rates relative to EUR (base currency) - // EUR to EUR is 1 + // Add EUR as base cache.insert(BASE_CURRENCY, D128::from(1)); - for (quote_currency, rate) in rates { - cache.insert(quote_currency, rate); + for entry in rates { + if entry.base != "EUR" { + return Err("Exchange rate base currency must be EUR".to_string()); + } + if let Ok(quote_unit) = currency_code_to_unit(&entry.quote) { + let rate_str = entry.rate.to_string(); + let rate = D128::parse_str(&rate_str, Context::default()); + cache.insert(quote_unit, rate); + } } *CURRENCY_CACHE.write().unwrap() = Some(cache); Ok(()) } +#[cfg(not(target_arch = "wasm32"))] +pub fn initialize_currency_cache() -> Result<(), String> { + if CURRENCY_CACHE.read().unwrap().is_some() { + return Ok(()); // Already initialized + } + + let rates = fetch_currency_rates()?; + set_currency_cache(rates)?; + Ok(()) +} + /// Get the exchange rate from one currency to another /// Both currencies must be currency units pub fn get_exchange_rate(from: Unit, to: Unit) -> Result { @@ -44,6 +66,7 @@ pub fn get_exchange_rate(from: Unit, to: Unit) -> Result { } // Ensure cache is initialized + #[cfg(not(target_arch = "wasm32"))] initialize_currency_cache()?; let cache = CURRENCY_CACHE.read().unwrap(); @@ -64,48 +87,17 @@ pub fn get_exchange_rate(from: Unit, to: Unit) -> Result { /// Fetch currency rates from the Frankfurter API (native version) #[cfg(not(target_arch = "wasm32"))] -fn fetch_currency_rates() -> Result, String> { +fn fetch_currency_rates() -> Result, String> { use reqwest::blocking::get; - use serde::Deserialize; - - #[derive(Deserialize)] - struct RateEntry { - #[allow(dead_code)] - date: String, - base: String, - quote: String, - rate: f64, - } let url = "https://api.frankfurter.dev/v2/rates?base=EUR"; let response = get(url).map_err(|e| format!("Failed to fetch currency rates: {}", e))?; - let rates: Vec = response + let rates: Vec = response .json() - .map_err(|e| format!("Failed to parse currency rates: {}", e))?; - - let mut result = Vec::new(); - for entry in rates { - if entry.base == "EUR" { - // Only process currencies that we support - if let Ok(quote_unit) = currency_code_to_unit(&entry.quote) { - let rate = D128::from_f64(entry.rate); - result.push((quote_unit, rate)); - } - } - } + .map_err(|e| format!("Failed to parse currency rates: {:?}", e))?; - Ok(result) -} - -/// Fetch currency rates for WASM target -/// Returns empty - the web app should fetch real rates -/// and call init_currency_cache_with_json -#[cfg(target_arch = "wasm32")] -fn fetch_currency_rates() -> Result, String> { - // Return empty for WASM - the web app must call init_currency_cache_with_json - // with rates fetched via JavaScript - Ok(Vec::new()) + Ok(rates) } /// Convert currency code to Unit enum @@ -131,35 +123,11 @@ fn currency_code_to_unit(code: &str) -> Result { #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn init_currency_cache_with_json(rates_json: &str) -> Result<(), JsValue> { - use serde::Deserialize; - - #[derive(Deserialize)] - struct RateEntry { - #[allow(dead_code)] - date: Option, - base: String, - quote: String, - rate: f64, - } - - // Parse the JSON array of rate entries - let rates: Vec = serde_json::from_str(rates_json) + let rates: Vec = serde_json::from_str(rates_json) .map_err(|e| JsValue::from_str(&format!("Failed to parse JSON: {}", e)))?; - let mut cache = HashMap::new(); - - // Add EUR as base - cache.insert(BASE_CURRENCY, D128::from(1)); - - // Parse individual rates - only accept entries where base is EUR - for entry in rates { - if entry.base == "EUR" { - if let Ok(unit) = currency_code_to_unit(&entry.quote) { - cache.insert(unit, D128::from_f64(entry.rate)); - } - } - } + set_currency_cache(rates) + .map_err(|e| JsValue::from_str(&format!("Failed to parse JSON: {}", e)))?; - *CURRENCY_CACHE.write().unwrap() = Some(cache); Ok(()) } diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 9bd60e2..69502d4 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -17,16 +17,18 @@ try { const response = await fetch("https://api.frankfurter.dev/v2/rates?base=EUR"); const ratesJson = await response.text(); - if (mod.init_currency_cache_with_json) { - mod.init_currency_cache_with_json(ratesJson); + if (typeof mod.init_currency_cache_with_json !== 'function') { + output = 'Error: Missing currency init function' } + mod.init_currency_cache_with_json(ratesJson); } catch (e) { console.error("Failed to fetch currency rates:", e); + output = "Error: Failed to fetch currency rates:", e } }); - function wasm_eval(input: string) { - if (!cpc || input.trim().length === 0) { + function wasm_eval() { + if (input === '' || !cpc || input.trim().length === 0) { return ""; } try { @@ -37,7 +39,7 @@ } let input = $state(""); - let output = $derived(wasm_eval(input)); + let output = $derived(wasm_eval()); let calc_history = new PersistedState< { id: number; in: string; out: string }[] >("calc_history", []);