diff --git a/CHANGELOG.md b/CHANGELOG.md index e8987d9d..7532ddac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## [Unreleased] ### Added - `Ruby::time_nano_new`, `Time::tv_sec`, `Time::tv_usec` and `Time::tv_nsec`. +- The `chrono` feature can be enabled to allow automatic conversions + between `chrono::DateTime` and `chrono::DateTime` and Ruby `Time` objects. ### Changed - Conversions between Ruby's `Time` and Rust's `SystemTime` now preserve diff --git a/Cargo.lock b/Cargo.lock index fae067be..ca9bcfa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "bindgen" version = "0.69.1" @@ -37,12 +58,24 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytes" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "cc" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" + [[package]] name = "cexpr" version = "0.6.0" @@ -58,6 +91,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "clang-sys" version = "1.6.1" @@ -69,12 +116,50 @@ dependencies = [ "libloading", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -103,11 +188,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + [[package]] name = "magnus" version = "0.7.1" dependencies = [ "bytes", + "chrono", "magnus", "magnus-macros", "rb-sys", @@ -147,6 +239,21 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -271,6 +378,60 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "winapi" version = "0.3.9" @@ -292,3 +453,76 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 8b13691f..ca73dba6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,12 +23,14 @@ exclude = [ [features] default = ["old-api"] bytes = ["dep:bytes"] +chrono = ["dep:chrono"] embed = ["rb-sys/link-ruby"] old-api = [] rb-sys = [] [dependencies] bytes = { version = "1", optional = true } +chrono = { version = "0.4.38", optional = true } magnus-macros = { version = "0.6.0", path = "magnus-macros" } rb-sys = { version = "0.9.85", default-features = false, features = [ "bindgen-rbimpls", @@ -42,6 +44,7 @@ magnus = { path = ".", default-features = false, features = [ "embed", "rb-sys", "bytes", + "chrono", ] } rb-sys = { version = "0.9", default-features = false, features = [ "stable-api-compiled-fallback", diff --git a/README.md b/README.md index 202287c1..3be0e8ac 100644 --- a/README.md +++ b/README.md @@ -319,7 +319,7 @@ See `magnus::TryConvert` for more details. | `i8`,`i16`,`i32`,`i64`,`isize`, `magnus::Integer` | `Integer`, `#to_int` | | `u8`,`u16`,`u32`,`u64`,`usize` | `Integer`, `#to_int` | | `f32`,`f64`, `magnus::Float` | `Float`, `Numeric` | -| `String`, `PathBuf`, `char`, `magnus::RString`, `bytes::Bytes`\*\*\* | `String`, `#to_str` | +| `String`, `PathBuf`, `char`, `magnus::RString`, `bytes::Bytes`‡ | `String`, `#to_str` | | `magnus::Symbol` | `Symbol`, `#to_sym` | | `bool` | any object | | `magnus::Range` | `Range` | @@ -329,17 +329,19 @@ See `magnus::TryConvert` for more details. | `[T; N]` | `[T]`, `#to_ary` | | `magnus::RArray` | `Array`, `#to_ary` | | `magnus::RHash` | `Hash`, `#to_hash` | -| `std::time::SystemTime`, `magnus::Time` | `Time` | +| `std::time::SystemTime`, `magnus::Time`, `chrono::DateTime`§ | `Time` | | `magnus::Value` | any object | | `Vec`\* | `[T]`, `#to_ary` | | `HashMap`\* | `{K => V}`, `#to_hash` | -| `&T`, `typed_data::Obj` where `T: TypedData`\*\* | instance of `::class()` | +| `&T`, `typed_data::Obj` where `T: TypedData`† | instance of `::class()` | \* when converting to `Vec` and `HashMap` the types of `T`/`K`,`V` must be native Rust types. -\*\* see the `wrap` macro. +† see the `wrap` macro. -\*\*\* when the `bytes` feature is enabled +‡ when the `bytes` feature is enabled + +§ when the `chrono` feature is enabled; `T` can be `Utc` or `FixedOffset`. ### Rust returning / passing values to Ruby @@ -360,9 +362,9 @@ and `magnus::ArgList` for some additional details. | `(T, U)`, `(T, U, V)`, etc, `[T; N]`, `Vec` | `Array` | | `HashMap` | `Hash` | | `std::time::SystemTime` | `Time` | -| `T`, `typed_data::Obj` where `T: TypedData`\*\* | instance of `::class()` | +| `T`, `typed_data::Obj` where `T: TypedData`\* | instance of `::class()` | -\*\* see the `wrap` macro. +\* see the `wrap` macro. ### Conversions via Serde diff --git a/src/time.rs b/src/time.rs index 1d4f2a4c..114857f0 100644 --- a/src/time.rs +++ b/src/time.rs @@ -253,6 +253,32 @@ impl IntoValue for SystemTime { } } +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +impl IntoValue for chrono::DateTime { + #[inline] + fn into_value_with(self, ruby: &Ruby) -> Value { + let delta = self.signed_duration_since(Self::UNIX_EPOCH); + ruby.time_nano_new(delta.num_seconds(), delta.subsec_nanos() as _) + .unwrap() + .as_value() + } +} + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +impl IntoValue for chrono::DateTime { + #[inline] + fn into_value_with(self, ruby: &Ruby) -> Value { + use chrono::{DateTime, Utc}; + let epoch = DateTime::::UNIX_EPOCH.with_timezone(&self.timezone()); + let delta = self.signed_duration_since(epoch); + ruby.time_nano_new(delta.num_seconds(), delta.subsec_nanos() as _) + .unwrap() + .as_value() + } +} + impl Object for Time {} unsafe impl private::ReprValue for Time {} @@ -294,3 +320,48 @@ impl TryConvert for SystemTime { } } } + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +impl TryConvert for chrono::DateTime { + fn try_convert(val: Value) -> Result { + let mut timespec = timespec { + tv_sec: 0, + tv_nsec: 0, + }; + protect(|| unsafe { + timespec = rb_time_timespec(val.as_rb_value()); + Ruby::get_unchecked().qnil() + })?; + if timespec.tv_sec >= 0 && timespec.tv_nsec >= 0 { + let mut duration = Duration::from_secs(timespec.tv_sec as _); + duration += Duration::from_nanos(timespec.tv_nsec as _); + Ok(Self::UNIX_EPOCH + duration) + } else { + Err(Error::new( + Ruby::get_with(val).exception_arg_error(), + "time must not be negative", + )) + } + } +} + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +impl TryConvert for chrono::DateTime { + fn try_convert(val: Value) -> Result { + use chrono::{DateTime, FixedOffset, Utc}; + let offset: i32 = val.funcall("utc_offset", ())?; + let dt: DateTime = TryConvert::try_convert(val)?; + let tz = match FixedOffset::east_opt(offset) { + Some(tz) => tz, + None => { + return Err(Error::new( + Ruby::get_with(val).exception_arg_error(), + "invalid UTC offset", + )) + } + }; + Ok(dt.with_timezone(&tz)) + } +} diff --git a/tests/time.rs b/tests/time.rs new file mode 100644 index 00000000..11897ece --- /dev/null +++ b/tests/time.rs @@ -0,0 +1,24 @@ +#[test] +#[cfg(feature = "chrono")] +fn it_supports_chrono() { + use chrono::{DateTime, Datelike, FixedOffset, Utc}; + let ruby = unsafe { magnus::embed::init() }; + + let t = ruby.eval::>("Time.at(0, 10, :nsec)").unwrap(); + assert_eq!(t.year(), 1970); + assert_eq!(t.month(), 1); + assert_eq!(t.day(), 1); + assert_eq!(t.timestamp_subsec_nanos(), 10); + + let dt = ruby + .eval::>(r#"Time.new(1971, 1, 1, 2, 2, 2.0000001, "Z")"#) + .unwrap(); + assert_eq!(&dt.to_rfc3339(), "1971-01-01T02:02:02.000000099+00:00"); + + let dt = ruby + .eval::>( + r#"Time.new(2022, 5, 31, 9, 8, 123456789/1000000000r, "-07:00")"#, + ) + .unwrap(); + assert_eq!(&dt.to_rfc3339(), "2022-05-31T09:08:00.123456789-07:00"); +}