Support serde serializing Amount as string
Sometimes JSON parsers may munge floats. Instead of using `f64` we can serialize BTC amounts as strings. Includes addition of `alloc` feature gate to `DisplayFullError` to remove lint warnings when building with `--no-default-features`. Close: #894
This commit is contained in:
parent
aa25adfc64
commit
22530f6a2b
|
@ -15,7 +15,7 @@ exclude = ["tests", "contrib"]
|
|||
[features]
|
||||
default = ["std"]
|
||||
std = ["alloc", "internals/std"]
|
||||
alloc = ["internals/alloc"]
|
||||
alloc = ["internals/alloc","serde?/alloc"]
|
||||
|
||||
[dependencies]
|
||||
internals = { package = "bitcoin-internals", version = "0.4.0" }
|
||||
|
|
|
@ -20,13 +20,16 @@
|
|||
//! }
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use core::fmt;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[cfg(feature = "alloc")] // This is because `to_float_in` uses `to_string`.
|
||||
use super::Denomination;
|
||||
use super::{Amount, ParseAmountError, SignedAmount};
|
||||
#[cfg(feature = "alloc")]
|
||||
use super::ParseAmountError;
|
||||
use super::{Amount, SignedAmount};
|
||||
|
||||
/// This trait is used only to avoid code duplication and naming collisions
|
||||
/// of the different serde serialization crates.
|
||||
|
@ -37,6 +40,10 @@ pub trait SerdeAmount: Copy + Sized {
|
|||
fn ser_btc<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error>;
|
||||
#[cfg(feature = "alloc")]
|
||||
fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result<Self, D::Error>;
|
||||
#[cfg(feature = "alloc")]
|
||||
fn ser_str<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error>;
|
||||
#[cfg(feature = "alloc")]
|
||||
fn des_str<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result<Self, D::Error>;
|
||||
}
|
||||
|
||||
mod private {
|
||||
|
@ -50,8 +57,11 @@ pub trait SerdeAmountForOpt: Copy + Sized + SerdeAmount {
|
|||
fn ser_sat_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error>;
|
||||
#[cfg(feature = "alloc")]
|
||||
fn ser_btc_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error>;
|
||||
#[cfg(feature = "alloc")]
|
||||
fn ser_str_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
struct DisplayFullError(ParseAmountError);
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
|
@ -70,6 +80,7 @@ impl fmt::Display for DisplayFullError {
|
|||
}
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
#[cfg(feature = "alloc")]
|
||||
impl fmt::Display for DisplayFullError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
|
||||
}
|
||||
|
@ -90,6 +101,18 @@ impl SerdeAmount for Amount {
|
|||
use serde::de::Error;
|
||||
Amount::from_btc(f64::deserialize(d)?).map_err(DisplayFullError).map_err(D::Error::custom)
|
||||
}
|
||||
#[cfg(feature = "alloc")]
|
||||
fn ser_str<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_str(&self.to_string_in(Denomination::Bitcoin))
|
||||
}
|
||||
#[cfg(feature = "alloc")]
|
||||
fn des_str<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result<Self, D::Error> {
|
||||
use serde::de::Error;
|
||||
let s: alloc::string::String = Deserialize::deserialize(d)?;
|
||||
Amount::from_str_in(&s, Denomination::Bitcoin)
|
||||
.map_err(DisplayFullError)
|
||||
.map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl SerdeAmountForOpt for Amount {
|
||||
|
@ -101,6 +124,10 @@ impl SerdeAmountForOpt for Amount {
|
|||
fn ser_btc_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_some(&self.to_btc())
|
||||
}
|
||||
#[cfg(feature = "alloc")]
|
||||
fn ser_str_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_some(&self.to_string_in(Denomination::Bitcoin))
|
||||
}
|
||||
}
|
||||
|
||||
impl SerdeAmount for SignedAmount {
|
||||
|
@ -121,6 +148,18 @@ impl SerdeAmount for SignedAmount {
|
|||
.map_err(DisplayFullError)
|
||||
.map_err(D::Error::custom)
|
||||
}
|
||||
#[cfg(feature = "alloc")]
|
||||
fn ser_str<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_str(self.to_string_in(Denomination::Bitcoin).as_str())
|
||||
}
|
||||
#[cfg(feature = "alloc")]
|
||||
fn des_str<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result<Self, D::Error> {
|
||||
use serde::de::Error;
|
||||
let s: alloc::string::String = Deserialize::deserialize(d)?;
|
||||
SignedAmount::from_str_in(&s, Denomination::Bitcoin)
|
||||
.map_err(DisplayFullError)
|
||||
.map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl SerdeAmountForOpt for SignedAmount {
|
||||
|
@ -132,6 +171,10 @@ impl SerdeAmountForOpt for SignedAmount {
|
|||
fn ser_btc_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_some(&self.to_btc())
|
||||
}
|
||||
#[cfg(feature = "alloc")]
|
||||
fn ser_str_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_some(&self.to_string_in(Denomination::Bitcoin))
|
||||
}
|
||||
}
|
||||
|
||||
pub mod as_sat {
|
||||
|
@ -272,3 +315,73 @@ pub mod as_btc {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub mod as_str {
|
||||
//! Serialize and deserialize [`Amount`](crate::Amount) as a JSON string denominated in BTC.
|
||||
//! Use with `#[serde(with = "amount::serde::as_str")]`.
|
||||
|
||||
use serde::{Deserializer, Serializer};
|
||||
|
||||
use super::private;
|
||||
use crate::amount::serde::SerdeAmount;
|
||||
|
||||
pub fn serialize<A: SerdeAmount, S: Serializer>(a: &A, s: S) -> Result<S::Ok, S::Error> {
|
||||
a.ser_str(s, private::Token)
|
||||
}
|
||||
|
||||
pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result<A, D::Error> {
|
||||
A::des_str(d, private::Token)
|
||||
}
|
||||
|
||||
pub mod opt {
|
||||
//! Serialize and deserialize `Option<Amount>` as a JSON string denominated in BTC.
|
||||
//! Use with `#[serde(default, with = "amount::serde::as_str::opt")]`.
|
||||
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use serde::{de, Deserializer, Serializer};
|
||||
|
||||
use super::private;
|
||||
use crate::amount::serde::SerdeAmountForOpt;
|
||||
|
||||
pub fn serialize<A: SerdeAmountForOpt, S: Serializer>(
|
||||
a: &Option<A>,
|
||||
s: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
match *a {
|
||||
Some(a) => a.ser_str_opt(s, private::Token),
|
||||
None => s.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'d, A: SerdeAmountForOpt, D: Deserializer<'d>>(
|
||||
d: D,
|
||||
) -> Result<Option<A>, D::Error> {
|
||||
struct VisitOptAmt<X>(PhantomData<X>);
|
||||
|
||||
impl<'de, X: SerdeAmountForOpt> de::Visitor<'de> for VisitOptAmt<X> {
|
||||
type Value = Option<X>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(formatter, "An Option<String>")
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
fn visit_some<D>(self, d: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(Some(X::des_str(d, private::Token)?))
|
||||
}
|
||||
}
|
||||
d.deserialize_option(VisitOptAmt::<A>(PhantomData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -741,6 +741,31 @@ fn serde_as_btc() {
|
|||
assert!(t.unwrap_err().to_string().contains(&OutOfRangeError::negative().to_string()));
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg(feature = "alloc")]
|
||||
#[test]
|
||||
fn serde_as_str() {
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct T {
|
||||
#[serde(with = "crate::amount::serde::as_str")]
|
||||
pub amt: Amount,
|
||||
#[serde(with = "crate::amount::serde::as_str")]
|
||||
pub samt: SignedAmount,
|
||||
}
|
||||
|
||||
serde_test::assert_tokens(
|
||||
&T { amt: Amount::from_sat(123456789), samt: SignedAmount::from_sat(-123456789) },
|
||||
&[
|
||||
serde_test::Token::Struct { name: "T", len: 2 },
|
||||
serde_test::Token::String("amt"),
|
||||
serde_test::Token::String("1.23456789"),
|
||||
serde_test::Token::String("samt"),
|
||||
serde_test::Token::String("-1.23456789"),
|
||||
serde_test::Token::StructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg(feature = "alloc")]
|
||||
#[test]
|
||||
|
@ -825,6 +850,49 @@ fn serde_as_sat_opt() {
|
|||
assert_eq!(without, serde_json::from_value(value_without).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg(feature = "alloc")]
|
||||
#[test]
|
||||
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
|
||||
fn serde_as_str_opt() {
|
||||
use serde_json;
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
|
||||
struct T {
|
||||
#[serde(default, with = "crate::amount::serde::as_str::opt")]
|
||||
pub amt: Option<Amount>,
|
||||
#[serde(default, with = "crate::amount::serde::as_str::opt")]
|
||||
pub samt: Option<SignedAmount>,
|
||||
}
|
||||
|
||||
let with = T {
|
||||
amt: Some(Amount::from_sat(123456789)),
|
||||
samt: Some(SignedAmount::from_sat(-123456789)),
|
||||
};
|
||||
let without = T { amt: None, samt: None };
|
||||
|
||||
// Test Roundtripping
|
||||
for s in [&with, &without].iter() {
|
||||
let v = serde_json::to_string(s).unwrap();
|
||||
let w: T = serde_json::from_str(&v).unwrap();
|
||||
assert_eq!(w, **s);
|
||||
}
|
||||
|
||||
let t: T =
|
||||
serde_json::from_str("{\"amt\": \"1.23456789\", \"samt\": \"-1.23456789\"}").unwrap();
|
||||
assert_eq!(t, with);
|
||||
|
||||
let t: T = serde_json::from_str("{}").unwrap();
|
||||
assert_eq!(t, without);
|
||||
|
||||
let value_with: serde_json::Value =
|
||||
serde_json::from_str("{\"amt\": \"1.23456789\", \"samt\": \"-1.23456789\"}").unwrap();
|
||||
assert_eq!(with, serde_json::from_value(value_with).unwrap());
|
||||
|
||||
let value_without: serde_json::Value = serde_json::from_str("{}").unwrap();
|
||||
assert_eq!(without, serde_json::from_value(value_without).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sum_amounts() {
|
||||
assert_eq!(Amount::from_sat(0), [].iter().sum::<Amount>());
|
||||
|
|
Loading…
Reference in New Issue