use crate::MetaValue;
#[must_use]
pub fn meta_value_to_json(value: &MetaValue) -> serde_json::Value {
match value {
MetaValue::String(s) => serde_json::Value::String(s.clone()),
MetaValue::Account(a) => serde_json::Value::String(a.to_string()),
MetaValue::Currency(c) => serde_json::Value::String(c.to_string()),
MetaValue::Tag(t) => serde_json::Value::String(t.to_string()),
MetaValue::Link(l) => serde_json::Value::String(l.to_string()),
MetaValue::Date(d) => serde_json::Value::String(d.to_string()),
MetaValue::Number(n) => serde_json::Value::String(n.to_string()),
MetaValue::Int(i) => serde_json::Value::String(i.to_string()),
MetaValue::Bool(b) => serde_json::Value::Bool(*b),
MetaValue::Amount(a) => serde_json::json!({
"number": a.number.to_string(),
"currency": a.currency.to_string(),
}),
MetaValue::None => serde_json::Value::Null,
}
}
#[must_use]
pub const fn meta_value_type_tag(value: &MetaValue) -> &'static str {
match value {
MetaValue::String(_) => "string",
MetaValue::Account(_) => "account",
MetaValue::Currency(_) => "currency",
MetaValue::Tag(_) => "tag",
MetaValue::Link(_) => "link",
MetaValue::Date(_) => "date",
MetaValue::Number(_) => "number",
MetaValue::Int(_) => "int",
MetaValue::Bool(_) => "bool",
MetaValue::Amount(_) => "amount",
MetaValue::None => "null",
}
}
#[must_use]
pub fn json_to_meta_value(value: &serde_json::Value) -> MetaValue {
use rust_decimal::Decimal;
match value {
serde_json::Value::String(s) => MetaValue::String(s.clone()),
serde_json::Value::Bool(b) => MetaValue::Bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
MetaValue::Int(i)
} else {
Decimal::from_str_exact(&n.to_string()).map_or(MetaValue::None, MetaValue::Number)
}
}
serde_json::Value::Object(obj) => {
if let (Some(number), Some(currency)) = (obj.get("number"), obj.get("currency"))
&& let (Some(n), Some(c)) = (number.as_str(), currency.as_str())
&& let Ok(number) = Decimal::from_str_exact(n)
{
return MetaValue::Amount(crate::Amount {
number,
currency: c.into(),
});
}
MetaValue::None
}
serde_json::Value::Null | serde_json::Value::Array(_) => MetaValue::None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn codec_covers_every_variant() {
let cases = [
MetaValue::String("hi".into()),
MetaValue::Account("Assets:Cash".into()),
MetaValue::Currency("USD".into()),
MetaValue::Tag("trip".into()),
MetaValue::Link("inv-1".into()),
MetaValue::Date(crate::naive_date(2024, 6, 15).unwrap()),
MetaValue::Number(dec!(123.456)),
MetaValue::Int(42),
MetaValue::Bool(true),
MetaValue::Amount(crate::Amount::new(dec!(99.99), "EUR")),
MetaValue::None,
];
for mv in &cases {
assert!(!meta_value_type_tag(mv).is_empty());
let _ = meta_value_to_json(mv);
}
let tags: Vec<&str> = cases.iter().map(meta_value_type_tag).collect();
let mut uniq = tags.clone();
uniq.sort_unstable();
uniq.dedup();
assert_eq!(
uniq.len(),
tags.len(),
"type tags must be distinct: {tags:?}"
);
}
#[test]
fn json_round_trip() {
assert_eq!(
json_to_meta_value(&meta_value_to_json(&MetaValue::String("x".into()))),
MetaValue::String("x".into())
);
assert_eq!(
json_to_meta_value(&meta_value_to_json(&MetaValue::Bool(true))),
MetaValue::Bool(true)
);
assert_eq!(
json_to_meta_value(&meta_value_to_json(&MetaValue::None)),
MetaValue::None
);
assert_eq!(
json_to_meta_value(&meta_value_to_json(&MetaValue::Amount(crate::Amount::new(
dec!(1.50),
"USD"
)))),
MetaValue::Amount(crate::Amount::new(dec!(1.50), "USD"))
);
assert_eq!(
json_to_meta_value(&meta_value_to_json(&MetaValue::Int(7))),
MetaValue::String("7".into())
);
assert_eq!(json_to_meta_value(&serde_json::json!(7)), MetaValue::Int(7));
assert_eq!(
json_to_meta_value(&serde_json::json!(18_446_744_073_709_551_615_u64)),
MetaValue::Number(dec!(18446744073709551615))
);
}
}