Skip to content

Commit

Permalink
Date & Id MySql conversions
Browse files Browse the repository at this point in the history
  • Loading branch information
tinrab committed Dec 11, 2024
1 parent 94d1f81 commit 9279286
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 234 deletions.
12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ wasm = [
js = ["bomboni_common/js", "bomboni_proto/js", "bomboni_wasm/js"]
fs = ["dep:bomboni_fs"]
postgres = ["bomboni_common/postgres", "bomboni_request/postgres"]
mysql = ["bomboni_common/mysql", "bomboni_request/mysql"]

[dependencies]
bomboni_common = { path = "bomboni_common", version = "0.1.62" }
Expand Down Expand Up @@ -77,10 +78,10 @@ bomboni_wasm_core = { path = "bomboni_wasm_core", version = "0.1.62" }
bomboni_wasm_derive = { path = "bomboni_wasm_derive", version = "0.1.62" }
bomboni_fs = { path = "bomboni_fs", version = "0.1.62" }

thiserror = "2.0.4"
thiserror = "2.0.6"
regex = "1.11.1"
time = "0.3.37"
chrono = "0.4.38"
chrono = "0.4.39"
ulid = "1.1.3"
bytes = "1.9.0"
serde = "1.0.215"
Expand All @@ -92,12 +93,13 @@ rand = "0.8.5"
itertools = "0.13.0"

http = "1.2.0"
prost = "0.13.3"
prost-types = "0.13.3"
prost-build = "0.13.3"
prost = "0.13.4"
prost-types = "0.13.4"
prost-build = "0.13.4"
tonic = "0.12.3"
tokio = "1.42.0"
postgres-types = "0.2.8"
mysql_common = "0.32.4"

proc-macro2 = "1.0.92"
syn = "2.0.90"
Expand Down
6 changes: 4 additions & 2 deletions bomboni_common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ wasm = [
]
js = []
postgres = ["dep:postgres-types", "dep:bytes"]
mysql = ["dep:mysql_common"]

[dependencies]
bomboni_wasm = { workspace = true, features = ["derive"], optional = true }
Expand All @@ -41,10 +42,11 @@ postgres-types = { workspace = true, features = [
"with-time-0_3",
], optional = true }
bytes = { workspace = true, optional = true }
mysql_common = { workspace = true, features = ["time"], optional = true }

[target.'cfg(all(target_family = "wasm", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies]
wasm-bindgen = { version = "0.2.97", optional = true }
js-sys = { version = "0.3.74", optional = true }
wasm-bindgen = { version = "0.2.99", optional = true }
js-sys = { version = "0.3.76", optional = true }

[dev-dependencies]
serde_json.workspace = true
Expand Down
195 changes: 80 additions & 115 deletions bomboni_common/src/date_time/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
use std::fmt::{self, Display, Formatter};
use std::ops::Deref;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::SystemTime;
use thiserror::Error;
use time::convert::{Nanosecond, Second};
pub use time::Month;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use time::PrimitiveDateTime;
use time::{
convert::{Nanosecond, Second},
format_description::well_known::Rfc3339,
OffsetDateTime,
};

#[cfg(feature = "mysql")]
mod mysql;
#[cfg(feature = "postgres")]
mod postgres;

/// A date and time in the UTC time zone.
///
/// This exists (temporary?) because other crates don't support WASM well.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
all(
Expand All @@ -33,7 +40,7 @@ mod postgres;
feature = "js",
),
derive(bomboni_wasm::Wasm),
wasm(wasm_abi, js_value, override_type = "Date",)
wasm(wasm_abi, js_value, override_type = "Date")
)]
pub struct UtcDateTime(OffsetDateTime);

Expand All @@ -54,43 +61,39 @@ impl UtcDateTime {
Self(OffsetDateTime::now_utc())
}

