diff --git a/Cargo.lock b/Cargo.lock index b96d94f5..d7269978 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -148,16 +159,30 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" dependencies = [ + "borsh-derive", "cfg_aliases", ] +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "bson" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "068208f2b6fcfa27a7f1ee37488d2bb8ba2640f68f5475d08e1d9130696aba59" dependencies = [ - "ahash", + "ahash 0.8.11", "base64", "bitvec", "hex", @@ -181,6 +206,28 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -496,13 +543,22 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -970,6 +1026,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -988,6 +1053,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.37" @@ -1033,6 +1118,60 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1057,6 +1196,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "semver" version = "1.0.23" @@ -1127,6 +1272,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "0.3.11" @@ -1442,6 +1593,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.41.1" @@ -1452,6 +1618,36 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ad0b7ae9cfeef5605163839cb9221f453399f15cfb5c10be9885fcf56611f9" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + [[package]] name = "tracing" version = "0.1.41" @@ -1507,6 +1703,7 @@ dependencies = [ "indexmap", "jiff", "ordered-float", + "rust_decimal", "semver", "serde", "serde_json", @@ -1744,6 +1941,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/README.md b/README.md index 76f7ffff..5e116bce 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ We are currently waiting for [#54140](https://github.com/rust-lang/rust/issues/5 | smol_str-impl | Implement `TS` for types from *smol_str* | | tokio-impl | Implement `TS` for types from *tokio* | | jiff-impl | Implement `TS` for types from *jiff* | +| rust_decimal-impl | Implement `TS` for types from *rust_de | | arrayvec-impl | Implement `TS` for types from *arrayvec* | ### Contributing diff --git a/ts-rs/Cargo.toml b/ts-rs/Cargo.toml index 967f8820..a34c988e 100644 --- a/ts-rs/Cargo.toml +++ b/ts-rs/Cargo.toml @@ -34,6 +34,7 @@ heapless-impl = ["heapless"] semver-impl = ["semver"] smol_str-impl = ["smol_str"] serde-json-impl = ["serde_json"] +rust-decimal-impl = ["rust_decimal", "serde", "rust_decimal/serde"] no-serde-warnings = ["ts-rs-macros/no-serde-warnings"] tokio-impl = ["tokio"] jiff-impl = ["jiff"] @@ -61,7 +62,9 @@ semver = { version = "1", optional = true } smol_str = { version = "0.3", optional = true } indexmap = { version = "2", optional = true } ordered-float = { version = ">= 3, < 6", optional = true } +serde = { version = "1.0", optional = true } serde_json = { version = "1", optional = true } tokio = { version = "1", features = ["sync"], optional = true } jiff = { version = "0.2", optional = true } +rust_decimal = { version = "1", optional = true } arrayvec = { version = ">= 0.6, < 0.8", optional = true } diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 524296ff..63c25454 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -122,6 +122,7 @@ //! | smol_str-impl | Implement `TS` for types from *smol_str* | //! | tokio-impl | Implement `TS` for types from *tokio* | //! | jiff-impl | Implement `TS` for types from *jiff* | +//! | rust_decimal-impl | Implement `TS` for types from *rust_de | //! | arrayvec-impl | Implement `TS` for types from *arrayvec* | //! //! ## Contributing @@ -153,6 +154,8 @@ mod chrono; mod export; #[cfg(feature = "jiff-impl")] mod jiff; +#[cfg(feature = "rust-decimal-impl")] +mod rust_decimal; #[cfg(feature = "serde-json-impl")] mod serde_json; #[cfg(feature = "tokio-impl")] diff --git a/ts-rs/src/rust_decimal.rs b/ts-rs/src/rust_decimal.rs new file mode 100644 index 00000000..c84c1020 --- /dev/null +++ b/ts-rs/src/rust_decimal.rs @@ -0,0 +1,270 @@ +use std::sync::OnceLock; + +use rust_decimal::Decimal; + +use crate::{Config, TS}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DecimalSerdeShape { + String, + Number, +} + +fn detect_decimal_serde_shape() -> DecimalSerdeShape { + // `rust_decimal::Decimal` has an inherent method named `serialize`, so we must call the + // *trait* method explicitly to invoke serde serialization. + match serde::Serialize::serialize(&Decimal::default(), DecimalShapeProbeSerializer) { + Ok(shape) => shape, + Err(_) => DecimalSerdeShape::String, + } +} + +/// A minimal serializer which only cares whether `Serialize` emits a string or a number. +/// +/// Any unexpected serialization shape is treated as an error and falls back to `string`. +struct DecimalShapeProbeSerializer; + +impl serde::ser::Serializer for DecimalShapeProbeSerializer { + type Ok = DecimalSerdeShape; + type Error = ShapeProbeError; + + type SerializeSeq = serde::ser::Impossible; + type SerializeTuple = serde::ser::Impossible; + type SerializeTupleStruct = serde::ser::Impossible; + type SerializeTupleVariant = serde::ser::Impossible; + type SerializeMap = serde::ser::Impossible; + type SerializeStruct = serde::ser::Impossible; + type SerializeStructVariant = serde::ser::Impossible; + + fn serialize_str(self, _value: &str) -> Result { + Ok(DecimalSerdeShape::String) + } + + fn serialize_f32(self, _value: f32) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_f64(self, _value: f64) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_i8(self, _value: i8) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_i16(self, _value: i16) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_i32(self, _value: i32) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_i64(self, _value: i64) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_i128(self, _value: i128) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_u8(self, _value: u8) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_u16(self, _value: u16) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_u32(self, _value: u32) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_u64(self, _value: u64) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_u128(self, _value: u128) -> Result { + Ok(DecimalSerdeShape::Number) + } + + fn serialize_bool(self, _value: bool) -> Result { + Err(ShapeProbeError) + } + + fn serialize_char(self, _value: char) -> Result { + Err(ShapeProbeError) + } + + fn serialize_bytes(self, _value: &[u8]) -> Result { + Err(ShapeProbeError) + } + + fn serialize_none(self) -> Result { + Err(ShapeProbeError) + } + + fn serialize_some(self, _value: &T) -> Result + where + T: serde::Serialize, + { + Err(ShapeProbeError) + } + + fn serialize_unit(self) -> Result { + Err(ShapeProbeError) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(ShapeProbeError) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + Err(ShapeProbeError) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result + where + T: serde::Serialize, + { + Err(ShapeProbeError) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: serde::Serialize, + { + Err(ShapeProbeError) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(ShapeProbeError) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(ShapeProbeError) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(ShapeProbeError) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(ShapeProbeError) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(ShapeProbeError) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(ShapeProbeError) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(ShapeProbeError) + } +} + +#[derive(Debug)] +struct ShapeProbeError; + +impl std::fmt::Display for ShapeProbeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("unsupported serialization shape") + } +} + +impl std::error::Error for ShapeProbeError {} + +impl serde::ser::Error for ShapeProbeError { + fn custom(_msg: T) -> Self { + ShapeProbeError + } +} + +fn decimal_ts_binding() -> &'static str { + static DECIMAL_BINDING: OnceLock<&'static str> = OnceLock::new(); + + DECIMAL_BINDING.get_or_init(|| match detect_decimal_serde_shape() { + DecimalSerdeShape::Number => "number", + DecimalSerdeShape::String => "string", + }) +} + +impl TS for Decimal { + type WithoutGenerics = Decimal; + type OptionInnerType = Decimal; + + fn decl(cfg: &Config) -> String { + panic!("{} cannot be declared", ::name(cfg)) + } + + fn decl_concrete(cfg: &Config) -> String { + panic!("{} cannot be declared", ::name(cfg)) + } + + fn name(_: &Config) -> String { + decimal_ts_binding().to_owned() + } + + fn inline(cfg: &Config) -> String { + ::name(cfg) + } + + fn inline_flattened(cfg: &Config) -> String { + panic!("{} cannot be flattened", ::name(cfg)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decimal_ts_name_matches_actual_serialization() { + let decimal = Decimal::new(123, 2); // 1.23 + let json = serde_json::to_value(&decimal).unwrap(); + + match Decimal::name(&Config::default()).as_str() { + "number" => assert!(json.is_number()), + "string" => assert!(json.is_string()), + other => panic!("unexpected type: {}", other), + } + } +}