From 829355a377ef01affbd53a0c12de431c6904173e Mon Sep 17 00:00:00 2001 From: Paul Mason Date: Fri, 1 Dec 2023 09:07:21 -0800 Subject: [PATCH] Introduces `macros` feature and cleans up serde documentation a little bit (#628) --- CHANGELOG.md | 10 ++++ Cargo.toml | 10 +++- README.md | 67 ++++++++++++++++++----- examples/README.md | 16 ++++++ examples/serde-json-scenarios/Cargo.toml | 12 ++++ examples/serde-json-scenarios/src/main.rs | 56 +++++++++++++++++++ src/lib.rs | 5 ++ 7 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/serde-json-scenarios/Cargo.toml create mode 100644 examples/serde-json-scenarios/src/main.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a3591a46..6599743c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Version History +## 1.x + +### Added + +* `rust_decimal_macros` can now be utilized using the `macros` feature flag. + +### Changed + +* Added documentation for serde features as well as a few examples. + ## 1.33.1 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index b036a684..d1c1be96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ proptest = { default-features = false, optional = true, features = ["std"], vers rand = { default-features = false, optional = true, version = "0.8" } rkyv = { default-features = false, features = ["size_32", "std"], optional = true, version = "0.7.42" } rocket = { default-features = false, optional = true, version = "0.5.0-rc.3" } +rust_decimal_macros = { default-features = false, optional = true, version = "1.33" } # This needs to be the n-1 published version serde = { default-features = false, optional = true, version = "1.0" } serde_json = { default-features = false, optional = true, version = "1.0" } tokio-postgres = { default-features = false, optional = true, version = "0.7" } @@ -42,7 +43,7 @@ criterion = { default-features = false, version = "0.5" } csv = "1" futures = { default-features = false, version = "0.3" } rand = { default-features = false, features = ["getrandom"], version = "0.8" } -rust_decimal_macros = { path = "macros" } # This should be ok since it's just for tests +rust_decimal_macros = { default-features = false, version = "1.33" } serde = { default-features = false, features = ["derive"], version = "1.0" } serde_json = "1.0" tokio = { default-features = false, features = ["macros", "rt-multi-thread", "test-util"], version = "1.0" } @@ -50,6 +51,7 @@ version-sync = { default-features = false, features = ["html_root_url_updated", [features] default = ["serde", "std"] +macros = ["dep:rust_decimal_macros"] borsh = ["dep:borsh", "std"] c-repr = [] # Force Decimal to be repr(C) @@ -88,4 +90,8 @@ name = "comparison" path = "benches/comparison.rs" [workspace] -members = [".", "./macros"] +members = [ + ".", + "./macros" +] +resolver = "2" diff --git a/README.md b/README.md index c70280b2..9d6a4a21 100644 --- a/README.md +++ b/README.md @@ -19,26 +19,27 @@ Using [`cargo-edit`](https://crates.io/crates/cargo-edit): $ cargo add rust_decimal ``` -In addition, if you would like to use the optimized macro for convenient creation of decimals: +If you would like to use the optimized macro for convenient creation of decimals you can add `rust_decimal` with the `macros` feature flag: ```sh -$ cargo add rust_decimal_macros +$ cargo add rust_decimal --features macros ``` Alternatively, you can edit your `Cargo.toml` directly and run `cargo update`: ```toml [dependencies] -rust_decimal = "1.33" -rust_decimal_macros = "1.33" +rust_decimal = { version = "1.33", features = ["macros"] } ``` ## Usage -Decimal numbers can be created in a few distinct ways. The easiest and most efficient method of creating a Decimal is to use the procedural macro within the `rust_decimal_macros` crate: +Decimal numbers can be created in a few distinct ways. The easiest and most efficient method of creating a Decimal is to use the procedural macro that can be enabled using the `macros` feature: ```rust -// Procedural macros need importing directly +// The macros feature exposes a `dec` macro which will parse the input into a raw decimal number at compile time. +// It is also exposed when using `rust_decimal::prelude::*`. That said, you can also import the +// `rust_decimal_macros` crate and use the macro directly from there. use rust_decimal_macros::dec; let number = dec!(-1.23) + dec!(3.45); @@ -198,8 +199,8 @@ Enable `rust-fuzz` support by implementing the `Arbitrary` trait. ### `serde-float` -**Note:** it is recommended to use the `serde-with-*` features for greater control. This allows configurability at the data -level. +> **Note:** This feature applies float serialization/deserialization rules as the default method for handling `Decimal` numbers. +See also the `serde-with-*` features for greater flexibility. Enable this so that JSON serialization of `Decimal` types are sent as a float instead of a string (default). @@ -212,8 +213,8 @@ e.g. with this turned on, JSON serialization would output: ### `serde-str` -**Note:** it is recommended to use the `serde-with-*` features for greater control. This allows configurability at the data -level. +> **Note:** This feature applies string serialization/deserialization rules as the default method for handling `Decimal` numbers. +See also the `serde-with-*` features for greater flexibility. This is typically useful for `bincode` or `csv` like implementations. @@ -227,17 +228,20 @@ converting to `f64` _loses_ precision, it's highly recommended that you do NOT e ### `serde-arbitrary-precision` -**Note:** it is recommended to use the `serde-with-*` features for greater control. This allows configurability at the data -level. +> **Note:** This feature applies arbitrary serialization/deserialization rules as the default method for handling `Decimal` numbers. +See also the `serde-with-*` features for greater flexibility. 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. +Please note, this currently serializes numbers in a float like format by default, which can be an unexpected consequence. For greater +control over the serialization format, please use the `serde-with-arbitrary-precision` feature. + ### `serde-with-float` -Enable this to access the module for serializing `Decimal` types to a float. This can be use in `struct` definitions like so: +Enable this to access the module for serializing `Decimal` types to a float. This can be used in `struct` definitions like so: ```rust #[derive(Serialize, Deserialize)] @@ -254,9 +258,18 @@ pub struct OptionFloatExample { } ``` +Alternatively, if only the serialization feature is desired (e.g. to keep flexibility while deserialization): +```rust +#[derive(Serialize, Deserialize)] +pub struct FloatExample { + #[serde(serialize_with = "rust_decimal::serde::float::serialize")] + value: Decimal, +} +``` + ### `serde-with-str` -Enable this to access the module for serializing `Decimal` types to a `String`. This can be use in `struct` definitions like so: +Enable this to access the module for serializing `Decimal` types to a `String`. This can be used in `struct` definitions like so: ```rust #[derive(Serialize, Deserialize)] @@ -273,9 +286,19 @@ pub struct OptionStrExample { } ``` +This feature isn't typically required for serialization however can be useful for deserialization purposes since it does not require +a type hint. Consequently, you can force this for just deserialization by: +```rust +#[derive(Serialize, Deserialize)] +pub struct StrExample { + #[serde(deserialize_with = "rust_decimal::serde::str::deserialize")] + value: Decimal, +} +``` + ### `serde-with-arbitrary-precision` -Enable this to access the module for serializing `Decimal` types to a `String`. This can be use in `struct` definitions like so: +Enable this to access the module for deserializing `Decimal` types using the `serde_json/arbitrary_precision` feature. This can be used in `struct` definitions like so: ```rust #[derive(Serialize, Deserialize)] @@ -292,6 +315,20 @@ pub struct OptionArbitraryExample { } ``` +An unexpected consequence of this feature is that it will serialize as a float like number. To prevent this, you can +target the struct to only deserialize with the `arbitrary_precision` feature: +```rust +#[derive(Serialize, Deserialize)] +pub struct ArbitraryExample { + #[serde(deserialize_with = "rust_decimal::serde::arbitrary_precision::deserialize")] + value: Decimal, +} +``` + +This will ensure that serialization still occurs as a string. + +Please see the `examples` directory for more information regarding `serde_json` scenarios. + ### `std` Enable `std` library support. This is enabled by default, however in the future will be opt in. For now, to support `no_std` diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..c7eecde5 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,16 @@ +# Examples + +This contains some more advanced examples of using the rust decimal library of complex usage. + +All examples are crate based to demonstrate feature configurations. Examples can be run by using: + +```shell +cd examples/ +cargo run +``` + +## serde-json-scenarios + +This example shows how to use the `serde` crate to serialize and deserialize the `Decimal` type using multiple different +serialization formats. + diff --git a/examples/serde-json-scenarios/Cargo.toml b/examples/serde-json-scenarios/Cargo.toml new file mode 100644 index 00000000..5d390481 --- /dev/null +++ b/examples/serde-json-scenarios/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "serde-json-scenarios" +version = "0.0.0" +edition = "2021" +publish = false + +[workspace] + +[dependencies] +rust_decimal = { path = "../..", features = ["macros", "serde-with-arbitrary-precision"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["arbitrary_precision"]} diff --git a/examples/serde-json-scenarios/src/main.rs b/examples/serde-json-scenarios/src/main.rs new file mode 100644 index 00000000..168950a0 --- /dev/null +++ b/examples/serde-json-scenarios/src/main.rs @@ -0,0 +1,56 @@ +use rust_decimal::prelude::*; + +type ExampleResult = Result<(), Box>; + +fn main() -> ExampleResult { + demonstrate_default_behavior()?; + demonstrate_arbitrary_precision_deserialization_with_string_serialization()?; + Ok(()) +} + +/// The default behavior of the library always expects string results. That is, it will serialize the +/// Decimal as string, but also expect a string when deserializing. +/// Note: this is not enough for bincode representations since there is no deserialization hint that the +/// field is a string. +fn demonstrate_default_behavior() -> ExampleResult { + #[derive(serde::Serialize, serde::Deserialize)] + struct Total { + value: Decimal, + } + let total = Total { value: dec!(1.23) }; + let serialized = serde_json::to_string(&total)?; + assert_eq!(r#"{"value":"1.23"}"#, serialized); + + // If we try to deserialize the same string we should succeed + let deserialized: Total = serde_json::from_str(&serialized)?; + assert_eq!(dec!(1.23), deserialized.value); + + // Technically, by default we also support deserializing from a number, however this is doing a float + // conversion and is not recommended. + let deserialized: Total = serde_json::from_str(r#"{"value":1.23}"#)?; + assert_eq!(dec!(1.23), deserialized.value); + Ok(()) +} + +/// This demonstrates using arbitrary precision for a decimal value - even though the +/// default string serialization behavior is baked in. +fn demonstrate_arbitrary_precision_deserialization_with_string_serialization() -> ExampleResult { + #[derive(serde::Serialize, serde::Deserialize)] + struct Total { + #[serde(deserialize_with = "rust_decimal::serde::arbitrary_precision::deserialize")] + value: Decimal, + } + + let total = Total { value: dec!(1.23) }; + let serialized = serde_json::to_string(&total)?; + assert_eq!(r#"{"value":"1.23"}"#, serialized); + + // If we try to deserialize the same string we should succeed + let deserialized: Total = serde_json::from_str(&serialized)?; + assert_eq!(dec!(1.23), deserialized.value); + + // If we try to deserialize a float then this will succeed as well + let deserialized: Total = serde_json::from_str(r#"{"value":1.23}"#)?; + assert_eq!(dec!(1.23), deserialized.value); + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 59900e65..56ecaaf6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,9 @@ pub use error::Error; #[cfg(feature = "maths")] pub use maths::MathematicalOps; +#[cfg(feature = "macros")] +pub use rust_decimal_macros::dec; + /// A convenience module appropriate for glob imports (`use rust_decimal::prelude::*;`). pub mod prelude { #[cfg(feature = "maths")] @@ -64,6 +67,8 @@ pub mod prelude { pub use crate::{Decimal, RoundingStrategy}; pub use core::str::FromStr; pub use num_traits::{FromPrimitive, One, Signed, ToPrimitive, Zero}; + #[cfg(feature = "macros")] + pub use rust_decimal_macros::dec; } #[cfg(all(feature = "diesel1", not(feature = "diesel2")))]