diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 71b6087..cf61de0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -124,12 +124,19 @@ jobs: name: - stable - beta + - stable / wasm + - beta / wasm include: - # This allows us to define targets in the future (e.g. wasm32-unknown-unknown) - name: stable rust: stable - name: beta rust: beta + - name: stable / wasm + rust: stable + target: wasm32-unknown-unknown + - name: beta / wasm + rust: beta + target: wasm32-unknown-unknown steps: - uses: actions/checkout@v4 @@ -148,11 +155,6 @@ jobs: - uses: davidB/rust-cargo-make@v1 - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get -y install libpq-dev - - name: Run no_std tests run: cargo make test-no-std env: @@ -187,12 +189,19 @@ jobs: name: - stable - beta + - stable / wasm + - beta / wasm include: - # This allows us to define targets in the future (e.g. wasm32-unknown-unknown) - name: stable rust: stable - name: beta rust: beta + - name: stable / wasm + rust: stable + target: wasm32-unknown-unknown + - name: beta / wasm + rust: beta + target: wasm32-unknown-unknown steps: - uses: actions/checkout@v4 @@ -211,11 +220,6 @@ jobs: - uses: davidB/rust-cargo-make@v1 - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get -y install libpq-dev - - name: Run serde tests run: cargo make test-serde env: diff --git a/Cargo.toml b/Cargo.toml index 0dae4ad..d760e4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] authors = ["Paul Mason "] build = "build.rs" -categories = ["science","mathematics","data-structures"] +categories = ["science", "mathematics", "data-structures"] description = "Decimal number implementation written in pure Rust suitable for financial and fixed-precision calculations." documentation = "https://docs.rs/rust_decimal/" edition = "2021" -exclude = [ "tests/generated/*" ] -keywords = ["decimal","financial","fixed","precision","number"] +exclude = ["tests/generated/*"] +keywords = ["decimal", "financial", "fixed", "precision", "number"] license = "MIT" name = "rust_decimal" readme = "./README.md" @@ -17,6 +17,7 @@ version = "1.35.0" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] +targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [dependencies] arbitrary = { default-features = false, optional = true, version = "1.0" } @@ -37,19 +38,27 @@ 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" } +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = { default-features = false, version = "0.2" } + [dev-dependencies] -bincode = { default-features = false, version = "1.0" } -bytes = { default-features = false, version = "1.0" } 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 = { 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" } + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen = { default-features = false, version = "0.2" } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +bincode = { default-features = false, version = "1.0" } +bytes = { default-features = false, version = "1.0" } +futures = { default-features = false, version = "0.3" } +rand = { default-features = false, features = ["getrandom"], version = "0.8" } postgres = { default-features = false, version = "0.19" } +tokio = { default-features = false, features = ["macros", "rt-multi-thread", "test-util"], version = "1.0" } tokio-postgres = { default-features = false, version = "0.7" } [features] @@ -77,7 +86,7 @@ rkyv = ["dep:rkyv"] rkyv-safe = ["rkyv/validation"] rocket-traits = ["dep:rocket"] rust-fuzz = ["dep:arbitrary"] -serde = ["dep:serde"] +serde = ["dep:serde", "wasm-bindgen/serde"] serde-arbitrary-precision = ["serde-with-arbitrary-precision"] serde-bincode = ["serde-str"] # Backwards compatability serde-float = ["serde-with-float"] @@ -85,7 +94,7 @@ serde-str = ["serde-with-str"] serde-with-arbitrary-precision = ["serde", "serde_json/arbitrary_precision", "serde_json/std"] serde-with-float = ["serde"] serde-with-str = ["serde"] -std = ["arrayvec/std", "borsh?/std", "bytes?/std", "rand?/std", "rkyv?/std", "serde?/std", "serde_json?/std"] +std = ["arrayvec/std", "wasm-bindgen/std", "borsh?/std", "bytes?/std", "rand?/std", "rkyv?/std", "serde?/std", "serde_json?/std"] tokio-pg = ["db-tokio-postgres"] # Backwards compatability [[bench]] diff --git a/README.md b/README.md index cf22c16..4e7c072 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ assert_eq!(total, dec!(27.26)); * [rocket-traits](#rocket-traits) * [rust-fuzz](#rust-fuzz) * [std](#std) +* [wasm](#wasm) **Database** @@ -331,6 +332,12 @@ Please see the `examples` directory for more information regarding `serde_json` Enable `std` library support. This is enabled by default, however in the future will be opt in. For now, to support `no_std` libraries, this crate can be compiled with `--no-default-features`. +### `wasm` + +Enable [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) support which makes `Decimal` compatible with the +`wasm_bindgen` attribute macro and exposes `fromNumber()` and `toNumber()` methods to convert between `Decimal` and +the primitive `number` type across boundaries. + ## Building Please refer to the [Build document](BUILD.md) for more information on building and testing Rust Decimal. diff --git a/make/tests.toml b/make/tests.toml index dd53620..0d742f0 100644 --- a/make/tests.toml +++ b/make/tests.toml @@ -6,31 +6,77 @@ extend = [ { path = "tests/serde.toml" } ] +[tasks.test-runner] +private = true +env_scripts = [ + ''' + #!@duckscript + target_value = get_env CI_DECIMAL_TEST_TARGET + target_set = is_set ${target_value} + empty = is_empty ${value} + if target_set and not empty + set_env TEST_TARGET ${target_value} + end + ''' +] +dependencies = [ + "test-runner-default", + "test-runner-ci-target", + "test-feature-runner-default", + "test-feature-runner-ci-target" +] + +[tasks.test-runner-default] +private = true +condition = { env_not_set = ["TEST_FEATURES", "TEST_TARGET"] } +command = "cargo" +args = ["test", "--workspace", "--no-default-features", "${TEST_FILTER}"] + +[tasks.test-runner-ci-target] +private = true +condition = { env_not_set = ["TEST_FEATURES"], env_set = ["TEST_TARGET"] } +command = "cargo" +args = ["test", "--workspace", "--no-default-features", "--target=${TEST_TARGET}", "${TEST_FILTER}"] + +[tasks.test-feature-runner-default] +private = true +condition = { env_set = ["TEST_FEATURES"], env_not_set = ["TEST_TARGET"] } +command = "cargo" +args = ["test", "--workspace", "--no-default-features", "--features=${TEST_FEATURES}", "${TEST_FILTER}"] + +[tasks.test-feature-runner-ci-target] +private = true +condition = { env_set = ["TEST_FEATURES", "TEST_TARGET"] } +command = "cargo" +args = ["test", "--workspace", "--no-default-features", "--features=${TEST_FEATURES}", "--target=${TEST_TARGET}", "${TEST_FILTER}"] + [tasks.test] clear = true dependencies = ["test-no-std", "test-default"] -# Some tests need cleaning before hand to ensure we don't inadvertantly test +# Some tests need cleaning beforehand to ensure we don't inadvertantly test # using prebuilt logic [tasks.clean-no-std] alias = "clean" [tasks.test-no-std] -dependencies = ["clean-no-std"] -command = "cargo" -args = ["test", "--no-default-features"] +dependencies = [ + "clean-no-std" +] +env = { TEST_FILTER = "" } +run_task = "test-runner" [tasks.clean-default] alias = "clean" [tasks.test-default] dependencies = ["clean-default"] -command = "cargo" -args = ["test", "--workspace", "--features=default"] +env = { TEST_FEATURES = "default", TEST_FILTER = "" } +run_task = "test-runner" [tasks.test-legacy-ops] -command = "cargo" -args = ["test", "--workspace", "--features=legacy-ops"] +env = { TEST_FEATURES = "legacy-ops", TEST_FILTER = "" } +run_task = "test-runner" # This should reflect the steps in github [tasks.test-all] diff --git a/make/tests/misc.toml b/make/tests/misc.toml index a149971..b88ba9e 100644 --- a/make/tests/misc.toml +++ b/make/tests/misc.toml @@ -9,29 +9,29 @@ dependencies = [ ] [tasks.test-proptest] -command = "cargo" -args = ["test", "--workspace", "--no-default-features", "--features=proptest", "proptest_tests", "--", "--skip", "generated"] +env = { TEST_FEATURES = "proptest", TEST_FILTER = "proptest_tests" } +run_task = "test-runner" [tasks.test-rust-fuzz] -command = "cargo" -args = ["test", "--workspace", "--no-default-features", "--features=rust-fuzz", "rust_fuzz_tests", "--", "--skip", "generated"] +env = { TEST_FEATURES = "rust-fuzz", TEST_FILTER = "rust_fuzz_tests" } +run_task = "test-runner" [tasks.test-rocket-traits] -command = "cargo" -args = ["test", "--workspace", "--features=rocket-traits", "rocket_tests"] +env = { TEST_FEATURES = "rocket-traits", TEST_FILTER = "rocket_tests" } +run_task = "test-runner" [tasks.test-borsh] -command = "cargo" -args = ["test", "--workspace", "--features=borsh", "borsh_tests", "--", "--skip", "generated"] +env = { TEST_FEATURES = "borsh", TEST_FILTER = "borsh_tests" } +run_task = "test-runner" [tasks.test-ndarray] -command = "cargo" -args = ["test", "--workspace", "--features=ndarray", "nd_array_tests", "--", "--skip", "generated"] +env = { TEST_FEATURES = "ndarray", TEST_FILTER = "ndarray_tests" } +run_task = "test-runner" [tasks.test-rkyv] -command = "cargo" -args = ["test", "--workspace", "--features=rkyv", "--features=rkyv-safe", "rkyv_tests", "--", "--skip", "generated"] +env = { TEST_FEATURES = "rkyv", TEST_FILTER = "rkyv_tests" } +run_task = "test-runner" [tasks.test-rand] -command = "cargo" -args = ["test", "--workspace", "--features=rand", "rand_tests", "--", "--skip", "generated"] +env = { TEST_FEATURES = "rand", TEST_FILTER = "rand_tests" } +run_task = "test-runner" diff --git a/src/decimal.rs b/src/decimal.rs index 20dd9c0..d5dfbc7 100644 --- a/src/decimal.rs +++ b/src/decimal.rs @@ -27,6 +27,8 @@ use num_traits::float::FloatCore; use num_traits::{FromPrimitive, Num, One, Signed, ToPrimitive, Zero}; #[cfg(feature = "rkyv")] use rkyv::{Archive, Deserialize, Serialize}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::wasm_bindgen; /// The smallest value that can be represented by this decimal type. const MIN: Decimal = Decimal { @@ -121,6 +123,7 @@ pub struct UnpackedDecimal { archive_attr(derive(Clone, Copy, Debug)) )] #[cfg_attr(feature = "rkyv-safe", archive(check_bytes))] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] pub struct Decimal { // Bits 0-15: unused // Bits 16-23: Contains "e", a value between 0-28 that indicates the scale diff --git a/src/lib.rs b/src/lib.rs index d49db64..9fd0ced 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,13 +18,19 @@ mod arithmetic_impls; mod fuzz; #[cfg(feature = "maths")] mod maths; -#[cfg(any(feature = "db-diesel1-mysql", feature = "db-diesel2-mysql"))] +#[cfg(all( + any(feature = "db-diesel1-mysql", feature = "db-diesel2-mysql"), + not(target_arch = "wasm32") +))] mod mysql; -#[cfg(any( - feature = "db-tokio-postgres", - feature = "db-postgres", - feature = "db-diesel1-postgres", - feature = "db-diesel2-postgres", +#[cfg(all( + any( + feature = "db-tokio-postgres", + feature = "db-postgres", + feature = "db-diesel1-postgres", + feature = "db-diesel2-postgres", + ), + not(target_arch = "wasm32") ))] mod postgres; #[cfg(feature = "proptest")] @@ -52,6 +58,8 @@ mod serde; ) ))] pub mod serde; +#[cfg(target_arch = "wasm32")] +pub mod wasm; pub use decimal::{Decimal, RoundingStrategy}; pub use error::Error; @@ -72,11 +80,11 @@ pub mod prelude { // pub use rust_decimal_macros::dec; } -#[cfg(all(feature = "diesel1", not(feature = "diesel2")))] +#[cfg(all(feature = "diesel1", not(feature = "diesel2"), not(target_arch = "wasm32")))] #[macro_use] extern crate diesel1 as diesel; -#[cfg(feature = "diesel2")] +#[cfg(all(feature = "diesel2", not(target_arch = "wasm32")))] extern crate diesel2 as diesel; /// Shortcut for `core::result::Result`. Useful to distinguish diff --git a/src/wasm.rs b/src/wasm.rs new file mode 100644 index 0000000..b377f20 --- /dev/null +++ b/src/wasm.rs @@ -0,0 +1,26 @@ +use num_traits::{FromPrimitive, ToPrimitive}; +use wasm_bindgen::prelude::*; + +use crate::Decimal; + +#[wasm_bindgen] +impl Decimal { + /// Returns a new `Decimal` object instance by converting a primitive number. + #[wasm_bindgen(js_name = fromNumber)] + #[must_use] + pub fn from_number(value: f64) -> Option { + Decimal::from_f64(value) + } + + /// Returns the value of this `Decimal` converted to a primitive number. + /// + /// # Caution + /// At the time of writing this implementation the conversion from `Decimal` to `f64` cannot + /// fail. To prevent undefined behavior in case the underlying implementation changes `f64::NAN` + /// is returned as a stable fallback value. + #[wasm_bindgen(js_name = toNumber)] + #[must_use] + pub fn to_number(&self) -> f64 { + self.to_f64().unwrap_or(f64::NAN) + } +}