diff --git a/Cargo.lock b/Cargo.lock index 256ba4c..10c38c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1756,7 +1756,6 @@ dependencies = [ "rustyline", "serde", "serde_derive", - "serde_json", "similar-asserts", "tempfile", "tiny_http", @@ -1796,7 +1795,6 @@ dependencies = [ "rink-sandbox", "serde", "serde_derive", - "serde_json", "tokio", "toml 0.8.12", "ubyte", @@ -1813,7 +1811,6 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_derive", - "serde_json", "wasm-bindgen", "wasm-bindgen-test", "wee_alloc", diff --git a/Cargo.toml b/Cargo.toml index ebf7e5e..5a5fb28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,6 @@ members = ["core", "rink-js", "sandbox", "cli", "irc"] default-members = ["cli"] [profile.release] -strip = true +strip = "debuginfo" opt-level = "z" lto = true diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b66b3f5..93cb582 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -19,7 +19,6 @@ clap = "4.5" dirs = "4" curl = "0.4.46" chrono = { version = "0.4.19", default-features = false } -serde_json = "1" toml = "0.5" serde_derive = "1" serde = { version = "1", default-features = false } @@ -35,6 +34,7 @@ ubyte = { version = "0.10.3", features = ["serde"] } [dependencies.rink-core] version = "0.8" path = "../core" +features = [ "serde_json" ] [dependencies.rink-sandbox] version = "0.6" diff --git a/cli/src/config.rs b/cli/src/config.rs index 836aeb9..96a5adb 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -10,7 +10,7 @@ use nu_ansi_term::{Color, Style}; use rink_core::output::fmt::FmtToken; use rink_core::parsing::datetime; use rink_core::Context; -use rink_core::{ast, loader::gnu_units, CURRENCY_FILE, DATES_FILE, DEFAULT_FILE}; +use rink_core::{loader::gnu_units, CURRENCY_FILE, DATES_FILE, DEFAULT_FILE}; use serde_derive::{Deserialize, Serialize}; use std::env; use std::ffi::OsString; @@ -279,7 +279,7 @@ pub(crate) fn force_refresh_currency(config: &Currency) -> Result { )) } -fn load_live_currency(config: &Currency) -> Result { +fn load_live_currency(config: &Currency) -> Result { let duration = if config.fetch_on_startup { Some(config.cache_duration) } else { @@ -287,7 +287,7 @@ fn load_live_currency(config: &Currency) -> Result { }; let file = cached("currency.json", &config.endpoint, duration, config.timeout)?; let contents = file_to_string(file)?; - serde_json::from_str(&contents).wrap_err("Invalid JSON") + Ok(contents) } fn try_load_currency(config: &Currency, ctx: &mut Context, search_path: &[PathBuf]) -> Result<()> { @@ -295,15 +295,9 @@ fn try_load_currency(config: &Currency, ctx: &mut Context, search_path: &[PathBu .into_iter() .next() .unwrap(); - - let mut base_defs = gnu_units::parse_str(&base); - let mut live_defs = load_live_currency(config)?; - - let mut defs = vec![]; - defs.append(&mut base_defs.defs); - defs.append(&mut live_defs.defs); - ctx.load(ast::Defs { defs }).map_err(|err| eyre!(err))?; - + let live_defs = load_live_currency(config)?; + ctx.load_currency(&live_defs, &base) + .map_err(|err| eyre!("{err}"))?; Ok(()) } @@ -506,7 +500,9 @@ mod tests { Duration::from_millis(5), ); let result = result.expect_err("this should always fail"); - assert_eq!(result.to_string(), "[28] Timeout was reached (Operation timed out after 5 milliseconds with 0 bytes received)"); + let result = result.to_string(); + assert!(result.starts_with("[28] Timeout was reached (Operation timed out after ")); + assert!(result.ends_with(" milliseconds with 0 bytes received)")); thread_handle.join().unwrap(); drop(server); } @@ -547,7 +543,7 @@ mod tests { let thread_handle = std::thread::spawn(move || { let request = server2.recv().expect("the request should not fail"); assert_eq!(request.url(), "/data/currency.json"); - let mut data = b"{}".to_owned(); + let mut data = include_bytes!("../../core/tests/currency.snapshot.json").to_owned(); let cursor = std::io::Cursor::new(&mut data); request .respond(Response::new(StatusCode(200), vec![], cursor, None, None)) @@ -563,7 +559,10 @@ mod tests { result .read_to_string(&mut string) .expect("the file should exist"); - assert_eq!(string, "{}"); + assert_eq!( + string, + include_str!("../../core/tests/currency.snapshot.json") + ); thread_handle.join().unwrap(); drop(server); } @@ -584,7 +583,7 @@ mod tests { let thread_handle = std::thread::spawn(move || { let request = server2.recv().expect("the request should not fail"); assert_eq!(request.url(), "/data/currency.json"); - let mut data = b"{}".to_owned(); + let mut data = include_bytes!("../../core/tests/currency.snapshot.json").to_owned(); let cursor = std::io::Cursor::new(&mut data); request .respond(Response::new(StatusCode(200), vec![], cursor, None, None)) @@ -592,7 +591,7 @@ mod tests { }); let result = super::force_refresh_currency(&config); let result = result.expect("this should succeed"); - assert!(result.starts_with("Fetched 2 byte currency file after ")); + assert!(result.starts_with("Fetched 6599 byte currency file after ")); thread_handle.join().unwrap(); drop(server); } diff --git a/core/Cargo.toml b/core/Cargo.toml index 58fb3b9..b5d5672 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -12,6 +12,7 @@ edition = "2018" [features] default = ["chrono-humanize"] bundle-files = [] +serde_json = ["dep:serde_json"] [dependencies] num-bigint = { version = "0.4", features = ["serde"] } @@ -22,6 +23,7 @@ strsim = "0.10.0" chrono-tz = { version = "0.5.2", default-features = false } chrono-humanize = { version = "0.1.2", optional = true } serde = { version = "1", features = ["rc"], default-features = false } +serde_json = { version = "1", optional = true } serde_derive = "1" indexmap = "2" diff --git a/core/src/ast/mod.rs b/core/src/ast/mod.rs index 8ae12b3..ed342f2 100644 --- a/core/src/ast/mod.rs +++ b/core/src/ast/mod.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +//! Abstract syntax tree for rink's query language. + use crate::output::Digits; use crate::types::Numeric; use chrono_tz::Tz; diff --git a/core/src/commands/mod.rs b/core/src/commands/mod.rs index d612c2e..cbbfe5c 100644 --- a/core/src/commands/mod.rs +++ b/core/src/commands/mod.rs @@ -2,6 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +//! Provides direct access to commands that can be run in rink, like +//! [search()] and [factorize()]. + mod factorize; mod search; diff --git a/core/src/helpers.rs b/core/src/helpers.rs index b112279..d7c0dfe 100644 --- a/core/src/helpers.rs +++ b/core/src/helpers.rs @@ -3,27 +3,50 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::{ - loader::gnu_units, output::{QueryError, QueryReply}, parsing::text_query, Context, }; +/// The default `definitions.units` file that contains all of the base +/// units, units, prefixes, quantities, and substances. +/// +/// This will be Some if the `bundle-files` feature is enabled, +/// otherwise it will be None. #[cfg(feature = "bundle-files")] pub static DEFAULT_FILE: Option<&'static str> = Some(include_str!("../definitions.units")); #[cfg(not(feature = "bundle-files"))] pub static DEFAULT_FILE: Option<&'static str> = None; +/// The default `datepatterns.txt` file that contains patterns that rink +/// uses for parsing datetimes. +/// +/// This will be Some if the `bundle-files` feature is enabled, +/// otherwise it will be None. #[cfg(feature = "bundle-files")] pub static DATES_FILE: Option<&'static str> = Some(include_str!("../datepatterns.txt")); #[cfg(not(feature = "bundle-files"))] pub static DATES_FILE: Option<&'static str> = None; +/// The default `currenty.units` file that contains currency information +/// that changes rarely. It's used together with live currency data +/// to add currency support to rink. +/// +/// This will be Some if the `bundle-files` feature is enabled, +/// otherwise it will be None. #[cfg(feature = "bundle-files")] pub static CURRENCY_FILE: Option<&'static str> = Some(include_str!("../currency.units")); #[cfg(not(feature = "bundle-files"))] pub static CURRENCY_FILE: Option<&'static str> = None; +/// Helper function that updates the `now` to the current time, parses +/// the query, evaluates it, and updates the `previous_result` field +/// that's used to return the previous query when using `ans`. +/// +/// ## Panics +/// +/// Panics on platforms where fetching the current time is not possible, +/// such as WASM. pub fn eval(ctx: &mut Context, line: &str) -> Result { ctx.update_time(); let mut iter = text_query::TokenIterator::new(line.trim()).peekable(); @@ -39,7 +62,7 @@ pub fn eval(ctx: &mut Context, line: &str) -> Result { Ok(res) } -/// A version of eval() that converts results and errors into strings. +/// A version of eval() that converts results and errors into plain-text strings. pub fn one_line(ctx: &mut Context, line: &str) -> Result { eval(ctx, line) .as_ref() @@ -54,20 +77,16 @@ pub fn simple_context() -> Result { let message = "bundle-files feature not enabled, cannot create simple context."; let units = DEFAULT_FILE.ok_or(message.to_owned())?; - let mut iter = gnu_units::TokenIterator::new(&*units).peekable(); - let units = gnu_units::parse(&mut iter); - let dates = DATES_FILE.ok_or(message.to_owned())?; - let dates = crate::parsing::datetime::parse_datefile(dates); let mut ctx = Context::new(); - ctx.load(units)?; - ctx.load_dates(dates); + ctx.load_definitions(units)?; + ctx.load_date_file(dates); Ok(ctx) } -// Returns `env!("CARGO_PKG_VERSION")`, a string in `x.y.z` format. +/// Returns `env!("CARGO_PKG_VERSION")`, a string in `x.y.z` format. pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") } diff --git a/core/src/lib.rs b/core/src/lib.rs index fb30503..3e6f03d 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -2,32 +2,100 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -/*! The primary interface of this library is meant to expose a very -simple command-reply model for frontends, and to allow gradual -addition of more advanced functionality. For now, only basic -functionality exists. - -Using Rink as a library for uses other than simple unit conversion -tools is not currently well supported, and if you wish to do so, -please make issues for any problems you have. - -There are currently a number of hardcoded `println!`s and `unwrap()`s -because most of this code was written in a day without much thought -towards making it into a library. - -To use the library, check how the CLI tool does it. To get additional -features like currency and BTC you'll need to fetch those files -yourself and add them into the Context. - -## Example - -```rust -use rink_core::*; - -let mut ctx = simple_context().unwrap(); -println!("{}", one_line(&mut ctx, "kWh / year -> W").unwrap()); -``` -*/ +//! Rink is a small language for calculations and unit conversions. +//! It is available as a CLI, a web interface, an IRC client. +//! `rink_core` is the library that the frontends use. +//! +//! The API is designed to let you start simple and then progressively +//! add more features. +//! +//! Rink is designed to be used interactively, with the user typing a +//! query and then seeing the result. It's common for this to be a +//! session, so the previous query can be referenced with `ans`, and +//! some form of history is available using up/down arrows. +//! +//! Using rink for purposes other than this is out of scope, but may be +//! possible anyway depending on what you're trying to do. +//! +//! ## Example +//! +//! Minimal implementation. +//! +//! ```rust +//! # fn main() -> Result<(), String> { +//! // Create a context. This is expensive (30+ ms), so do it once at +//! // startup and keep it around. +//! let mut ctx = rink_core::simple_context()?; +//! // `one_line` is a helper function that parses a query, evaluates +//! // it, then converts the result into a plain text string. +//! println!("{}", rink_core::one_line(&mut ctx, "kWh / year -> W")?); +//! // Prints: approx. 0.1140795 watt (power) +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Currency fetching +//! +//! The first step to adding currency fetching is to add code to +//! download this file: +//! +//! +//! +//! You can use any http library, such as `curl` or `reqwest`. The file +//! updates about once an hour. Please make sure to set an accurate +//! user-agent when fetching it. +//! +//! ```rust +//! # fn fetch_from_http(_url: &str) -> String { include_str!("../tests/currency.snapshot.json").to_owned() } +//! # fn main() -> Result<(), String> { +//! # let mut ctx = rink_core::simple_context()?; +//! let live_data: String = fetch_from_http("https://rinkcalc.app/data/currency.json"); +//! // CURRENCY_FILE requires that the `bundle-features` feature is +//! // enabled. Otherwise, you'll need to install and load this file +//! // yourself. +//! let base_defs = rink_core::CURRENCY_FILE.expect("bundle-files feature to be enabled"); +//! ctx.load_currency(&live_data, base_defs)?; +//! +//! println!("{}", rink_core::one_line(&mut ctx, "USD").unwrap()); +//! // Definition: USD = (1 / 1.0843) EUR = approx. 922.2539 millieuro (money; EUR). +//! // Sourced from European Central Bank. Current as of 2024-05-27. +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Markup +//! +//! To add color highlighting, or other forms of rich markup such as +//! links or superscripts, you can use [eval] instead of [one_line] and +//! then call [output::fmt::TokenFmt::to_spans] on the result. This +//! returns a tree of spans, each of which has a formatting hint +//! attached to it. See [output::fmt::Span] and [output::fmt::FmtToken]. +//! +//! ```rust +//! use rink_core::output::fmt::{TokenFmt, Span, FmtToken}; +//! # let mut ctx = rink_core::simple_context().unwrap(); +//! let result = rink_core::eval(&mut ctx, "meter"); +//! // converts both the Ok and Err cases to spans +//! let spans = result.to_spans(); +//! +//! fn write_xml(out: &mut String, spans: &[Span]) { +//! for span in spans { +//! match span { +//! Span::Content {text, token: FmtToken::DocString} => { +//! out.push_str(""); +//! out.push_str(&text); +//! out.push_str(""); +//! } +//! Span::Content {text, ..} => out.push_str(&text), +//! Span::Child(child) => write_xml(out, &child.to_spans()), +//! } +//! } +//! } +//! +//! let mut out = String::new(); +//! write_xml(&mut out, &spans); +//! println!("{}", out); +//! ``` // False positives, or make code harder to understand. #![allow(clippy::cognitive_complexity)] diff --git a/core/src/loader/context.rs b/core/src/loader/context.rs index d15b369..aadc42d 100644 --- a/core/src/loader/context.rs +++ b/core/src/loader/context.rs @@ -65,6 +65,11 @@ impl Context { self.registry.datepatterns.append(&mut dates) } + pub fn load_date_file(&mut self, file: &str) { + let dates = crate::parsing::datetime::parse_datefile(file); + self.load_dates(dates) + } + /// Given a unit name, returns its value if it exists. Supports SI /// prefixes, plurals, bare dimensions like length, and quantities. pub fn lookup(&self, name: &str) -> Option { @@ -196,6 +201,33 @@ impl Context { } } + // Takes the string definition.units file, parses it, and loads it. + pub fn load_definitions(&mut self, content: &str) -> Result<(), String> { + let defs = crate::loader::gnu_units::parse_str(&content); + self.load(defs) + } + + // Takes the currency JSON and the currency.units file, parses both, + // and loads them. + // + // The latest live_data string can be obtained by making a web + // request to: https://rinkcalc.app/data/currency.json + // + // The currency.unit file exists at rink_core::CURRENCY_FILE when + // the `bundle-files` feature is enabled. Otherwise, it needs to be + // installed on the system somewhere and loaded. + #[cfg(feature = "serde_json")] + pub fn load_currency(&mut self, live_data: &str, currency_units: &str) -> Result<(), String> { + let mut base_defs = crate::loader::gnu_units::parse_str(currency_units); + let mut live_defs: Vec = + serde_json::from_str(&live_data).map_err(|err| format!("{}", err))?; + + let mut defs = vec![]; + defs.append(&mut base_defs.defs); + defs.append(&mut live_defs); + self.load(crate::ast::Defs { defs }) + } + /// Evaluates an expression to compute its value, *excluding* `->` /// conversions. pub fn eval(&self, expr: &Expr) -> Result { diff --git a/core/src/loader/mod.rs b/core/src/loader/mod.rs index 5a79864..159ff3a 100644 --- a/core/src/loader/mod.rs +++ b/core/src/loader/mod.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +//! [Context], [Registry], and the [definitions file parser][gnu_units] + mod context; pub mod gnu_units; mod load; diff --git a/core/src/output/mod.rs b/core/src/output/mod.rs index 2c8a1dc..5aed496 100644 --- a/core/src/output/mod.rs +++ b/core/src/output/mod.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +//! Return types from evaluating a query, starting from [QueryReply] and [QueryError] + mod doc_string; pub mod fmt; mod number_parts; diff --git a/core/src/output/reply.rs b/core/src/output/reply.rs index 7628a9f..4a088a1 100644 --- a/core/src/output/reply.rs +++ b/core/src/output/reply.rs @@ -640,3 +640,12 @@ impl<'a> TokenFmt<'a> for NotFoundError { tokens } } + +impl<'a> TokenFmt<'a> for Result { + fn to_spans(&'a self) -> Vec> { + match self { + Ok(res) => res.to_spans(), + Err(err) => err.to_spans(), + } + } +} diff --git a/core/src/parsing/mod.rs b/core/src/parsing/mod.rs index 97f44fa..f3f6df3 100644 --- a/core/src/parsing/mod.rs +++ b/core/src/parsing/mod.rs @@ -2,6 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +//! Parsers for [rink's query language][text_query], [molecular +//! formulas][formula::substance_from_formula], and [datetimes][datetime]. + pub mod datetime; pub mod formula; pub mod text_query; diff --git a/core/src/runtime/mod.rs b/core/src/runtime/mod.rs index a03bb50..c7734ad 100644 --- a/core/src/runtime/mod.rs +++ b/core/src/runtime/mod.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +//! [Value] stores all types that rink is capable of processing + mod eval; mod substance; mod value; diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index 4d65c46..b3a26a9 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +//! Basic types that rink uses, such as [arbitrary-precision rationals][BigRat] + mod base_unit; mod bigint; mod bigrat; diff --git a/core/tests/currency.snapshot.json b/core/tests/currency.snapshot.json new file mode 100644 index 0000000..f014ebc --- /dev/null +++ b/core/tests/currency.snapshot.json @@ -0,0 +1,269 @@ +[ + { + "name": "BTC", + "doc": null, + "category": "currencies", + "type": "unit", + "expr": "price of bitcoin" + }, + { + "name": "bitcoin", + "doc": "Properties of the global Bitcoin network. Sourced from . Current as of Sun, 02 Jun 2024 21:38:26 GMT", + "category": "currencies", + "type": "substance", + "symbol": null, + "properties": [ + { + "name": "price", + "doc": "Current market price of 1 BTC.", + "category": "currencies", + "inputName": "bitcoin", + "input": "1", + "outputName": "bitcoin", + "output": "67710.94 USD" + }, + { + "name": "hashrate", + "doc": "Current hash rate of the global network", + "category": null, + "inputName": "hashrate", + "input": "1", + "outputName": "rate", + "output": "562079779113.5204 1e9 'hash'/sec" + }, + { + "name": "total", + "doc": "Total number of BTC in circulation.", + "category": null, + "inputName": "total", + "input": "1", + "outputName": "bitcoin", + "output": "1970701250000000 / 1e8" + } + ] + }, + { + "name": "USD", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 1.0852) EUR" + }, + { + "name": "JPY", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 170.52) EUR" + }, + { + "name": "BGN", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 1.9558) EUR" + }, + { + "name": "CZK", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 24.709) EUR" + }, + { + "name": "DKK", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 7.4588) EUR" + }, + { + "name": "GBP", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 0.85365) EUR" + }, + { + "name": "HUF", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 388.83) EUR" + }, + { + "name": "PLN", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 4.2645) EUR" + }, + { + "name": "RON", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 4.9767) EUR" + }, + { + "name": "SEK", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 11.4210) EUR" + }, + { + "name": "CHF", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 0.9818) EUR" + }, + { + "name": "ISK", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 149.10) EUR" + }, + { + "name": "NOK", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 11.3830) EUR" + }, + { + "name": "TRY", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 34.9691) EUR" + }, + { + "name": "AUD", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 1.6315) EUR" + }, + { + "name": "BRL", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 5.6418) EUR" + }, + { + "name": "CAD", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 1.4804) EUR" + }, + { + "name": "CNY", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 7.8577) EUR" + }, + { + "name": "HKD", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 8.4838) EUR" + }, + { + "name": "IDR", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 17641.72) EUR" + }, + { + "name": "ILS", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 4.0342) EUR" + }, + { + "name": "INR", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 90.5255) EUR" + }, + { + "name": "KRW", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 1501.11) EUR" + }, + { + "name": "MXN", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 18.4387) EUR" + }, + { + "name": "MYR", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 5.1080) EUR" + }, + { + "name": "NZD", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 1.7696) EUR" + }, + { + "name": "PHP", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 63.604) EUR" + }, + { + "name": "SGD", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 1.4663) EUR" + }, + { + "name": "THB", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 39.925) EUR" + }, + { + "name": "ZAR", + "doc": "Sourced from European Central Bank. Current as of 2024-05-31.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 20.2927) EUR" + }, + { + "name": "HRK", + "doc": "Croatian Kuna. Pinned to Euro at a fixed rate since 2023-01-01.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 7.5345) EUR" + }, + { + "name": "RUB", + "doc": "Fetching live data failed. Fallback value provided from xe.com on 2024-01-06.", + "category": "currencies", + "type": "unit", + "expr": "(1 / 99.477867) EUR" + } +] \ No newline at end of file diff --git a/core/tests/currency_loading.rs b/core/tests/currency_loading.rs new file mode 100644 index 0000000..234c34d --- /dev/null +++ b/core/tests/currency_loading.rs @@ -0,0 +1,17 @@ +#[test] +fn load_currency() { + let mut ctx = rink_core::simple_context().unwrap(); + let live_data = include_str!("../tests/currency.snapshot.json"); + let base_defs = rink_core::CURRENCY_FILE.unwrap(); + ctx.load_currency(&live_data, base_defs).unwrap(); + + let result = rink_core::one_line(&mut ctx, "USD"); + assert_eq!( + result, + Ok("Definition: USD = (1 / 1.0852) EUR = \ + approx. 921.4891 millieuro (money; EUR). \ + Sourced from European Central Bank. \ + Current as of 2024-05-31." + .to_owned()) + ); +} diff --git a/core/tests/token_fmt.rs b/core/tests/token_fmt.rs index effbf81..6ecbca8 100644 --- a/core/tests/token_fmt.rs +++ b/core/tests/token_fmt.rs @@ -63,10 +63,7 @@ fn test(input: &str, output: &[FlatSpan<'static>]) { let expr = text_query::parse_query(&mut iter); CONTEXT.with(|ctx| { let res = ctx.eval_query(&expr); - let res = match res { - Ok(ref v) => v.to_spans(), - Err(ref v) => v.to_spans(), - }; + let res = res.to_spans(); let res = res.into_iter().map(FlatSpan::from).collect::>(); similar_asserts::assert_eq!(res, output); }); diff --git a/irc/Cargo.toml b/irc/Cargo.toml index aca78be..8b2eb57 100644 --- a/irc/Cargo.toml +++ b/irc/Cargo.toml @@ -10,7 +10,7 @@ edition = "2018" [dependencies.rink-core] version = "0.8" path = "../core" -features = [ "bundle-files" ] +features = [ "bundle-files", "serde_json" ] [dependencies.rink-sandbox] path = "../sandbox" @@ -22,7 +22,6 @@ humantime-serde = "1.0.1" irc = "1.0.0" serde = { version = "1", default-features = false } serde_derive = "1" -serde_json = "1" tokio = { version = "1", features = ["full"] } toml = "0.8" ubyte = { version = "0.10.3", features = ["serde"] } diff --git a/irc/src/config.rs b/irc/src/config.rs index 3c66132..d5142e2 100644 --- a/irc/src/config.rs +++ b/irc/src/config.rs @@ -3,7 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use irc::client::data::Config as IrcConfig; -use rink_core::{ast, loader::gnu_units, parsing::datetime, Context}; +use rink_core::{parsing::datetime, Context}; use serde_derive::{Deserialize, Serialize}; use std::{path::PathBuf, time::Duration}; use ubyte::ByteUnit; @@ -76,13 +76,7 @@ fn try_load_currency(config: &Currency, ctx: &mut Context) { let base = rink_core::CURRENCY_FILE.unwrap(); let live = std::fs::read_to_string(&config.path).unwrap(); - let mut base_defs = gnu_units::parse_str(&base); - let mut live_defs: ast::Defs = serde_json::from_str(&live).unwrap(); - - let mut defs = vec![]; - defs.append(&mut base_defs.defs); - defs.append(&mut live_defs.defs); - ctx.load(ast::Defs { defs }).unwrap(); + ctx.load_currency(&live, &base).unwrap(); } /// Creates a context by searching standard directories @@ -94,7 +88,7 @@ pub fn load(config: &Config) -> Context { let mut ctx = Context::new(); ctx.save_previous_result = true; - ctx.load(gnu_units::parse_str(&units)).unwrap(); + ctx.load_definitions(&units).unwrap(); ctx.load_dates(datetime::parse_datefile(&dates)); // Load currency data. diff --git a/irc/src/main.rs b/irc/src/main.rs index 0af448e..a4b0f57 100644 --- a/irc/src/main.rs +++ b/irc/src/main.rs @@ -42,10 +42,7 @@ async fn server_task(config: config::Config, index: usize) { }; println!("[{servername}] <== {command}"); let result = rink_core::eval(&mut ctx, command); - let result = match &result { - Ok(res) => res.to_spans(), - Err(err) => err.to_spans(), - }; + let result = result.to_spans(); let result = fmt::to_irc_string(&config, &result); println!("[{servername}] ==> {result}"); let where_to = if channel == client.current_nickname() { diff --git a/rink-js/Cargo.toml b/rink-js/Cargo.toml index 73ecc7c..b18fc0f 100644 --- a/rink-js/Cargo.toml +++ b/rink-js/Cargo.toml @@ -17,7 +17,7 @@ default = ["console_error_panic_hook"] [dependencies.rink-core] path = "../core" version = "0.8" -features = ["bundle-files"] +features = ["bundle-files", "serde_json"] [dependencies] wasm-bindgen = { version = "0.2", default-features = false } @@ -26,7 +26,6 @@ wee_alloc = { version = "0.4.5", default-features = false } chrono = { version = "0.4.13", default-features = false } serde = { version = "1", default-features = false } serde_derive = "1" -serde_json = "1" serde-wasm-bindgen = "0.6" # The `console_error_panic_hook` crate provides better debugging of panics by diff --git a/rink-js/src/lib.rs b/rink-js/src/lib.rs index af2b56e..4f5f36c 100644 --- a/rink-js/src/lib.rs +++ b/rink-js/src/lib.rs @@ -123,22 +123,8 @@ impl Context { #[wasm_bindgen(js_name = loadCurrency)] pub fn load_currency(&mut self, live_defs: String) -> Result<(), JsValue> { - let mut live_defs: Vec = - serde_json::from_str(&live_defs).map_err(|e| e.to_string())?; - - let mut base_defs = { - use rink_core::loader::gnu_units; - let defs = rink_core::CURRENCY_FILE.unwrap(); - let mut iter = gnu_units::TokenIterator::new(defs).peekable(); - gnu_units::parse(&mut iter) - }; - let currency = { - let mut defs = vec![]; - defs.append(&mut live_defs); - defs.append(&mut base_defs.defs); - ast::Defs { defs } - }; - self.context.load(currency)?; + let base_defs = rink_core::CURRENCY_FILE.unwrap(); + self.context.load_currency(&live_defs, base_defs)?; Ok(()) } @@ -170,10 +156,7 @@ impl Context { } } } - let spans = match value { - Ok(ref value) => value.to_spans(), - Err(ref value) => value.to_spans(), - }; + let spans = value.to_spans(); let tokens = visit_tokens(&spans); match serde_wasm_bindgen::to_value(&tokens) {