Skip to content

Commit

Permalink
Customisable serialisation per field using serde's with attribute (#459)
Browse files Browse the repository at this point in the history
* Add module to serialise a Decimal as an arbitrary number using serde with

* Add module to serialise a Decimal as a float using serde with

* Add module to serialise a Decimal as a string using serde with

* Run makers format

* Add serde-with features to the readme

Co-authored-by: Paul Mason <paul@paulmason.me>
  • Loading branch information
Levi Wright and paupino authored Jan 8, 2022
1 parent fca96a5 commit caee892
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 27 deletions.
10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ futures = "0.3"
serde_derive = "1.0"
serde_json = "1.0"
tokio = { features = ["rt-multi-thread", "test-util", "macros"], version = "1.0" }
serde = { default-features = false, features = ["derive"], version = "1.0" }

[features]
c-repr = [] # Force Decimal to be repr(C)
Expand All @@ -55,10 +56,13 @@ maths = []
maths-nopanic = ["maths"]
rocket-traits = ["rocket"]
rust-fuzz = ["arbitrary"]
serde-arbitrary-precision = ["serde", "serde_json/arbitrary_precision"]
serde-arbitrary-precision = ["serde-with-arbitrary-precision"]
serde-bincode = ["serde-str"] # Backwards compatability
serde-float = ["serde"]
serde-str = ["serde"]
serde-float = ["serde-with-float"]
serde-str = ["serde-with-str"]
serde-with-arbitrary-precision = ["serde", "serde_json/arbitrary_precision"]
serde-with-float = ["serde"]
serde-with-str = ["serde"]
std = []
tokio-pg = ["db-tokio-postgres"] # Backwards compatability

Expand Down
17 changes: 16 additions & 1 deletion Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ dependencies = [
"test-serde-str",
"test-serde-str-float",
"test-serde-arbitrary-precision",
"test-serde-arbitrary-precision-float"
"test-serde-arbitrary-precision-float",
"test-serde-with-arbitrary-precision",
"test-serde-with-float",
"test-serde-with-str",
]

