use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::error::{Error, Result};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Budget {
pub amount: u64,
pub unit: String,
pub decimals: u32,
}
const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
impl Budget {
pub fn new(amount: u64, unit: impl Into<String>, decimals: u32) -> Self {
Self {
amount,
unit: unit.into(),
decimals,
}
}
pub fn from_value(value: Option<&Value>) -> Result<Option<Self>> {
let Some(value) = value.filter(|v| !v.is_null()) else {
return Ok(None);
};
let invalid = || Error::Invalid("budget is {amount, unit, decimals}".into());
let amount = value
.get("amount")
.and_then(Value::as_u64)
.filter(|a| *a <= MAX_SAFE_INTEGER)
.ok_or_else(invalid)?;
let unit = value
.get("unit")
.and_then(Value::as_str)
.ok_or_else(invalid)?;
let decimals = value
.get("decimals")
.and_then(Value::as_u64)
.and_then(|d| u32::try_from(d).ok())
.ok_or_else(invalid)?;
Ok(Some(Self::new(amount, unit, decimals)))
}
pub fn same_scale(&self, other: &Budget) -> bool {
self.unit == other.unit && self.decimals == other.decimals
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn budgets_parse_strictly() {
for bad in [
json!({"amount": -1, "unit": "USD", "decimals": 2}),
json!({"amount": 1.5, "unit": "USD", "decimals": 2}),
json!({"amount": 1, "decimals": 2}),
json!({"amount": 1, "unit": "USD", "decimals": -2}),
json!({"amount": 9_007_199_254_740_992u64, "unit": "USD", "decimals": 2}),
json!("100 USD"),
] {
assert!(Budget::from_value(Some(&bad)).is_err(), "{bad}");
}
assert!(Budget::from_value(Some(&Value::Null)).unwrap().is_none());
}
#[test]
fn only_the_same_unit_and_scale_compare() {
let usd = Budget::new(1, "USD", 2);
assert!(usd.same_scale(&Budget::new(5, "USD", 2)));
assert!(!usd.same_scale(&Budget::new(5, "USD", 0)));
assert!(!usd.same_scale(&Budget::new(5, "EUR", 2)));
}
}