Skip to content

Commit

Permalink
API usability improvements (#184)
Browse files Browse the repository at this point in the history
* Added helper functions `load_definitions()` and `load_currency()` on
Context to deduplicate code every frontend had to write out
* Added ToSpans impl for `Result<QueryReply, QueryError>` to avoid a
pointless looking match statement
* Added more examples to the API docs
* Fleshed out the API docs a bit more
  • Loading branch information
tiffany352 authored Jun 2, 2024
1 parent df8b961 commit db3f8d0
Show file tree
Hide file tree
Showing 24 changed files with 495 additions and 98 deletions.
3 changes: 0 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"
Expand Down
33 changes: 16 additions & 17 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -279,31 +279,25 @@ pub(crate) fn force_refresh_currency(config: &Currency) -> Result<String> {
))
}

fn load_live_currency(config: &Currency) -> Result<ast::Defs> {
fn load_live_currency(config: &Currency) -> Result<String> {
let duration = if config.fetch_on_startup {
Some(config.cache_duration)
} else {
None
};
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<()> {
let base = read_from_search_path("currency.units", search_path, CURRENCY_FILE)?
.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(())
}

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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))
Expand All @@ -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);
}
Expand All @@ -584,15 +583,15 @@ 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))
.expect("the response should go through");
});
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);
}
Expand Down
2 changes: 2 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"

Expand Down
2 changes: 2 additions & 0 deletions core/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions core/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
37 changes: 28 additions & 9 deletions core/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<QueryReply, QueryError> {
ctx.update_time();
let mut iter = text_query::TokenIterator::new(line.trim()).peekable();
Expand All @@ -39,7 +62,7 @@ pub fn eval(ctx: &mut Context, line: &str) -> Result<QueryReply, QueryError> {
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<String, String> {
eval(ctx, line)
.as_ref()
Expand All @@ -54,20 +77,16 @@ pub fn simple_context() -> Result<Context, String> {
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")
}
120 changes: 94 additions & 26 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
//!
//! <https://rinkcalc.app/data/currency.json>
//!
//! 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("<i>");
//! out.push_str(&text);
//! out.push_str("</i>");
//! }
//! 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)]
Expand Down
Loading

0 comments on commit db3f8d0

Please sign in to comment.