use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
static DECIMAL_FROM_JSON_NUMBER: AtomicU64 = AtomicU64::new(0);
#[must_use]
pub fn decimal_from_json_number_count() -> u64 {
DECIMAL_FROM_JSON_NUMBER.load(Ordering::Relaxed)
}
#[inline]
fn note_json_number() {
DECIMAL_FROM_JSON_NUMBER.fetch_add(1, Ordering::Relaxed);
#[cfg(feature = "metrics")]
metrics::counter!("bo4e_decimal_from_json_number_total").increment(1);
}
#[inline]
fn note_lossy_json_number(#[allow(unused_variables)] rendered: &str) {
note_json_number();
#[cfg(feature = "tracing")]
tracing::debug!(
value = rendered,
"decimal read from a fractional JSON number; scale and precision beyond f64 are already lost"
);
}
#[cfg(feature = "decimal")]
mod imp {
use super::{fmt, note_json_number, note_lossy_json_number};
use rust_decimal::Decimal;
pub(super) type Value = Decimal;
pub(super) struct DecimalVisitor;
impl serde::de::Visitor<'_> for DecimalVisitor {
type Value = Decimal;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a decimal as a JSON string or number")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Decimal, E> {
v.parse::<Decimal>().map_err(E::custom)
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Decimal, E> {
note_json_number();
Ok(Decimal::from(v))
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Decimal, E> {
note_json_number();
Ok(Decimal::from(v))
}
#[allow(clippy::unnecessary_fallible_conversions)]
fn visit_u128<E: serde::de::Error>(self, v: u128) -> Result<Decimal, E> {
note_json_number();
Decimal::try_from(v).map_err(E::custom)
}
#[allow(clippy::unnecessary_fallible_conversions)]
fn visit_i128<E: serde::de::Error>(self, v: i128) -> Result<Decimal, E> {
note_json_number();
Decimal::try_from(v).map_err(E::custom)
}
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Decimal, E> {
let rendered = v.to_string();
note_lossy_json_number(&rendered);
rendered.parse::<Decimal>().map_err(E::custom)
}
}
}
#[cfg(not(feature = "decimal"))]
mod imp {
use super::{fmt, note_json_number, note_lossy_json_number};
pub(super) type Value = String;
pub(super) struct DecimalVisitor;
impl serde::de::Visitor<'_> for DecimalVisitor {
type Value = String;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a decimal as a JSON string or number")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<String, E> {
Ok(v.to_owned())
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<String, E> {
Ok(v)
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<String, E> {
note_json_number();
Ok(v.to_string())
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<String, E> {
note_json_number();
Ok(v.to_string())
}
fn visit_u128<E: serde::de::Error>(self, v: u128) -> Result<String, E> {
note_json_number();
Ok(v.to_string())
}
fn visit_i128<E: serde::de::Error>(self, v: i128) -> Result<String, E> {
note_json_number();
Ok(v.to_string())
}
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<String, E> {
let rendered = v.to_string();
note_lossy_json_number(&rendered);
Ok(rendered)
}
}
}
use imp::{DecimalVisitor, Value};
pub fn deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Value, D::Error> {
d.deserialize_any(DecimalVisitor)
}
struct OptDecimalVisitor;
impl<'de> serde::de::Visitor<'de> for OptDecimalVisitor {
type Value = Option<Value>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a decimal as a JSON string or number, or null")
}
fn visit_none<E: serde::de::Error>(self) -> Result<Option<Value>, E> {
Ok(None)
}
fn visit_unit<E: serde::de::Error>(self) -> Result<Option<Value>, E> {
Ok(None)
}
fn visit_some<D: serde::Deserializer<'de>>(self, d: D) -> Result<Option<Value>, D::Error> {
d.deserialize_any(DecimalVisitor).map(Some)
}
}
pub fn deserialize_opt<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<Value>, D::Error> {
d.deserialize_option(OptDecimalVisitor)
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(serde::Deserialize)]
struct Holder {
#[serde(default, deserialize_with = "deserialize_opt")]
wert: Option<Value>,
}
fn read(json: &str) -> Option<Value> {
serde_json::from_str::<Holder>(json).expect("valid").wert
}
#[test]
fn accepts_both_wire_spellings_and_null() {
assert!(read(r#"{"wert":"119.00"}"#).is_some());
assert!(read(r#"{"wert":119.00}"#).is_some());
assert!(read(r#"{"wert":119}"#).is_some());
assert!(read(r#"{"wert":null}"#).is_none());
assert!(read("{}").is_none());
}
#[test]
fn rejects_a_non_numeric_json_type() {
assert!(serde_json::from_str::<Holder>(r#"{"wert":[1]}"#).is_err());
assert!(serde_json::from_str::<Holder>(r#"{"wert":true}"#).is_err());
}
#[test]
#[cfg(feature = "decimal")]
fn a_json_string_keeps_its_scale() {
assert_eq!(read(r#"{"wert":"119.00"}"#).unwrap().to_string(), "119.00");
assert_eq!(read(r#"{"wert":"0.0725"}"#).unwrap().to_string(), "0.0725");
}
#[test]
#[cfg(feature = "decimal")]
fn a_json_string_keeps_precision_f64_would_lose() {
assert_eq!(
read(r#"{"wert":"12345678901234567890.12"}"#)
.unwrap()
.to_string(),
"12345678901234567890.12"
);
}
#[test]
#[cfg(feature = "decimal")]
fn a_json_number_arrives_at_f64_fidelity() {
let got = read(r#"{"wert":12345678901234567890.12}"#).unwrap();
assert_eq!(got.to_string(), "12345678901234567000");
}
#[test]
fn reading_a_decimal_from_a_json_number_is_counted() {
assert!(read(r#"{"wert":0.5}"#).is_some());
assert!(
decimal_from_json_number_count() > 0,
"a decimal read from a JSON number must be counted"
);
}
#[test]
fn an_integer_json_number_moves_the_counter() {
let before = decimal_from_json_number_count();
assert!(read(r#"{"wert":119}"#).is_some());
assert!(
decimal_from_json_number_count() > before,
"an integer JSON number is a number spelling, and is counted as one"
);
}
#[test]
#[cfg(feature = "decimal")]
fn a_fractional_number_uses_the_shortest_rendering() {
assert_eq!(read(r#"{"wert":0.1}"#).unwrap().to_string(), "0.1");
}
#[test]
#[cfg(feature = "decimal")]
fn an_integer_is_exact() {
assert_eq!(read(r#"{"wert":119}"#).unwrap().to_string(), "119");
assert_eq!(read(r#"{"wert":-42}"#).unwrap().to_string(), "-42");
assert_eq!(
read(r#"{"wert":9007199254740993}"#).unwrap().to_string(),
"9007199254740993",
"2^53 + 1 must survive: an integer must not go through the f64 path"
);
}
#[test]
#[cfg(not(feature = "decimal"))]
fn without_the_feature_the_lexical_form_is_kept() {
assert_eq!(read(r#"{"wert":"119.00"}"#).unwrap(), "119.00");
assert_eq!(read(r#"{"wert":119}"#).unwrap(), "119");
}
}