diff --git a/.gitignore b/.gitignore index 1b9ff896..000e572d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target Cargo.lock /.idea/ +.vscode diff --git a/tarantool/Cargo.toml b/tarantool/Cargo.toml index 3cca52a2..4def4808 100644 --- a/tarantool/Cargo.toml +++ b/tarantool/Cargo.toml @@ -44,6 +44,7 @@ uuid = "0.8.2" futures = "0.3.25" linkme = "0.2.10" tester = { version = "0.7.0", optional = true } +time = "0.3.17" [target.'cfg(not(all(target_arch = "aarch64", target_os = "macos")))'.dependencies] va_list = "0.1.3" @@ -58,3 +59,6 @@ network_client = [] test = ["tester"] all = ["default", "schema", "defer", "test"] internal_test = ["test", "tlua/test"] + +[dev-dependencies] +time-macros = "0.2.6" diff --git a/tarantool/src/datetime.rs b/tarantool/src/datetime.rs new file mode 100644 index 00000000..8859ed80 --- /dev/null +++ b/tarantool/src/datetime.rs @@ -0,0 +1,175 @@ +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::fmt::Display; +use std::os::raw::c_char; +use time::{Duration, UtcOffset}; + +type Inner = time::OffsetDateTime; + +const MP_DATETIME: std::os::raw::c_char = 4; + +#[derive(Debug, Copy, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Datetime { + inner: Inner, +} + +impl Datetime { + #[inline(always)] + pub fn from_inner(inner: Inner) -> Self { + inner.into() + } + + #[inline(always)] + pub fn into_inner(self) -> Inner { + self.into() + } + + /// Convert an array of bytes in the endian order into a `DateTime`. + #[inline(always)] + pub fn from_bytes(bytes: [u8; 16]) -> Result> { + // pub fn from_bytes(bytes: [u8; 16]) -> Self { + let mut sec_bytes: [u8; 8] = [0; 8]; + sec_bytes.copy_from_slice(&bytes[0..8]); + let mut nsec_bytes: [u8; 4] = [0; 4]; + nsec_bytes.copy_from_slice(&bytes[8..12]); + let mut tzoffest_bytes: [u8; 2] = [0; 2]; + tzoffest_bytes.copy_from_slice(&bytes[12..14]); + + let secs = i64::from_le_bytes(sec_bytes); + let nsecs = u32::from_le_bytes(nsec_bytes); + let tzoffset: i32 = i16::from_le_bytes(tzoffest_bytes).into(); + + let dt = Inner::from_unix_timestamp(secs)? + .to_offset(UtcOffset::from_whole_seconds(tzoffset * 60)?) + + Duration::nanoseconds(nsecs as i64); + + Ok(dt.into()) + } + + /// Convert a slice of bytes in the little endian order into a `DateTime`. Return + /// `None` if there's not enough bytes in the slice. + #[inline(always)] + pub fn try_from_slice(bytes: &[u8]) -> Option { + let b = std::convert::TryInto::try_into(bytes).ok(); + if let Some(b) = b { + Self::from_bytes(b).ok() + } else { + None + } + } + + /// Return an array of bytes in the little endian order + #[inline(always)] + pub fn as_bytes(&self) -> [u8; 16] { + let mut buf: [u8; 16] = [0; 16]; + + buf[0..8].copy_from_slice(&self.inner.unix_timestamp().to_le_bytes()); + buf[8..12].copy_from_slice(&self.inner.nanosecond().to_le_bytes()); + buf[12..14].copy_from_slice(&self.inner.offset().whole_minutes().to_le_bytes()); + + buf + } +} + +impl From for Datetime { + #[inline(always)] + fn from(inner: Inner) -> Self { + Self { inner } + } +} + +impl From for Inner { + #[inline(always)] + fn from(dt: Datetime) -> Self { + dt.inner + } +} + +impl Display for Datetime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.inner.fmt(f) + } +} + +//////////////////////////////////////////////////////////////////////////////// +/// Tuple +//////////////////////////////////////////////////////////////////////////////// + +impl serde::Serialize for Datetime { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + #[derive(Serialize)] + struct _ExtStruct((c_char, serde_bytes::ByteBuf)); + + let data = self.as_bytes(); + _ExtStruct((MP_DATETIME, serde_bytes::ByteBuf::from(&data as &[_]))).serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for Datetime { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct _ExtStruct((c_char, serde_bytes::ByteBuf)); + + let _ExtStruct((kind, bytes)) = serde::Deserialize::deserialize(deserializer)?; + + if kind != MP_DATETIME { + return Err(serde::de::Error::custom(format!( + "Expected Datetime, found msgpack ext #{}", + kind + ))); + } + + let data = bytes.into_vec(); + Self::try_from_slice(&data).ok_or_else(|| { + serde::de::Error::custom(format!( + "Not enough bytes for Datetime: expected 16, got {}", + data.len() + )) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use time_macros::datetime; + + #[test] + pub fn serialize() { + let exp = [ + 216, 4, 23, 11, 79, 101, 0, 0, 0, 0, 208, 208, 28, 21, 76, 255, 0, 0, + ]; + let dt: Datetime = datetime!(2023-11-11 2:03:19.35421 -3).into(); + let buf = vec![]; + let mut s = rmp_serde::encode::Serializer::new(buf); + dt.serialize(&mut s).unwrap(); + let result = s.get_ref(); + + assert_eq!(18, result.len()); + + let matching = exp + .iter() + .zip(result.iter()) + .filter(|&(a, b)| a == b) + .count(); + assert!(matching == exp.len() && matching == result.len()); + } + + #[test] + pub fn deserialize() { + let exp: Datetime = datetime!(2023-11-11 2:03:19.35421 -3).into(); + let data: &[u8] = &[ + 216, 4, 23, 11, 79, 101, 0, 0, 0, 0, 208, 208, 28, 21, 76, 255, 0, 0, + ]; + let mut d = rmp_serde::decode::Deserializer::new(data); + let result = Datetime::deserialize(&mut d).unwrap(); + + assert_eq!(exp, result); + } +} diff --git a/tarantool/src/lib.rs b/tarantool/src/lib.rs index b400dd2f..5e7c60a5 100644 --- a/tarantool/src/lib.rs +++ b/tarantool/src/lib.rs @@ -184,6 +184,7 @@ pub mod uuid; #[cfg(all(target_arch = "aarch64", target_os = "macos"))] #[doc(hidden)] mod va_list; +pub mod datetime; /// `#[tarantool::proc]` is a macro attribute for creating stored procedure /// functions.