pub fn from_timestamp(seconds: i64, nanoseconds: u32) -> Result<Self, UtcDateTimeError> {
pub const fn new(seconds: i64, nanoseconds: i32) -> Self {
match OffsetDateTime::from_unix_timestamp_nanos(
(seconds as i128) * (Nanosecond::per(Second) as i128) + nanoseconds as i128,
) {
Ok(value) => Self(value),
Err(_err) => Self(OffsetDateTime::UNIX_EPOCH),
}
}

pub fn from_timestamp(seconds: i64, nanoseconds: i32) -> Result<Self, UtcDateTimeError> {
OffsetDateTime::from_unix_timestamp_nanos(
i128::from(seconds) * i128::from(Nanosecond::per(Second)) + i128::from(nanoseconds),
(i128::from(seconds)) * i128::from(Nanosecond::per(Second)) + i128::from(nanoseconds),
)
.map_err(|_| UtcDateTimeError::NotUtc)
.map(Self)
.map_err(|_| UtcDateTimeError::NotUtc)
}

pub fn from_ymd(year: i32, month: Month, day: u8) -> Result<Self, UtcDateTimeError> {
OffsetDateTime::UNIX_EPOCH
.replace_year(year)
.and_then(|dt| dt.replace_month(month))
.and_then(|dt| dt.replace_day(day))
.map_err(|_| UtcDateTimeError::NotUtc)
.map(Self)
}

pub fn with_ymd(self, year: i32, month: Month, day: u8) -> Result<Self, UtcDateTimeError> {
self.0
.replace_year(year)
.and_then(|dt| dt.replace_month(month))
.and_then(|dt| dt.replace_day(day))
.map_err(|_| UtcDateTimeError::NotUtc)
.map(Self)
pub fn from_seconds(seconds: i64) -> Result<Self, UtcDateTimeError> {
match OffsetDateTime::from_unix_timestamp(seconds) {
Err(_) => Err(UtcDateTimeError::NotUtc),
Ok(value) => Ok(Self(value)),
}
}

pub fn with_hms(self, hour: u8, minute: u8, second: u8) -> Result<Self, UtcDateTimeError> {
self.0
.replace_hour(hour)
.and_then(|dt| dt.replace_minute(minute))
.and_then(|dt| dt.replace_second(second))
.map_err(|_| UtcDateTimeError::NotUtc)
.map(Self)
pub fn from_nanoseconds(nanoseconds: i128) -> Result<Self, UtcDateTimeError> {
match OffsetDateTime::from_unix_timestamp_nanos(nanoseconds) {
Err(_) => Err(UtcDateTimeError::NotUtc),
Ok(value) => Ok(Self(value)),
}
}

pub fn timestamp(self) -> (i64, u32) {
(self.0.unix_timestamp(), self.0.nanosecond())
pub fn timestamp(self) -> (i64, i32) {
(self.0.unix_timestamp(), self.0.nanosecond() as i32)
}

pub fn parse_rfc3339<S: AsRef<str>>(input: S) -> Result<Self, UtcDateTimeError> {
Expand All @@ -104,14 +107,6 @@ impl UtcDateTime {
}
}

impl Deref for UtcDateTime {
type Target = OffsetDateTime;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl Display for UtcDateTime {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.format_rfc3339() {
Expand Down Expand Up @@ -141,70 +136,54 @@ impl From<UtcDateTime> for OffsetDateTime {
}
}

impl TryFrom<SystemTime> for UtcDateTime {
type Error = UtcDateTimeError;
impl From<PrimitiveDateTime> for UtcDateTime {
fn from(value: PrimitiveDateTime) -> Self {
Self(value.assume_utc())
}
}

fn try_from(value: SystemTime) -> Result<Self, Self::Error> {
const NANOS_PER_SECOND: i32 = 1_000_000_000;
let (seconds, nanoseconds) = match value.duration_since(UNIX_EPOCH) {
Ok(duration) => {
let seconds = i64::try_from(duration.as_secs()).unwrap();
(seconds, duration.subsec_nanos() as i32)
}
Err(error) => {
let duration = error.duration();
let seconds = i64::try_from(duration.as_secs()).unwrap();
let nanoseconds = duration.subsec_nanos() as i32;
if nanoseconds == 0 {
(-seconds, 0)
} else {
(-seconds - 1, NANOS_PER_SECOND - nanoseconds)
}
}
};
Ok(OffsetDateTime::from_unix_timestamp_nanos(
i128::from(seconds) * i128::from(Nanosecond::per(Second)) + i128::from(nanoseconds),
)
.map_err(|_| UtcDateTimeError::NotUtc)?
.into())
impl From<UtcDateTime> for PrimitiveDateTime {
fn from(value: UtcDateTime) -> Self {
PrimitiveDateTime::new(value.0.date(), value.0.time())
}
}

impl From<SystemTime> for UtcDateTime {
fn from(value: SystemTime) -> Self {
Self(OffsetDateTime::from(value))
}
}

#[cfg(feature = "chrono")]
const _: () = {
use chrono::{DateTime, NaiveDateTime, Utc};

impl TryFrom<NaiveDateTime> for UtcDateTime {
type Error = UtcDateTimeError;

fn try_from(value: NaiveDateTime) -> Result<Self, Self::Error> {
let utc = value.and_utc();
Self::from_timestamp(utc.timestamp(), utc.timestamp_subsec_nanos())
impl From<DateTime<Utc>> for UtcDateTime {
fn from(value: DateTime<Utc>) -> Self {
Self::from_timestamp(value.timestamp(), value.timestamp_subsec_nanos() as i32)
// Always valid UTC
.unwrap()
}
}

impl TryFrom<UtcDateTime> for NaiveDateTime {
type Error = UtcDateTimeError;

fn try_from(value: UtcDateTime) -> Result<Self, Self::Error> {
DateTime::try_from(value).map(|dt| dt.naive_utc())
impl From<UtcDateTime> for DateTime<Utc> {
fn from(value: UtcDateTime) -> Self {
let (seconds, nanoseconds) = value.timestamp();
DateTime::from_timestamp_nanos(
seconds * i64::from(Nanosecond::per(Second)) + i64::from(nanoseconds),
)
}
}

impl TryFrom<DateTime<Utc>> for UtcDateTime {
type Error = UtcDateTimeError;

fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
Self::from_timestamp(value.timestamp(), value.timestamp_subsec_nanos())
impl From<NaiveDateTime> for UtcDateTime {
fn from(value: NaiveDateTime) -> Self {
value.and_utc().into()
}
}

impl TryFrom<UtcDateTime> for DateTime<Utc> {
type Error = UtcDateTimeError;

fn try_from(value: UtcDateTime) -> Result<Self, Self::Error> {
let (seconds, nanoseconds) = value.timestamp();
DateTime::from_timestamp(seconds, nanoseconds).ok_or(UtcDateTimeError::NotUtc)
impl From<UtcDateTime> for NaiveDateTime {
fn from(value: UtcDateTime) -> Self {
DateTime::from(value).naive_utc()
}
}
};
Expand Down Expand Up @@ -286,25 +265,20 @@ const _: () = {

#[cfg(test)]
mod tests {
use std::time::Duration;

use super::*;

#[test]
fn convert() {
assert_eq!(
UtcDateTime::try_from(UNIX_EPOCH + Duration::from_secs(1)).unwrap(),
UtcDateTime::new(1, 0),
UtcDateTime::from_str("1970-01-01T00:00:01Z").unwrap()
);
assert_eq!(
UtcDateTime::try_from(UNIX_EPOCH + Duration::from_nanos(1)).unwrap(),
UtcDateTime::from_timestamp(0, 1).unwrap()
UtcDateTime::new(0, 1),
UtcDateTime::from_nanoseconds(1).unwrap()
);

assert_eq!(
UtcDateTime::from_timestamp(10, 20).unwrap().timestamp(),
(10, 20)
);
assert_eq!(UtcDateTime::new(10, 20).timestamp(), (10, 20));
}

#[cfg(feature = "serde")]
Expand All @@ -322,36 +296,27 @@ mod tests {
let chrono_naive =
chrono::NaiveDateTime::parse_from_str("2020-01-01 12:00:00", "%Y-%m-%d %H:%M:%S")
.unwrap();
let utc = UtcDateTime::try_from(chrono_naive).unwrap();
let utc = UtcDateTime::from(chrono_naive);
assert_eq!(utc.to_string(), "2020-01-01T12:00:00Z");
assert_eq!(chrono::NaiveDateTime::try_from(utc).unwrap(), chrono_naive);
assert_eq!(chrono::NaiveDateTime::from(utc), chrono_naive);

let chrono_naive_nanos = chrono::DateTime::from_timestamp(1337, 420)
.unwrap()
.naive_utc();
let utc = UtcDateTime::try_from(chrono_naive_nanos).unwrap();
let utc = UtcDateTime::from(chrono_naive_nanos);
assert_eq!(utc.timestamp(), (1337, 420));
assert_eq!(
chrono::NaiveDateTime::try_from(utc).unwrap(),
chrono_naive_nanos
);
assert_eq!(chrono::NaiveDateTime::from(utc), chrono_naive_nanos);

let chrono_dt = chrono::DateTime::parse_from_rfc3339("2020-01-01T12:00:00Z")
.unwrap()
.to_utc();
let utc = UtcDateTime::try_from(chrono_dt).unwrap();
let utc = UtcDateTime::from(chrono_dt);
assert_eq!(utc.to_string(), "2020-01-01T12:00:00Z");
assert_eq!(
chrono::DateTime::<chrono::Utc>::try_from(utc).unwrap(),
chrono_dt,
);
assert_eq!(chrono::DateTime::<chrono::Utc>::from(utc), chrono_dt);

let chrono_dt_nanos = chrono::DateTime::from_timestamp(1337, 420).unwrap();
let utc = UtcDateTime::try_from(chrono_dt_nanos).unwrap();
let utc = UtcDateTime::from(chrono_dt_nanos);
assert_eq!(utc.timestamp(), (1337, 420));
assert_eq!(
chrono::DateTime::<chrono::Utc>::try_from(utc).unwrap(),
chrono_dt_nanos,
);
assert_eq!(chrono::DateTime::<chrono::Utc>::from(utc), chrono_dt_nanos);
}
}
23 changes: 23 additions & 0 deletions bomboni_common/src/date_time/mysql.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use mysql_common::{
value::convert::{FromValue, ParseIr},
Value,
};
use time::PrimitiveDateTime;

use crate::date_time::UtcDateTime;

impl FromValue for UtcDateTime {
type Intermediate = ParseIr<PrimitiveDateTime>;
}

impl From<ParseIr<PrimitiveDateTime>> for UtcDateTime {
fn from(ir: ParseIr<PrimitiveDateTime>) -> Self {
ir.commit().into()
}
}

impl From<UtcDateTime> for Value {
fn from(value: UtcDateTime) -> Self {
PrimitiveDateTime::from(value).into()
}
}
Loading

0 comments on commit 9279286

Please sign in to comment.