Skip to content

Commit

Permalink
Introduces macros feature and cleans up serde documentation a littl…
Browse files Browse the repository at this point in the history
…e bit (#628)
  • Loading branch information
paupino authored Dec 1, 2023
1 parent c4d4d3e commit 829355a
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 17 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -42,14 +43,15 @@ 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" }
version-sync = { default-features = false, features = ["html_root_url_updated", "markdown_deps_updated"], version = "0.9" }

[features]
default = ["serde", "std"]
macros = ["dep:rust_decimal_macros"]

borsh = ["dep:borsh", "std"]
c-repr = [] # Force Decimal to be repr(C)
Expand Down Expand Up @@ -88,4 +90,8 @@ name = "comparison"
path = "benches/comparison.rs"

[workspace]
members = [".", "./macros"]
members = [
".",
"./macros"
]
resolver = "2"
67 changes: 52 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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).

Expand All @@ -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.

Expand All @@ -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)]
Expand All @@ -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)]
Expand All @@ -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)]
Expand All @@ -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`
Expand Down
16 changes: 16 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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/<example-name>
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.

12 changes: 12 additions & 0 deletions examples/serde-json-scenarios/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]}
56 changes: 56 additions & 0 deletions examples/serde-json-scenarios/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use rust_decimal::prelude::*;

type ExampleResult = Result<(), Box<dyn std::error::Error>>;

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(())
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,18 @@ 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")]
pub use crate::maths::MathematicalOps;
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")))]
Expand Down

0 comments on commit 829355a

Please sign in to comment.