feat(napi): support chrono datetime with timezones (#2074)

* feat(napi): implement From/ToNapiValue for chrono::DateTime<Local> and chrono::DateTime<FixedOffset> (#1902)

* chore(napi): update tests

Co-authored-by: sup39 <dev@sup39.dev>

---------

Co-authored-by: sup39 <dev@sup39.dev>
This commit is contained in:
naskya 2024-04-30 23:51:40 +09:00 committed by GitHub
parent a54b759bb1
commit 68b5f2c587
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 153 additions and 29 deletions

View File

@ -1,10 +1,10 @@
use std::{ptr, str::FromStr};
use chrono::{DateTime, NaiveDateTime, Utc};
use chrono::{DateTime, Local, LocalResult, NaiveDateTime, TimeZone};
use crate::{bindgen_prelude::*, check_status, sys, ValueType};
impl TypeName for DateTime<Utc> {
impl<Tz: TimeZone> TypeName for DateTime<Tz> {
fn type_name() -> &'static str {
"DateTime"
}
@ -14,7 +14,7 @@ impl TypeName for DateTime<Utc> {
}
}
impl ValidateNapiValue for DateTime<Utc> {
impl<Tz: TimeZone> ValidateNapiValue for DateTime<Tz> {
unsafe fn validate(env: sys::napi_env, napi_val: sys::napi_value) -> Result<sys::napi_value> {
let mut is_date = false;
check_status!(unsafe { sys::napi_is_date(env, napi_val, &mut is_date) })?;
@ -120,39 +120,38 @@ impl FromNapiValue for NaiveDateTime {
}
}
impl ToNapiValue for DateTime<Utc> {
unsafe fn to_napi_value(env: sys::napi_env, val: DateTime<Utc>) -> Result<sys::napi_value> {
impl<Tz: TimeZone> ToNapiValue for DateTime<Tz> {
unsafe fn to_napi_value(env: sys::napi_env, val: DateTime<Tz>) -> Result<sys::napi_value> {
let mut ptr = std::ptr::null_mut();
let millis_since_epoch_utc = val.timestamp_millis() as f64;
check_status!(
unsafe { sys::napi_create_date(env, millis_since_epoch_utc, &mut ptr) },
"Failed to convert rust type `DateTime<Utc>` into napi value",
"Failed to convert rust type `DateTime` into napi value",
)?;
Ok(ptr)
}
}
impl FromNapiValue for DateTime<Utc> {
impl<Tz: TimeZone> FromNapiValue for DateTime<Tz>
where
DateTime<Tz>: From<DateTime<Local>>,
{
unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result<Self> {
let mut milliseconds_since_epoch_utc = 0.0;
check_status!(
unsafe { sys::napi_get_date_value(env, napi_val, &mut milliseconds_since_epoch_utc) },
"Failed to convert napi value into rust type `DateTime<Utc>`",
"Failed to convert napi value into rust type `DateTime`",
)?;
let milliseconds_since_epoch_utc = milliseconds_since_epoch_utc as i64;
let timestamp_seconds = milliseconds_since_epoch_utc / 1_000;
let naive = DateTime::from_timestamp(
timestamp_seconds,
(milliseconds_since_epoch_utc % 1_000 * 1_000_000) as u32,
)
.ok_or_else(|| Error::new(Status::DateExpected, "Found invalid date".to_owned()))?;
Ok(DateTime::<Utc>::from_naive_utc_and_offset(
naive.naive_utc(),
Utc,
))
match Local.timestamp_millis_opt(milliseconds_since_epoch_utc as i64) {
LocalResult::Single(dt) => Ok(dt.into()),
_ => Err(Error::new(
Status::DateExpected,
"Found invalid date".to_owned(),
)),
}
}
}

View File

@ -343,12 +343,26 @@ Generated by [AVA](https://avajs.dev).
export function chronoDateAdd1Minute(input: Date): Date␊
export function chronoDateToMillis(input: Date): number␊
export function chronoDateFixtureReturn1(): Date␊
export function chronoDateFixtureReturn2(): Date␊
export function chronoDateWithTimezoneReturn(): Date | null␊
export function chronoDateWithTimezoneToMillis(input: Date): number␊
export function chronoLocalDateReturn(): Date | null␊
export function chronoLocalDateToMillis(input: Date): number␊
export function chronoNativeDateTime(date: Date): number␊
export function chronoNativeDateTimeReturn(): Date | null␊
export function chronoUtcDateReturn(): Date | null␊
export function chronoUtcDateToMillis(input: Date): number␊
export function concatLatin1(s: string): string␊
export function concatStr(s: string): string␊
@ -397,7 +411,7 @@ Generated by [AVA](https://avajs.dev).
export function customStatusCode(): void␊
export interface Dates {␊
export interface DatesWithTimeZone {␊
start: Date␊
end?: Date␊
}␊
@ -505,6 +519,11 @@ Generated by [AVA](https://avajs.dev).
export function listObjKeys(obj: object): Array<string>
export interface LocalDates {␊
start: Date␊
end?: Date␊
}␊
export function mapOption(val?: number | undefined | null): number | null␊
export function mutateExternal(external: ExternalObject<number>, newVal: number): void␊
@ -698,6 +717,11 @@ Generated by [AVA](https://avajs.dev).
nullableStringField: string | null␊
}␊
export interface UtcDates {␊
start: Date␊
end?: Date␊
}␊
export function validateArray(arr: Array<number>): number␊
export function validateBigint(input: bigint): bigint␊

View File

@ -126,7 +126,11 @@ import {
createObjWithProperty,
receiveObjectOnlyFromJs,
dateToNumber,
chronoDateToMillis,
chronoUtcDateToMillis,
chronoLocalDateToMillis,
chronoDateWithTimezoneToMillis,
chronoDateFixtureReturn1,
chronoDateFixtureReturn2,
derefUint8Array,
chronoDateAdd1Minute,
bufferPassThrough,
@ -1235,13 +1239,22 @@ Napi5Test('Date test', (t) => {
Napi5Test('Date to chrono test', (t) => {
const fixture = new Date('2022-02-09T19:31:55.396Z')
t.is(chronoDateToMillis(fixture), fixture.getTime())
t.is(chronoUtcDateToMillis(fixture), fixture.getTime())
t.is(chronoLocalDateToMillis(fixture), fixture.getTime())
t.is(chronoDateWithTimezoneToMillis(fixture), fixture.getTime())
t.deepEqual(
chronoDateAdd1Minute(fixture),
new Date(fixture.getTime() + 60 * 1000),
)
})
Napi5Test('Get date', (t) => {
const fixture1 = new Date('2024-02-07T18:28:18-0800')
t.deepEqual(chronoDateFixtureReturn1(), fixture1)
const fixture2 = new Date('2024-02-07T18:28:18+0530')
t.deepEqual(chronoDateFixtureReturn2(), fixture2)
})
Napi5Test('Class with getter setter closures', (t) => {
const instance = new GetterSetterWithClosures()
// @ts-expect-error

View File

@ -438,9 +438,16 @@ module.exports.callLongThreadsafeFunction = nativeBinding.callLongThreadsafeFunc
module.exports.callThreadsafeFunction = nativeBinding.callThreadsafeFunction
module.exports.captureErrorInCallback = nativeBinding.captureErrorInCallback
module.exports.chronoDateAdd1Minute = nativeBinding.chronoDateAdd1Minute
module.exports.chronoDateToMillis = nativeBinding.chronoDateToMillis
module.exports.chronoDateFixtureReturn1 = nativeBinding.chronoDateFixtureReturn1
module.exports.chronoDateFixtureReturn2 = nativeBinding.chronoDateFixtureReturn2
module.exports.chronoDateWithTimezoneReturn = nativeBinding.chronoDateWithTimezoneReturn
module.exports.chronoDateWithTimezoneToMillis = nativeBinding.chronoDateWithTimezoneToMillis
module.exports.chronoLocalDateReturn = nativeBinding.chronoLocalDateReturn
module.exports.chronoLocalDateToMillis = nativeBinding.chronoLocalDateToMillis
module.exports.chronoNativeDateTime = nativeBinding.chronoNativeDateTime
module.exports.chronoNativeDateTimeReturn = nativeBinding.chronoNativeDateTimeReturn
module.exports.chronoUtcDateReturn = nativeBinding.chronoUtcDateReturn
module.exports.chronoUtcDateToMillis = nativeBinding.chronoUtcDateToMillis
module.exports.concatLatin1 = nativeBinding.concatLatin1
module.exports.concatStr = nativeBinding.concatStr
module.exports.concatUtf16 = nativeBinding.concatUtf16

View File

@ -333,12 +333,26 @@ export function captureErrorInCallback(cb1: () => void, cb2: (arg0: Error) => vo
export function chronoDateAdd1Minute(input: Date): Date
export function chronoDateToMillis(input: Date): number
export function chronoDateFixtureReturn1(): Date
export function chronoDateFixtureReturn2(): Date
export function chronoDateWithTimezoneReturn(): Date | null
export function chronoDateWithTimezoneToMillis(input: Date): number
export function chronoLocalDateReturn(): Date | null
export function chronoLocalDateToMillis(input: Date): number
export function chronoNativeDateTime(date: Date): number
export function chronoNativeDateTimeReturn(): Date | null
export function chronoUtcDateReturn(): Date | null
export function chronoUtcDateToMillis(input: Date): number
export function concatLatin1(s: string): string
export function concatStr(s: string): string
@ -387,7 +401,7 @@ export const enum CustomNumEnum {
export function customStatusCode(): void
export interface Dates {
export interface DatesWithTimeZone {
start: Date
end?: Date
}
@ -495,6 +509,11 @@ export const enum Kind {
export function listObjKeys(obj: object): Array<string>
export interface LocalDates {
start: Date
end?: Date
}
export function mapOption(val?: number | undefined | null): number | null
export function mutateExternal(external: ExternalObject<number>, newVal: number): void
@ -688,6 +707,11 @@ export interface UseNullableStruct {
nullableStringField: string | null
}
export interface UtcDates {
start: Date
end?: Date
}
export function validateArray(arr: Array<number>): number
export function validateBigint(input: bigint): bigint

View File

@ -1,6 +1,6 @@
use std::str::FromStr;
use chrono::{Duration, Utc};
use chrono::{Duration, FixedOffset, Local, TimeZone, Utc};
use napi::bindgen_prelude::*;
#[napi]
@ -9,7 +9,17 @@ fn date_to_number(input: Date) -> Result<f64> {
}
#[napi]
fn chrono_date_to_millis(input: chrono::DateTime<Utc>) -> i64 {
fn chrono_utc_date_to_millis(input: chrono::DateTime<Utc>) -> i64 {
input.timestamp_millis()
}
#[napi]
fn chrono_local_date_to_millis(input: chrono::DateTime<Local>) -> i64 {
input.timestamp_millis()
}
#[napi]
fn chrono_date_with_timezone_to_millis(input: chrono::DateTime<FixedOffset>) -> i64 {
input.timestamp_millis()
}
@ -19,11 +29,23 @@ fn chrono_date_add_1_minute(input: chrono::DateTime<Utc>) -> chrono::DateTime<Ut
}
#[napi(object)]
pub struct Dates {
pub struct UtcDates {
pub start: chrono::DateTime<Utc>,
pub end: Option<chrono::DateTime<Utc>>,
}
#[napi(object)]
pub struct LocalDates {
pub start: chrono::DateTime<Local>,
pub end: Option<chrono::DateTime<Local>>,
}
#[napi(object)]
pub struct DatesWithTimeZone {
pub start: chrono::DateTime<FixedOffset>,
pub end: Option<chrono::DateTime<FixedOffset>>,
}
#[napi]
pub fn chrono_native_date_time(date: chrono::NaiveDateTime) -> i64 {
date.and_utc().timestamp_millis()
@ -33,3 +55,38 @@ pub fn chrono_native_date_time(date: chrono::NaiveDateTime) -> i64 {
pub fn chrono_native_date_time_return() -> Option<chrono::NaiveDateTime> {
chrono::NaiveDateTime::from_str("2016-12-23T15:25:59.325").ok()
}
#[napi]
pub fn chrono_utc_date_return() -> Option<chrono::DateTime<Utc>> {
chrono::DateTime::<Utc>::from_str("2016-12-23T15:25:59.325").ok()
}
#[napi]
pub fn chrono_local_date_return() -> Option<chrono::DateTime<Local>> {
chrono::DateTime::<Local>::from_str("2016-12-23T15:25:59.325").ok()
}
#[napi]
pub fn chrono_date_with_timezone_return() -> Option<chrono::DateTime<FixedOffset>> {
chrono::DateTime::<FixedOffset>::from_str("2016-12-23T15:25:59.325").ok()
}
#[napi]
pub fn chrono_date_fixture_return1() -> chrono::DateTime<FixedOffset> {
// Pacific Standard Time: UTC-08:00
let pst = FixedOffset::west_opt(8 * 3600).unwrap();
pst
.with_ymd_and_hms(2024, 2, 7, 18, 28, 18)
.single()
.unwrap()
}
#[napi]
pub fn chrono_date_fixture_return2() -> chrono::DateTime<FixedOffset> {
// Indian Standard Time: UTC+05:30
let ist = FixedOffset::east_opt(5 * 3600 + 30 * 60).unwrap();
ist
.with_ymd_and_hms(2024, 2, 7, 18, 28, 18)
.single()
.unwrap()
}