Lines
74.58 %
Functions
52.38 %
Branches
100 %
use finance::commodity::Commodity;
use num_rational::Rational64;
use scripting::runtime::{alloc_pair_chain, alloc_string_ref};
use server::command::account::{GetAccountCommodities, GetBalance, ListAccounts};
use server::command::{CmdResult, CommodityInfo};
use uuid::Uuid;
use wasmtime::{AnyRef, Caller, Rooted, StructRef};
use crate::session::SessionData;
pub(super) async fn resolve_balance(
user_id: Uuid,
id_arg: Option<String>,
) -> wasmtime::Result<(i64, i64, Uuid)> {
let raw = id_arg
.filter(|s| !s.is_empty())
.ok_or_else(|| wasmtime::Error::msg("account-balance: missing or empty :account-id arg"))?;
let account_id = Uuid::parse_str(&raw).map_err(|err| {
wasmtime::Error::msg(format!("account-balance: invalid uuid '{raw}': {err}"))
})?;
let commodity_id = single_commodity_for(user_id, account_id).await?;
let (numer, denom) = single_rational_for(user_id, account_id).await?;
Ok((numer, denom, commodity_id))
}
async fn single_commodity_for(user_id: Uuid, account_id: Uuid) -> wasmtime::Result<Uuid> {
match GetAccountCommodities::new()
.user_id(user_id)
.account_id(account_id)
.run()
.await
{
Ok(Some(CmdResult::CommodityInfoList(items))) => match items.as_slice() {
[info] => Ok(info.commodity_id),
[] => Err(wasmtime::Error::msg(
"account-balance: account has no commodity yet (no splits); cannot produce \
Commodity-typed value",
)),
_ => Err(wasmtime::Error::msg(
"account-balance: account holds multiple commodities; use get-balance instead",
},
Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
"account-balance: expected CommodityInfoList, got {other:?}"
))),
Ok(None) => Err(wasmtime::Error::msg(
Err(err) => Err(wasmtime::Error::msg(format!("account-balance: {err}"))),
async fn single_rational_for(user_id: Uuid, account_id: Uuid) -> wasmtime::Result<(i64, i64)> {
match GetBalance::new()
Ok(Some(CmdResult::Rational(r))) => Ok((*r.numer(), *r.denom())),
Ok(None) => Ok((0, 1)),
Ok(Some(CmdResult::MultiCurrencyBalance(_))) => Err(wasmtime::Error::msg(
"account-balance: unexpected variant {other:?}"
pub(super) async fn count_accounts(user_id: Uuid) -> i32 {
match ListAccounts::new().user_id(user_id).run().await {
Ok(Some(CmdResult::TaggedEntities { entities, .. })) => entities.len() as i32,
_ => 0,
pub(super) async fn run_get_balance_single(
) -> wasmtime::Result<(i64, i64)> {
.ok_or_else(|| wasmtime::Error::msg("get-balance: missing or empty :account-id arg"))?;
let account_id = Uuid::parse_str(&raw)
.map_err(|err| wasmtime::Error::msg(format!("get-balance: invalid uuid '{raw}': {err}")))?;
"get-balance: multi-currency account — use get-balances for the typed pair return",
"get-balance: expected Rational, got {other:?}"
Err(err) => Err(wasmtime::Error::msg(format!("get-balance: {err}"))),
pub(super) async fn run_get_balances(
) -> wasmtime::Result<Vec<String>> {
.ok_or_else(|| wasmtime::Error::msg("get-balances: missing or empty :account-id arg"))?;
wasmtime::Error::msg(format!("get-balances: invalid uuid '{raw}': {err}"))
let infos = fetch_commodity_infos(user_id, account_id).await?;
if infos.is_empty() {
return Ok(Vec::new());
let balance = GetBalance::new()
.map_err(|err| wasmtime::Error::msg(format!("get-balances: {err}")))?;
match balance {
None => Ok(Vec::new()),
Some(CmdResult::Rational(r)) => {
let info = infos.into_iter().next().ok_or_else(|| {
wasmtime::Error::msg("get-balances: rational balance but no commodity info")
Ok(vec![format_balance_plist(
&info.commodity_id.to_string(),
&info.symbol,
&info.name,
*r.numer(),
*r.denom(),
)])
Some(CmdResult::MultiCurrencyBalance(pairs)) => {
let result: Vec<String> = pairs
.iter()
.filter_map(|(commodity, r)| balance_plist_for(commodity, r, &infos))
.collect();
Ok(result)
Some(other) => Err(wasmtime::Error::msg(format!(
"get-balances: unexpected variant {other:?}"
fn balance_plist_for(
commodity: &Commodity,
r: &Rational64,
infos: &[CommodityInfo],
) -> Option<String> {
let info = infos.iter().find(|i| i.commodity_id == commodity.id)?;
Some(format_balance_plist(
&commodity.id.to_string(),
))
async fn fetch_commodity_infos(
account_id: Uuid,
) -> wasmtime::Result<Vec<CommodityInfo>> {
Ok(Some(CmdResult::CommodityInfoList(items))) => Ok(items),
Ok(None) => Ok(Vec::new()),
"get-balances: expected CommodityInfoList, got {other:?}"
Err(err) => Err(wasmtime::Error::msg(format!("get-balances: {err}"))),
fn format_balance_plist(id: &str, symbol: &str, name: &str, numer: i64, denom: i64) -> String {
format!(
"(:commodity-id {} :symbol {} :name {} :value-num {} :value-denom {})",
super::quote_string(id),
super::quote_string(symbol),
super::quote_string(name),
numer,
denom,
)
pub(super) async fn alloc_string_pair_chain(
caller: &mut Caller<'_, SessionData>,
lines: Vec<String>,
) -> wasmtime::Result<Option<Rooted<StructRef>>> {
let mut anyrefs: Vec<Rooted<AnyRef>> = Vec::with_capacity(lines.len());
for line in lines {
let s = alloc_string_ref(caller, line.as_bytes())?;
anyrefs.push(s.to_anyref());
alloc_pair_chain(caller, anyrefs).await
#[cfg(test)]
pub(super) fn format_rational(r: &Rational64) -> String {
if *r.denom() == 1 {
r.numer().to_string()
} else {
format!("{}/{}", r.numer(), r.denom())