[tasks.test-macros]
Expand Down Expand Up @@ -225,3 +228,15 @@ args = ["test", "--workspace", "--tests", "--features=serde-arbitrary-precision"
[tasks.test-serde-arbitrary-precision-float]
command = "cargo"
args = ["test", "--workspace", "--tests", "--features=serde-arbitrary-precision,serde-float", "serde", "--", "--skip", "generated"]

[tasks.test-serde-with-arbitrary-precision]
command = "cargo"
args = ["test", "--workspace", "--tests", "--features=serde-with-arbitrary-precision", "serde", "--", "--skip", "generated"]

[tasks.test-serde-with-float]
command = "cargo"
args = ["test", "--workspace", "--tests", "--features=serde-with-float", "serde", "--", "--skip", "generated"]

[tasks.test-serde-with-str]
command = "cargo"
args = ["test", "--workspace", "--tests", "--features=serde-with-str", "serde", "--", "--skip", "generated"]
69 changes: 53 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Decimal &emsp; [![Build Status]][actions] [![Latest Version]][crates.io] [![Docs Badge]][docs]
# Decimal &emsp; [![Build Status]][actions] [![Latest Version]][crates.io] [![Docs Badge]][docs]

[Build Status]: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fpaupino%2Frust-decimal%2Fbadge&label=build&logo=none
[actions]: https://actions-badge.atrox.dev/paupino/rust-decimal/goto
Expand Down Expand Up @@ -102,26 +102,26 @@ Enables the tokio postgres module allowing for async communication with PostgreS

### `db-diesel-postgres`

Enable `diesel` PostgreSQL support.
Enable `diesel` PostgreSQL support.

### `db-diesel-mysql`

Enable `diesel` MySQL support.

### `legacy-ops`

As of `1.10` the algorithms used to perform basic operations have changed which has benefits of significant speed improvements.
As of `1.10` the algorithms used to perform basic operations have changed which has benefits of significant speed improvements.
To maintain backwards compatibility this can be opted out of by enabling the `legacy-ops` feature.

### `maths`

The `maths` feature enables additional complex mathematical functions such as `pow`, `ln`, `enf`, `exp` etc.
Documentation detailing the additional functions can be found on the
The `maths` feature enables additional complex mathematical functions such as `pow`, `ln`, `enf`, `exp` etc.
Documentation detailing the additional functions can be found on the
[`MathematicalOps`](https://docs.rs/rust_decimal/latest/rust_decimal/trait.MathematicalOps.html) trait.

Please note that `ln` and `log10` will panic on invalid input with `checked_ln` and `checked_log10` the preferred functions
to curb against this. When the `maths` feature was first developed the library would return `0` on invalid input. To re-enable this
non-panicking behavior, please use the feature: `maths-nopanic`.
to curb against this. When the `maths` feature was first developed the library would return `0` on invalid input. To re-enable this
non-panicking behavior, please use the feature: `maths-nopanic`.

### `rocket-traits`

Expand All @@ -146,21 +146,58 @@ e.g. with this turned on, JSON serialization would output:

This is typically useful for `bincode` or `csv` like implementations.

Since `bincode` does not specify type information, we need to ensure that a type hint is provided in order to
correctly be able to deserialize. Enabling this feature on its own will force deserialization to use `deserialize_str`
instead of `deserialize_any`.
Since `bincode` does not specify type information, we need to ensure that a type hint is provided in order to
correctly be able to deserialize. Enabling this feature on its own will force deserialization to use `deserialize_str`
instead of `deserialize_any`.

If, for some reason, you also have `serde-float` enabled then this will use `deserialize_f64` as a type hint. Because
converting to `f64` _loses_ precision, it's highly recommended that you do NOT enable this feature when working with
converting to `f64` _loses_ precision, it's highly recommended that you do NOT enable this feature when working with
`bincode`. That being said, this will only use 8 bytes so is slightly more efficient in terms of storage size.

### `serde-arbitrary-precision`

This is used primarily with `serde_json` and consequently adds it as a "weak dependency". This supports the
`arbitrary_precision` feature inside `serde_json` when parsing decimals.
This is used primarily with `serde_json` and consequently adds it as a "weak dependency". This supports the
`arbitrary_precision` feature inside `serde_json` when parsing decimals.

This is recommended when parsing "float" looking data as it will prevent data loss.

### `serde-with-float`

Enable this to access the module for serialising `Decimal` types to a float. This can be use in `struct` definitions like so:

```rust
#[derive(Serialize, Deserialize)]
pub struct FloatExample {
#[serde(with = "rust_decimal::serde::float")]
value: Decimal,
}
```

### `serde-with-str`

Enable this to access the module for serialising `Decimal` types to a `String`. This can be use in `struct` definitions like so:

```rust
#[derive(Serialize, Deserialize)]
pub struct StrExample {
#[serde(with = "rust_decimal::serde::str")]
value: Decimal,
}
```

### `serde-with-arbitrary-precision`

Enable this to access the module for serialising `Decimal` types to a `String`. This can be use in `struct` definitions like so:

```rust
#[derive(Serialize, Deserialize)]
pub struct ArbitraryExample {
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
value: Decimal,
}
```


### `std`

Enable `std` library support. This is enabled by default, however in the future will be opt in. For now, to support `no_std`
Expand All @@ -177,6 +214,6 @@ which was released on `2021-03-25` and included support for "const generics".

### Updating the minimum supported version

This library maintains support for rust compiler versions that are 5 minor versions away from the current stable rust compiler version.
For example, if the current stable compiler version is `1.50.0` then we will guarantee support up to and including `1.45.0`.
Of note, we will only update the minimum supported version if and when required.
This library maintains support for rust compiler versions that are 5 minor versions away from the current stable rust compiler version.
For example, if the current stable compiler version is `1.50.0` then we will guarantee support up to and including `1.45.0`.
Of note, we will only update the minimum supported version if and when required.
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ mod postgres;
#[cfg(feature = "rocket-traits")]
mod rocket;
#[cfg(feature = "serde")]
mod serde;
pub mod serde;

pub use decimal::{Decimal, RoundingStrategy};
pub use error::Error;
Expand Down
177 changes: 171 additions & 6 deletions src/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,126 @@ use core::{fmt, str::FromStr};
use num_traits::FromPrimitive;
use serde::{self, de::Unexpected};

/// Serialize Decimals as arbitrary precision numbers in JSON.
///
/// ```
/// # use serde::{Serialize, Deserialize};
/// # use rust_decimal::Decimal;
/// # use std::str::FromStr;
///
/// #[derive(Serialize, Deserialize)]
/// pub struct ArbitraryExample {
/// #[serde(with = "rust_decimal::serde::arbitrary_precision")]
/// value: Decimal,
/// }
///
/// let value = ArbitraryExample { value: Decimal::from_str("123.400").unwrap() };
/// assert_eq!(
/// &serde_json::to_string(&value).unwrap(),
/// r#"{"value":123.400}"#
/// );
/// ```
#[cfg(feature = "serde-with-arbitrary-precision")]
pub mod arbitrary_precision {
use super::*;
use serde::Serialize;

pub fn deserialize<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
where
D: serde::de::Deserializer<'de>,
{
deserializer.deserialize_any(DecimalVisitor)
}

pub fn serialize<S>(value: &Decimal, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serde_json::Number::from_str(&value.to_string())
.map_err(serde::ser::Error::custom)?
.serialize(serializer)
}
}

/// Serialize Decimals as floats in JSON.
///
/// ```
/// # use serde::{Serialize, Deserialize};
/// # use rust_decimal::Decimal;
/// # use std::str::FromStr;
///
/// #[derive(Serialize, Deserialize)]
/// pub struct FloatExample {
/// #[serde(with = "rust_decimal::serde::float")]
/// value: Decimal,
/// }
///
/// let value = FloatExample { value: Decimal::from_str("123.400").unwrap() };
/// assert_eq!(
/// &serde_json::to_string(&value).unwrap(),
/// r#"{"value":123.4}"#
/// );
/// ```
#[cfg(feature = "serde-with-float")]
pub mod float {
use super::*;
use serde::Serialize;

pub fn deserialize<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
where
D: serde::de::Deserializer<'de>,
{
deserializer.deserialize_any(DecimalVisitor)
}

pub fn serialize<S>(value: &Decimal, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use num_traits::ToPrimitive;
value.to_f64().unwrap().serialize(serializer)
}
}

/// Serialize Decimals as floats in JSON.
///
/// ```
/// # use serde::{Serialize, Deserialize};
/// # use rust_decimal::Decimal;
/// # use std::str::FromStr;
///
/// #[derive(Serialize, Deserialize)]
/// pub struct StringExample {
/// #[serde(with = "rust_decimal::serde::string")]
/// value: Decimal,
/// }
///
/// let value = StringExample { value: Decimal::from_str("123.400").unwrap() };
/// assert_eq!(
/// &serde_json::to_string(&value).unwrap(),
/// r#"{"value":"123.400"}"#
/// );
/// ```
#[cfg(feature = "serde-with-str")]
pub mod str {
use super::*;
use serde::Serialize;

pub fn deserialize<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
where
D: serde::de::Deserializer<'de>,
{
deserializer.deserialize_any(DecimalVisitor)
}

pub fn serialize<S>(value: &Decimal, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
value.to_string().serialize(serializer)
}
}

#[cfg(not(feature = "serde-str"))]
impl<'de> serde::Deserialize<'de> for Decimal {
fn deserialize<D>(deserializer: D) -> Result<Decimal, D::Error>
Expand Down Expand Up @@ -35,7 +155,7 @@ impl<'de> serde::Deserialize<'de> for Decimal {
}

// It's a shame this needs to be redefined for this feature and not able to be referenced directly
#[cfg(feature = "serde-arbitrary-precision")]
#[cfg(feature = "serde-with-arbitrary-precision")]
const DECIMAL_KEY_TOKEN: &str = "$serde_json::private::Number";

struct DecimalVisitor;
Expand Down Expand Up @@ -83,7 +203,7 @@ impl<'de> serde::de::Visitor<'de> for DecimalVisitor {
.map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
}

#[cfg(feature = "serde-arbitrary-precision")]
#[cfg(feature = "serde-with-arbitrary-precision")]
fn visit_map<A>(self, map: A) -> Result<Decimal, A::Error>
where
A: serde::de::MapAccess<'de>,
Expand All @@ -98,10 +218,10 @@ impl<'de> serde::de::Visitor<'de> for DecimalVisitor {
}
}

#[cfg(feature = "serde-arbitrary-precision")]
#[cfg(feature = "serde-with-arbitrary-precision")]
struct DecimalKey;

#[cfg(feature = "serde-arbitrary-precision")]
#[cfg(feature = "serde-with-arbitrary-precision")]
impl<'de> serde::de::Deserialize<'de> for DecimalKey {
fn deserialize<D>(deserializer: D) -> Result<DecimalKey, D::Error>
where
Expand Down Expand Up @@ -133,12 +253,12 @@ impl<'de> serde::de::Deserialize<'de> for DecimalKey {
}
}

#[cfg(feature = "serde-arbitrary-precision")]
#[cfg(feature = "serde-with-arbitrary-precision")]
pub struct DecimalFromString {
pub value: Decimal,
}

#[cfg(feature = "serde-arbitrary-precision")]
#[cfg(feature = "serde-with-arbitrary-precision")]
impl<'de> serde::de::Deserialize<'de> for DecimalFromString {
fn deserialize<D>(deserializer: D) -> Result<DecimalFromString, D::Error>
where
Expand Down Expand Up @@ -373,4 +493,49 @@ mod test {
let des: Foo = bincode::deserialize(&ser).unwrap();
assert_eq!(des.value, s.value);
}

#[test]
#[cfg(feature = "serde-with-arbitrary-precision")]
fn with_arbitrary_precision() {
#[derive(Serialize, Deserialize)]
pub struct ArbitraryExample {
#[serde(with = "crate::serde::arbitrary_precision")]
value: Decimal,
}

let value = ArbitraryExample {
value: Decimal::from_str("123.400").unwrap(),
};
assert_eq!(&serde_json::to_string(&value).unwrap(), r#"{"value":123.400}"#);
}

#[test]
#[cfg(feature = "serde-with-float")]
fn with_float() {
#[derive(Serialize, Deserialize)]
pub struct FloatExample {
#[serde(with = "crate::serde::float")]
value: Decimal,
}

let value = FloatExample {
value: Decimal::from_str("123.400").unwrap(),
};
assert_eq!(&serde_json::to_string(&value).unwrap(), r#"{"value":123.4}"#);
}

#[test]
#[cfg(feature = "serde-with-str")]
fn with_str() {
#[derive(Serialize, Deserialize)]
pub struct StringExample {
#[serde(with = "crate::serde::str")]
value: Decimal,
}

let value = StringExample {
value: Decimal::from_str("123.400").unwrap(),
};
assert_eq!(&serde_json::to_string(&value).unwrap(), r#"{"value":"123.400"}"#);
}
}

0 comments on commit caee892

Please sign in to comment.