From 5b2f87df5850c4e46a66d0313090e9a850dcc9b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Fri, 15 May 2026 19:42:32 +0800 Subject: [PATCH 1/5] feat(rust): use fory temporal carriers by default --- compiler/README.md | 30 +- compiler/fory_compiler/generators/rust.py | 6 +- .../tests/test_generated_code.py | 22 + docs/compiler/schema-idl.md | 36 +- docs/guide/rust/basic-serialization.md | 17 +- docs/specification/xlang_type_mapping.md | 4 +- rust/fory-core/Cargo.toml | 5 +- rust/fory-core/src/lib.rs | 6 +- rust/fory-core/src/resolver/type_resolver.rs | 15 +- rust/fory-core/src/row/row.rs | 51 ++- rust/fory-core/src/serializer/datetime.rs | 414 ++++++++++-------- rust/fory-core/src/serializer/skip.rs | 6 +- rust/fory-core/src/type_id.rs | 10 +- rust/fory-core/src/types/mod.rs | 2 + rust/fory-core/src/types/temporal.rs | 284 ++++++++++++ rust/fory-core/src/util/mod.rs | 9 - rust/fory-derive/src/lib.rs | 6 +- rust/fory-derive/src/object/util.rs | 4 +- rust/fory/Cargo.toml | 4 + rust/fory/src/lib.rs | 12 +- rust/tests/Cargo.toml | 6 +- .../tests/tests/compatible/test_basic_type.rs | 58 +-- rust/tests/tests/test_complex_struct.rs | 10 +- rust/tests/tests/test_cross_language.rs | 22 +- rust/tests/tests/test_fory.rs | 13 +- 25 files changed, 715 insertions(+), 337 deletions(-) create mode 100644 rust/fory-core/src/types/temporal.rs diff --git a/compiler/README.md b/compiler/README.md index fb443ea4d9..236c38b77f 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -185,21 +185,21 @@ message Config { ... } // Registered as "package.Config" ### Primitive Types -| FDL Type | Java | Python | Go | Rust | C++ | C# | JavaScript | -| ----------- | ----------- | ------------------- | ----------- | ----------------------- | ---------------------- | ---------------- | ------------------ | -| `bool` | `boolean` | `bool` | `bool` | `bool` | `bool` | `bool` | `boolean` | -| `int8` | `byte` | `pyfory.Int8` | `int8` | `i8` | `int8_t` | `sbyte` | `number` | -| `int16` | `short` | `pyfory.Int16` | `int16` | `i16` | `int16_t` | `short` | `number` | -| `int32` | `int` | `pyfory.Int32` | `int32` | `i32` | `int32_t` | `int` | `number` | -| `int64` | `long` | `pyfory.Int64` | `int64` | `i64` | `int64_t` | `long` | `bigint \| number` | -| `float16` | `Float16` | `pyfory.Float16` | `float16` | `Float16` | `fory::float16_t` | `Half` | `number` | -| `bfloat16` | `BFloat16` | `pyfory.BFloat16` | `bfloat16` | `BFloat16` | `fory::bfloat16_t` | `BFloat16` | `number` | -| `float32` | `float` | `pyfory.Float32` | `float32` | `f32` | `float` | `float` | `number` | -| `float64` | `double` | `pyfory.Float64` | `float64` | `f64` | `double` | `double` | `number` | -| `string` | `String` | `str` | `string` | `String` | `std::string` | `string` | `string` | -| `bytes` | `byte[]` | `bytes` | `[]byte` | `Vec` | `std::vector` | `byte[]` | `Uint8Array` | -| `date` | `LocalDate` | `datetime.date` | `time.Time` | `chrono::NaiveDate` | `fory::Date` | `DateOnly` | `Date` | -| `timestamp` | `Instant` | `datetime.datetime` | `time.Time` | `chrono::NaiveDateTime` | `fory::Timestamp` | `DateTimeOffset` | `Date` | +| FDL Type | Java | Python | Go | Rust | C++ | C# | JavaScript | +| ----------- | ----------- | ------------------- | ----------- | ----------------- | ---------------------- | ---------------- | ------------------ | +| `bool` | `boolean` | `bool` | `bool` | `bool` | `bool` | `bool` | `boolean` | +| `int8` | `byte` | `pyfory.Int8` | `int8` | `i8` | `int8_t` | `sbyte` | `number` | +| `int16` | `short` | `pyfory.Int16` | `int16` | `i16` | `int16_t` | `short` | `number` | +| `int32` | `int` | `pyfory.Int32` | `int32` | `i32` | `int32_t` | `int` | `number` | +| `int64` | `long` | `pyfory.Int64` | `int64` | `i64` | `int64_t` | `long` | `bigint \| number` | +| `float16` | `Float16` | `pyfory.Float16` | `float16` | `Float16` | `fory::float16_t` | `Half` | `number` | +| `bfloat16` | `BFloat16` | `pyfory.BFloat16` | `bfloat16` | `BFloat16` | `fory::bfloat16_t` | `BFloat16` | `number` | +| `float32` | `float` | `pyfory.Float32` | `float32` | `f32` | `float` | `float` | `number` | +| `float64` | `double` | `pyfory.Float64` | `float64` | `f64` | `double` | `double` | `number` | +| `string` | `String` | `str` | `string` | `String` | `std::string` | `string` | `string` | +| `bytes` | `byte[]` | `bytes` | `[]byte` | `Vec` | `std::vector` | `byte[]` | `Uint8Array` | +| `date` | `LocalDate` | `datetime.date` | `time.Time` | `fory::Date` | `fory::Date` | `DateOnly` | `Date` | +| `timestamp` | `Instant` | `datetime.datetime` | `time.Time` | `fory::Timestamp` | `fory::Timestamp` | `DateTimeOffset` | `Date` | ### Collection Types diff --git a/compiler/fory_compiler/generators/rust.py b/compiler/fory_compiler/generators/rust.py index 64885b530b..988b2f94b9 100644 --- a/compiler/fory_compiler/generators/rust.py +++ b/compiler/fory_compiler/generators/rust.py @@ -61,9 +61,9 @@ class RustGenerator(BaseGenerator): PrimitiveKind.FLOAT64: "f64", PrimitiveKind.STRING: "::std::string::String", PrimitiveKind.BYTES: "::std::vec::Vec", - PrimitiveKind.DATE: "::chrono::NaiveDate", - PrimitiveKind.TIMESTAMP: "::chrono::NaiveDateTime", - PrimitiveKind.DURATION: "::chrono::Duration", + PrimitiveKind.DATE: "::fory::Date", + PrimitiveKind.TIMESTAMP: "::fory::Timestamp", + PrimitiveKind.DURATION: "::fory::Duration", PrimitiveKind.DECIMAL: "::fory::Decimal", PrimitiveKind.ANY: "::std::boxed::Box", } diff --git a/compiler/fory_compiler/tests/test_generated_code.py b/compiler/fory_compiler/tests/test_generated_code.py index b4fde196ba..551e6dc26d 100644 --- a/compiler/fory_compiler/tests/test_generated_code.py +++ b/compiler/fory_compiler/tests/test_generated_code.py @@ -154,6 +154,28 @@ def test_generated_code_scalar_types_equivalent(): assert_all_languages_equal(schemas) +def test_rust_generated_code_uses_fory_temporal_carriers(): + schema = parse_fdl( + dedent( + """ + package gen; + + message TemporalTypes { + date day = 1; + timestamp instant = 2; + duration elapsed = 3; + } + """ + ) + ) + + rust_output = render_files(generate_files(schema, RustGenerator)) + assert "pub day: ::fory::Date," in rust_output + assert "pub instant: ::fory::Timestamp," in rust_output + assert "pub elapsed: ::fory::Duration," in rust_output + assert "chrono::" not in rust_output + + def test_generated_code_integer_encoding_variants_equivalent(): fdl = dedent( """ diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md index ab98a0e07f..5039c80efe 100644 --- a/docs/compiler/schema-idl.md +++ b/docs/compiler/schema-idl.md @@ -1150,27 +1150,27 @@ Underscore spellings for integer encoding are not FDL type names. ##### Date -| Language | Type | Notes | -| ---------- | --------------------------- | ----------------------- | -| Java | `java.time.LocalDate` | | -| Python | `datetime.date` | | -| Go | `time.Time` | Time portion ignored | -| Rust | `chrono::NaiveDate` | Requires `chrono` crate | -| C++ | `fory::serialization::Date` | | -| JavaScript | `Date` | | -| Dart | `LocalDate` | Fory package type | +| Language | Type | Notes | +| ---------- | --------------------------- | --------------------------------------------------------------- | +| Java | `java.time.LocalDate` | | +| Python | `datetime.date` | | +| Go | `time.Time` | Time portion ignored | +| Rust | `fory::Date` | `chrono::NaiveDate` is supported with the Rust `chrono` feature | +| C++ | `fory::serialization::Date` | | +| JavaScript | `Date` | | +| Dart | `LocalDate` | Fory package type | ##### Timestamp -| Language | Type | Notes | -| ---------- | -------------------------------- | ----------------------- | -| Java | `java.time.Instant` | UTC-based | -| Python | `datetime.datetime` | | -| Go | `time.Time` | | -| Rust | `chrono::NaiveDateTime` | Requires `chrono` crate | -| C++ | `fory::serialization::Timestamp` | | -| JavaScript | `Date` | | -| Dart | `Timestamp` | Fory package type | +| Language | Type | Notes | +| ---------- | -------------------------------- | ------------------------------------------------------------------- | +| Java | `java.time.Instant` | UTC-based | +| Python | `datetime.datetime` | | +| Go | `time.Time` | | +| Rust | `fory::Timestamp` | `chrono::NaiveDateTime` is supported with the Rust `chrono` feature | +| C++ | `fory::serialization::Timestamp` | | +| JavaScript | `Date` | | +| Dart | `Timestamp` | Fory package type | #### Any diff --git a/docs/guide/rust/basic-serialization.md b/docs/guide/rust/basic-serialization.md index 2d2386f5a6..db6db0b4bc 100644 --- a/docs/guide/rust/basic-serialization.md +++ b/docs/guide/rust/basic-serialization.md @@ -119,10 +119,19 @@ assert_eq!(person, decoded); ### Date and Time -| Rust Type | Description | -| ----------------------- | -------------------------- | -| `chrono::NaiveDate` | Date without timezone | -| `chrono::NaiveDateTime` | Timestamp without timezone | +| Rust Type | Description | +| ----------- | ------------------------------------------------------- | +| `Date` | Date without timezone, stored as epoch days | +| `Timestamp` | Point in time, stored as epoch seconds and nanos | +| `Duration` | Signed duration, stored as seconds and normalized nanos | + +`chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration` are supported when the Rust +`chrono` feature is enabled: + +```toml +[dependencies] +fory = { version = "0.13", features = ["chrono"] } +``` ### Custom Types diff --git a/docs/specification/xlang_type_mapping.md b/docs/specification/xlang_type_mapping.md index a2a49ebfb8..96b1e6a723 100644 --- a/docs/specification/xlang_type_mapping.md +++ b/docs/specification/xlang_type_mapping.md @@ -89,8 +89,8 @@ FDL spells them as an encoding modifier plus a semantic integer type. | union | 33 | Union | typing.Union | / | `std::variant` | / | tagged union enum | | none | 36 | null | None | null | `std::monostate` | nil | `()` | | duration | 37 | Duration | timedelta | Number | duration | Duration | Duration | -| timestamp | 38 | Instant | datetime | Number | std::chrono::nanoseconds | Time | DateTime | -| date | 39 | LocalDate | datetime.date | Date | fory::serialization::Date | fory.Date | chrono::NaiveDate | +| timestamp | 38 | Instant | datetime | Number | std::chrono::nanoseconds | Time | Timestamp | +| date | 39 | LocalDate | datetime.date | Date | fory::serialization::Date | fory.Date | Date | | decimal | 40 | BigDecimal | Decimal | Decimal | / | fory.Decimal | fory::Decimal | | binary | 41 | byte[] | bytes | / | `uint8_t[n]/vector` | `[n]uint8/[]T` | `Vec` | | `array` (bool_array) | 43 | bool[] | BoolArray / ndarray(np.bool\_) | BoolArray / Type.boolArray() | `bool[n]` | `[n]bool/[]T` | `Vec` | diff --git a/rust/fory-core/Cargo.toml b/rust/fory-core/Cargo.toml index 6a96e6f8dc..da39378e09 100644 --- a/rust/fory-core/Cargo.toml +++ b/rust/fory-core/Cargo.toml @@ -32,12 +32,15 @@ proc-macro2 = { default-features = false, version = "1.0" } syn = { default-features = false, version = "2.0", features = ["full", "fold"] } quote = { default-features = false, version = "1.0" } byteorder = { version = "1.4" } -chrono = "0.4" +chrono = { version = "0.4", default-features = false, features = ["std"], optional = true } thiserror = { default-features = false, version = "1.0" } num_enum = "0.5.1" paste = "1.0" num-bigint = "0.4" +[features] +default = [] +chrono = ["dep:chrono"] [[bench]] name = "simd_bench" diff --git a/rust/fory-core/src/lib.rs b/rust/fory-core/src/lib.rs index 4d1ac219b5..65716af4a7 100644 --- a/rust/fory-core/src/lib.rs +++ b/rust/fory-core/src/lib.rs @@ -32,7 +32,7 @@ //! - **`serializer`**: Type-specific serialization implementations //! - **`resolver`**: Type resolution and metadata management //! - **`meta`**: Metadata handling for schema evolution -//! - **`types`**: Runtime value carriers such as decimal, Float16, BFloat16, and weak refs +//! - **`types`**: Runtime value carriers such as temporal values, decimal, Float16, BFloat16, and weak refs //! - **`type_id`**: Type IDs and protocol header helpers //! - **`error`**: Error handling and result types //! - **`util`**: Utility functions and helpers @@ -52,7 +52,7 @@ //! - Primitive types (bool, integers, floats, strings) //! - Collections (Vec, HashMap, BTreeMap) //! - Optional types (`Option`) -//! - Date/time types (chrono integration) +//! - Date/time carriers with optional chrono integration //! - Custom structs and enums //! - Trait objects (Box, Rc, Arc) //! @@ -205,4 +205,4 @@ pub use crate::serializer::{read_data, write_data, ForyDefault, Serializer, Stru pub use crate::type_id::TypeId; pub use crate::types::bfloat16::bfloat16 as BFloat16; pub use crate::types::float16::float16 as Float16; -pub use crate::types::{ArcWeak, Decimal, RcWeak}; +pub use crate::types::{ArcWeak, Date, Decimal, Duration, RcWeak, Timestamp}; diff --git a/rust/fory-core/src/resolver/type_resolver.rs b/rust/fory-core/src/resolver/type_resolver.rs index 3fcacda798..7983cc0c81 100644 --- a/rust/fory-core/src/resolver/type_resolver.rs +++ b/rust/fory-core/src/resolver/type_resolver.rs @@ -24,8 +24,10 @@ use crate::meta::{ use crate::resolver::RefMode; use crate::serializer::{ForyDefault, Serializer, StructSerializer}; use crate::type_id::{get_ext_actual_type_id, is_enum_type_id}; +use crate::types::{Date, Duration, Timestamp}; use crate::TypeId; -use chrono::{NaiveDate, NaiveDateTime}; +#[cfg(feature = "chrono")] +use chrono::{Duration as ChronoDuration, NaiveDate, NaiveDateTime}; use std::collections::{HashSet, LinkedList}; use std::rc::Rc; use std::vec; @@ -771,8 +773,15 @@ impl TypeResolver { self.register_internal_serializer::(TypeId::USIZE)?; self.register_internal_serializer::(TypeId::U128)?; self.register_internal_serializer::(TypeId::STRING)?; - self.register_internal_serializer::(TypeId::TIMESTAMP)?; - self.register_internal_serializer::(TypeId::DATE)?; + #[cfg(feature = "chrono")] + { + self.register_internal_serializer::(TypeId::DURATION)?; + self.register_internal_serializer::(TypeId::TIMESTAMP)?; + self.register_internal_serializer::(TypeId::DATE)?; + } + self.register_internal_serializer::(TypeId::DURATION)?; + self.register_internal_serializer::(TypeId::TIMESTAMP)?; + self.register_internal_serializer::(TypeId::DATE)?; self.register_internal_serializer::(TypeId::DECIMAL)?; self.register_internal_serializer::>(TypeId::BOOL_ARRAY)?; diff --git a/rust/fory-core/src/row/row.rs b/rust/fory-core/src/row/row.rs index 147a2f110b..8f6e5181ca 100644 --- a/rust/fory-core/src/row/row.rs +++ b/rust/fory-core/src/row/row.rs @@ -15,10 +15,9 @@ // specific language governing permissions and limitations // under the License. -use crate::util::EPOCH; +use crate::types::{Date, Duration, Timestamp}; use crate::{buffer::Writer, error::Error}; use byteorder::{ByteOrder, LittleEndian}; -use chrono::{DateTime, Days, NaiveDate, NaiveDateTime}; use std::collections::BTreeMap; use std::marker::PhantomData; @@ -133,40 +132,50 @@ impl<'a, T: Row<'a>, const N: usize> Row<'a> for [T; N] { } } -impl Row<'_> for NaiveDate { - type ReadResult = Result; +impl Row<'_> for Date { + type ReadResult = Result; fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { - let days_since_epoch = v.signed_duration_since(EPOCH).num_days(); - writer.write_u32(days_since_epoch as u32); + let days = i32::try_from(v.epoch_days()).map_err(|_| { + Error::invalid_data(format!( + "row date day count {} exceeds date32 range", + v.epoch_days() + )) + })?; + writer.write_i32(days); Ok(()) } fn cast(bytes: &[u8]) -> Self::ReadResult { - let days = LittleEndian::read_u32(bytes); - EPOCH - .checked_add_days(Days::new(days.into())) - .ok_or(Error::invalid_data(format!( - "Date out of range, {days} days since epoch" - ))) + Ok(Date::from_epoch_days(i64::from(LittleEndian::read_i32( + bytes, + )))) } } -impl Row<'_> for NaiveDateTime { - type ReadResult = Result; +impl Row<'_> for Timestamp { + type ReadResult = Result; fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { - writer.write_i64(v.and_utc().timestamp_millis()); + writer.write_i64(v.to_epoch_micros()?); Ok(()) } fn cast(bytes: &[u8]) -> Self::ReadResult { - let timestamp = LittleEndian::read_u64(bytes); - DateTime::from_timestamp_millis(timestamp as i64) - .map(|dt| dt.naive_utc()) - .ok_or(Error::invalid_data(format!( - "Date out of range, timestamp:{timestamp}" - ))) + Ok(Timestamp::from_epoch_micros(LittleEndian::read_i64(bytes))) + } +} + +impl Row<'_> for Duration { + type ReadResult = Result; + + fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { + writer.write_i64(v.to_micros()?); + Ok(()) + } + + fn cast(bytes: &[u8]) -> Self::ReadResult { + Ok(Duration::from_micros(LittleEndian::read_i64(bytes))) } } diff --git a/rust/fory-core/src/serializer/datetime.rs b/rust/fory-core/src/serializer/datetime.rs index 5823d0c7c0..efa7572450 100644 --- a/rust/fory-core/src/serializer/datetime.rs +++ b/rust/fory-core/src/serializer/datetime.rs @@ -23,19 +23,14 @@ use crate::serializer::util::read_basic_type_info; use crate::serializer::ForyDefault; use crate::serializer::Serializer; use crate::type_id::TypeId; -use crate::util::EPOCH; -use chrono::{Duration as ChronoDuration, NaiveDate, NaiveDateTime, TimeDelta}; +use crate::types::{Date, Duration, Timestamp}; use std::mem; -use std::time::Duration; -impl Serializer for NaiveDateTime { +impl Serializer for Timestamp { #[inline(always)] fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> { - let dt = self.and_utc(); - let seconds = dt.timestamp(); - let nanos = dt.timestamp_subsec_nanos(); - context.writer.write_i64(seconds); - context.writer.write_u32(nanos); + context.writer.write_i64(self.seconds()); + context.writer.write_u32(self.subsec_nanos()); Ok(()) } @@ -43,9 +38,7 @@ impl Serializer for NaiveDateTime { fn fory_read_data(context: &mut ReadContext) -> Result { let seconds = context.reader.read_i64()?; let nanos = context.reader.read_u32()?; - #[allow(deprecated)] - let result = NaiveDateTime::from_timestamp(seconds, nanos); - Ok(result) + Timestamp::new(seconds, nanos) } #[inline(always)] @@ -85,18 +78,22 @@ impl Serializer for NaiveDateTime { } } -impl Serializer for NaiveDate { +impl ForyDefault for Timestamp { + #[inline(always)] + fn fory_default() -> Self { + Timestamp::default() + } +} + +impl Serializer for Date { #[inline(always)] fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> { - let days_since_epoch = self.signed_duration_since(EPOCH).num_days(); + let days = self.epoch_days(); if context.is_xlang() { - context.writer.write_var_i64(days_since_epoch); + context.writer.write_var_i64(days); } else { - let native_days = i32::try_from(days_since_epoch).map_err(|_| { - Error::invalid_data(format!( - "date day count {} exceeds native i32 range", - days_since_epoch - )) + let native_days = i32::try_from(days).map_err(|_| { + Error::invalid_data(format!("date day count {} exceeds native i32 range", days)) })?; context.writer.write_i32(native_days); } @@ -110,18 +107,7 @@ impl Serializer for NaiveDate { } else { i64::from(context.reader.read_i32()?) }; - let duration = TimeDelta::try_days(days).ok_or_else(|| { - Error::invalid_data(format!( - "date day count {} is out of chrono::TimeDelta range", - days - )) - })?; - EPOCH.checked_add_signed(duration).ok_or_else(|| { - Error::invalid_data(format!( - "date day count {} is out of chrono::NaiveDate range", - days - )) - }) + Ok(Date::from_epoch_days(days)) } #[inline(always)] @@ -161,62 +147,31 @@ impl Serializer for NaiveDate { } } -impl ForyDefault for NaiveDateTime { +impl ForyDefault for Date { #[inline(always)] fn fory_default() -> Self { - NaiveDateTime::default() - } -} - -impl ForyDefault for NaiveDate { - #[inline(always)] - fn fory_default() -> Self { - NaiveDate::default() + Date::default() } } impl Serializer for Duration { #[inline(always)] fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> { - let raw = self.as_secs(); - if raw > i64::MAX as u64 { - return Err(Error::invalid_data(format!( - "std::time::Duration seconds {} exceeds i64::MAX and cannot be encoded with write_var_i64", - raw - ))); - } - let secs = raw as i64; - let nanos = self.subsec_nanos() as i32; - context.writer.write_var_i64(secs); - context.writer.write_i32(nanos); + context.writer.write_var_i64(self.seconds()); + context.writer.write_i32(self.subsec_nanos() as i32); Ok(()) } #[inline(always)] fn fory_read_data(context: &mut ReadContext) -> Result { - let secs = context.reader.read_var_i64()?; - if secs < 0 { - return Err(Error::invalid_data(format!( - "negative duration seconds {} cannot be represented as std::time::Duration; use chrono::Duration instead", - secs - ))); - } + let seconds = context.reader.read_var_i64()?; let nanos = context.reader.read_i32()?; - if !(0..=999_999_999).contains(&nanos) { - // negative nanos will also be rejected, even though the xlang spec actually allows it. - // RFC 1040 (https://rust-lang.github.io/rfcs/1040-duration-reform.html#detailed-design) explicitly forbids negative nanoseconds. - // If supporting for negative nanoseconds is really needed, we can implement **normalization** similar to chrono and Java. - return Err(Error::invalid_data(format!( - "duration nanoseconds {} out of valid range [0, 999_999_999] for std::time::Duration", - nanos - ))); - } - Ok(Duration::new(secs as u64, nanos as u32)) + Duration::new(seconds, nanos) } #[inline(always)] fn fory_reserved_space() -> usize { - 9 + mem::size_of::() // max write_var_i64 payload is 9 bytes + 4 bytes for i32 + 9 + mem::size_of::() } #[inline(always)] @@ -254,82 +209,175 @@ impl Serializer for Duration { impl ForyDefault for Duration { #[inline(always)] fn fory_default() -> Self { - Duration::ZERO + Duration::default() } } -impl Serializer for ChronoDuration { - #[inline(always)] - fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> { - let secs = self.num_seconds(); - let nanos = self.subsec_nanos(); - context.writer.write_var_i64(secs); - context.writer.write_i32(nanos); - Ok(()) - } +#[cfg(feature = "chrono")] +mod chrono_support { + use super::*; + use chrono::{Duration as ChronoDuration, NaiveDate, NaiveDateTime}; - #[inline(always)] - fn fory_read_data(context: &mut ReadContext) -> Result { - let secs = context.reader.read_var_i64()?; - let nanos = context.reader.read_i32()?; - if !(-999_999_999..=999_999_999).contains(&nanos) { - // chrono supports negative nanoseconds by applying normalization internally. - return Err(Error::invalid_data(format!( - "duration nanoseconds {} out of valid range [-999_999_999, 999_999_999]", - nanos - ))); - } - ChronoDuration::try_seconds(secs) // the maximum seconds chrono supports is i64::MAX / 1_000, which is smaller than what the spec allows(i64::MAX) - .and_then(|d| d.checked_add(&ChronoDuration::nanoseconds(nanos as i64))) - .ok_or_else(|| { - Error::invalid_data(format!( - "duration seconds {} out of chrono::Duration valid range", - secs - )) - }) - } + impl Serializer for NaiveDateTime { + #[inline(always)] + fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> { + Timestamp::from(*self).fory_write_data(context) + } - #[inline(always)] - fn fory_reserved_space() -> usize { - 9 + mem::size_of::() // max write_var_i64 payload is 9 bytes + 4 bytes for i32 - } + #[inline(always)] + fn fory_read_data(context: &mut ReadContext) -> Result { + Timestamp::fory_read_data(context)?.try_into() + } - #[inline(always)] - fn fory_get_type_id(_: &TypeResolver) -> Result { - Ok(TypeId::DURATION) - } + #[inline(always)] + fn fory_reserved_space() -> usize { + Timestamp::fory_reserved_space() + } - #[inline(always)] - fn fory_type_id_dyn(&self, _: &TypeResolver) -> Result { - Ok(TypeId::DURATION) + #[inline(always)] + fn fory_get_type_id(_: &TypeResolver) -> Result { + Ok(TypeId::TIMESTAMP) + } + + #[inline(always)] + fn fory_type_id_dyn(&self, _: &TypeResolver) -> Result { + Ok(TypeId::TIMESTAMP) + } + + #[inline(always)] + fn fory_static_type_id() -> TypeId { + TypeId::TIMESTAMP + } + + #[inline(always)] + fn as_any(&self) -> &dyn std::any::Any { + self + } + + #[inline(always)] + fn fory_write_type_info(context: &mut WriteContext) -> Result<(), Error> { + Timestamp::fory_write_type_info(context) + } + + #[inline(always)] + fn fory_read_type_info(context: &mut ReadContext) -> Result<(), Error> { + read_basic_type_info::(context) + } } - #[inline(always)] - fn fory_static_type_id() -> TypeId { - TypeId::DURATION + impl ForyDefault for NaiveDateTime { + #[inline(always)] + fn fory_default() -> Self { + NaiveDateTime::default() + } } - #[inline(always)] - fn as_any(&self) -> &dyn std::any::Any { - self + impl Serializer for NaiveDate { + #[inline(always)] + fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> { + Date::from(*self).fory_write_data(context) + } + + #[inline(always)] + fn fory_read_data(context: &mut ReadContext) -> Result { + Date::fory_read_data(context)?.try_into() + } + + #[inline(always)] + fn fory_reserved_space() -> usize { + Date::fory_reserved_space() + } + + #[inline(always)] + fn fory_get_type_id(_: &TypeResolver) -> Result { + Ok(TypeId::DATE) + } + + #[inline(always)] + fn fory_type_id_dyn(&self, _: &TypeResolver) -> Result { + Ok(TypeId::DATE) + } + + #[inline(always)] + fn fory_static_type_id() -> TypeId { + TypeId::DATE + } + + #[inline(always)] + fn as_any(&self) -> &dyn std::any::Any { + self + } + + #[inline(always)] + fn fory_write_type_info(context: &mut WriteContext) -> Result<(), Error> { + Date::fory_write_type_info(context) + } + + #[inline(always)] + fn fory_read_type_info(context: &mut ReadContext) -> Result<(), Error> { + read_basic_type_info::(context) + } } - #[inline(always)] - fn fory_write_type_info(context: &mut WriteContext) -> Result<(), Error> { - context.writer.write_u8(TypeId::DURATION as u8); - Ok(()) + impl ForyDefault for NaiveDate { + #[inline(always)] + fn fory_default() -> Self { + NaiveDate::default() + } } - #[inline(always)] - fn fory_read_type_info(context: &mut ReadContext) -> Result<(), Error> { - read_basic_type_info::(context) + impl Serializer for ChronoDuration { + #[inline(always)] + fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> { + Duration::try_from(*self)?.fory_write_data(context) + } + + #[inline(always)] + fn fory_read_data(context: &mut ReadContext) -> Result { + Duration::fory_read_data(context)?.try_into() + } + + #[inline(always)] + fn fory_reserved_space() -> usize { + Duration::fory_reserved_space() + } + + #[inline(always)] + fn fory_get_type_id(_: &TypeResolver) -> Result { + Ok(TypeId::DURATION) + } + + #[inline(always)] + fn fory_type_id_dyn(&self, _: &TypeResolver) -> Result { + Ok(TypeId::DURATION) + } + + #[inline(always)] + fn fory_static_type_id() -> TypeId { + TypeId::DURATION + } + + #[inline(always)] + fn as_any(&self) -> &dyn std::any::Any { + self + } + + #[inline(always)] + fn fory_write_type_info(context: &mut WriteContext) -> Result<(), Error> { + Duration::fory_write_type_info(context) + } + + #[inline(always)] + fn fory_read_type_info(context: &mut ReadContext) -> Result<(), Error> { + read_basic_type_info::(context) + } } -} -impl ForyDefault for ChronoDuration { - #[inline(always)] - fn fory_default() -> Self { - ChronoDuration::zero() + impl ForyDefault for ChronoDuration { + #[inline(always)] + fn fory_default() -> Self { + ChronoDuration::zero() + } } } @@ -339,80 +387,72 @@ mod tests { use crate::fory::Fory; #[test] - fn test_std_duration_serialization() { + fn test_temporal_carrier_serialization() { let fory = Fory::default(); - // Test various durations - let test_cases = vec![ - Duration::ZERO, - Duration::new(0, 0), - Duration::new(1, 0), - Duration::new(0, 1), - Duration::new(123, 456789), - Duration::new(i64::MAX as u64, 999_999_999), + let timestamps = [ + Timestamp::UNIX_EPOCH, + Timestamp::new(1, 0).unwrap(), + Timestamp::new(-1, 999_999_999).unwrap(), ]; - - for duration in test_cases { - let bytes = fory.serialize(&duration).unwrap(); - let deserialized: Duration = fory.deserialize(&bytes).unwrap(); - assert_eq!( - duration, deserialized, - "Failed for duration: {:?}", - duration - ); + for timestamp in timestamps { + let bytes = fory.serialize(×tamp).unwrap(); + let deserialized: Timestamp = fory.deserialize(&bytes).unwrap(); + assert_eq!(timestamp, deserialized); } - } - - #[test] - fn test_chrono_duration_serialization() { - let fory = Fory::default(); - // Test various durations - let test_cases = vec![ - ChronoDuration::zero(), - ChronoDuration::new(0, 0).unwrap(), - ChronoDuration::new(1, 0).unwrap(), - ChronoDuration::new(0, 1).unwrap(), - ChronoDuration::new(123, 456789).unwrap(), - ChronoDuration::seconds(-1), - ChronoDuration::nanoseconds(-1), - ChronoDuration::microseconds(-456789), - ChronoDuration::MAX, - ChronoDuration::MIN, + let dates = [ + Date::UNIX_EPOCH, + Date::from_epoch_days(-1), + Date::from_epoch_days(18_628), ]; + for date in dates { + let bytes = fory.serialize(&date).unwrap(); + let deserialized: Date = fory.deserialize(&bytes).unwrap(); + assert_eq!(date, deserialized); + } - for duration in test_cases { + let durations = [ + Duration::ZERO, + Duration::new(1, 0).unwrap(), + Duration::new(0, -1).unwrap(), + Duration::new(-123, 456_789).unwrap(), + ]; + for duration in durations { let bytes = fory.serialize(&duration).unwrap(); - let deserialized: ChronoDuration = fory.deserialize(&bytes).unwrap(); - assert_eq!( - duration, deserialized, - "Failed for duration: {:?}", - duration - ); + let deserialized: Duration = fory.deserialize(&bytes).unwrap(); + assert_eq!(duration, deserialized); } } #[test] - fn test_chrono_duration_out_of_range_is_error() { - let fory = Fory::default(); - let too_large = Duration::new(i64::MAX as u64, 0); - let bytes = fory.serialize(&too_large).unwrap(); - let result: Result = fory.deserialize(&bytes); - assert!( - result.is_err(), - "out-of-range seconds should not be deserialized into chrono::Duration!" + fn test_duration_normalizes_negative_nanoseconds() { + assert_eq!( + Duration::new(0, -1).unwrap(), + Duration::from_normalized(-1, 999_999_999).unwrap() ); } + #[cfg(feature = "chrono")] #[test] - fn test_negative_std_duration_read_is_error() { + fn test_chrono_temporal_feature_serialization() { + use chrono::{DateTime, Duration as ChronoDuration, NaiveDate, NaiveDateTime}; + let fory = Fory::default(); - let negative_duration = ChronoDuration::seconds(-1); - let bytes = fory.serialize(&negative_duration).unwrap(); - let result: Result = fory.deserialize(&bytes); - assert!( - result.is_err(), - "negative duration should not be deserialized into std::time::Duration!" - ); + let date = NaiveDate::from_ymd_opt(2024, 2, 3).unwrap(); + let timestamp = DateTime::from_timestamp(100, 1).unwrap().naive_utc(); + let duration = ChronoDuration::nanoseconds(-1); + + let bytes = fory.serialize(&date).unwrap(); + let deserialized: NaiveDate = fory.deserialize(&bytes).unwrap(); + assert_eq!(date, deserialized); + + let bytes = fory.serialize(×tamp).unwrap(); + let deserialized: NaiveDateTime = fory.deserialize(&bytes).unwrap(); + assert_eq!(timestamp, deserialized); + + let bytes = fory.serialize(&duration).unwrap(); + let deserialized: ChronoDuration = fory.deserialize(&bytes).unwrap(); + assert_eq!(duration, deserialized); } } diff --git a/rust/fory-core/src/serializer/skip.rs b/rust/fory-core/src/serializer/skip.rs index 944dadf341..4d8b90651d 100644 --- a/rust/fory-core/src/serializer/skip.rs +++ b/rust/fory-core/src/serializer/skip.rs @@ -23,9 +23,9 @@ use crate::serializer::collection::{DECL_ELEMENT_TYPE, HAS_NULL, IS_SAME_TYPE}; use crate::serializer::util; use crate::serializer::Serializer; use crate::type_id as types; +use crate::types::{Date, Duration, Timestamp}; use crate::util::ENABLE_FORY_DEBUG_OUTPUT; use crate::RefFlag; -use chrono::{Duration, NaiveDate, NaiveDateTime}; use std::rc::Rc; #[allow(unreachable_code)] @@ -608,12 +608,12 @@ fn skip_value( // ============ TIMESTAMP (TypeId = 36) ============ types::TIMESTAMP => { - ::fory_read_data(context)?; + ::fory_read_data(context)?; } // ============ DATE (TypeId = 37) ============ types::DATE => { - ::fory_read_data(context)?; + ::fory_read_data(context)?; } // ============ DECIMAL (TypeId = 38) ============ diff --git a/rust/fory-core/src/type_id.rs b/rust/fory-core/src/type_id.rs index 5c7ed103e6..9b7db9e0d9 100644 --- a/rust/fory-core/src/type_id.rs +++ b/rust/fory-core/src/type_id.rs @@ -184,7 +184,7 @@ pub const fn is_enum_type_id(type_id: TypeId) -> bool { matches!(type_id, TypeId::ENUM | TypeId::NAMED_ENUM | TypeId::UNION) } -pub static BASIC_TYPES: [TypeId; 34] = [ +pub static BASIC_TYPES: [TypeId; 35] = [ TypeId::BOOL, TypeId::INT8, TypeId::INT16, @@ -200,6 +200,7 @@ pub static BASIC_TYPES: [TypeId; 34] = [ TypeId::STRING, TypeId::DATE, TypeId::TIMESTAMP, + TypeId::DURATION, TypeId::BOOL_ARRAY, TypeId::BINARY, TypeId::INT8_ARRAY, @@ -272,7 +273,7 @@ pub static PRIMITIVE_ARRAY_TYPES: [u32; 19] = [ TypeId::USIZE_ARRAY as u32, TypeId::ISIZE_ARRAY as u32, ]; -pub static BASIC_TYPE_NAMES: [&str; 20] = [ +pub static BASIC_TYPE_NAMES: [&str; 21] = [ "bool", "i8", "i16", @@ -282,8 +283,9 @@ pub static BASIC_TYPE_NAMES: [&str; 20] = [ "f32", "f64", "String", - "NaiveDate", - "NaiveDateTime", + "Date", + "Timestamp", + "Duration", "u8", "u16", "u32", diff --git a/rust/fory-core/src/types/mod.rs b/rust/fory-core/src/types/mod.rs index c204d91f18..673d004c68 100644 --- a/rust/fory-core/src/types/mod.rs +++ b/rust/fory-core/src/types/mod.rs @@ -18,7 +18,9 @@ pub mod bfloat16; pub mod decimal; pub mod float16; +pub mod temporal; pub mod weak; pub use decimal::Decimal; +pub use temporal::{Date, Duration, Timestamp}; pub use weak::{ArcWeak, RcWeak}; diff --git a/rust/fory-core/src/types/temporal.rs b/rust/fory-core/src/types/temporal.rs new file mode 100644 index 0000000000..8b7fc84408 --- /dev/null +++ b/rust/fory-core/src/types/temporal.rs @@ -0,0 +1,284 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::error::Error; + +const NANOS_PER_SECOND: i32 = 1_000_000_000; +const MICROS_PER_SECOND: i64 = 1_000_000; + +/// Date without timezone, represented as signed days since Unix epoch. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Date { + days: i64, +} + +impl Date { + pub const UNIX_EPOCH: Self = Self { days: 0 }; + + #[inline(always)] + pub const fn from_epoch_days(days: i64) -> Self { + Self { days } + } + + #[inline(always)] + pub const fn epoch_days(self) -> i64 { + self.days + } +} + +/// Point in time, represented as seconds and nanoseconds since Unix epoch. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Timestamp { + seconds: i64, + nanos: u32, +} + +impl Timestamp { + pub const UNIX_EPOCH: Self = Self { + seconds: 0, + nanos: 0, + }; + + #[inline(always)] + pub fn new(seconds: i64, nanos: u32) -> Result { + if nanos >= NANOS_PER_SECOND as u32 { + return Err(Error::invalid_data(format!( + "timestamp nanoseconds {} out of valid range [0, 999_999_999]", + nanos + ))); + } + Ok(Self { seconds, nanos }) + } + + #[inline(always)] + pub const fn seconds(self) -> i64 { + self.seconds + } + + #[inline(always)] + pub const fn subsec_nanos(self) -> u32 { + self.nanos + } + + #[inline(always)] + pub fn from_epoch_micros(micros: i64) -> Self { + let seconds = micros.div_euclid(MICROS_PER_SECOND); + let micros = micros.rem_euclid(MICROS_PER_SECOND); + Self { + seconds, + nanos: (micros as u32) * 1_000, + } + } + + #[inline(always)] + pub fn to_epoch_micros(self) -> Result { + let seconds = self.seconds.checked_mul(MICROS_PER_SECOND).ok_or_else(|| { + Error::invalid_data(format!( + "timestamp seconds {} overflow microsecond conversion", + self.seconds + )) + })?; + seconds + .checked_add(i64::from(self.nanos / 1_000)) + .ok_or_else(|| { + Error::invalid_data(format!( + "timestamp {:?} overflow microsecond conversion", + self + )) + }) + } +} + +/// Signed duration, represented as seconds and normalized nanoseconds. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Duration { + seconds: i64, + nanos: u32, +} + +impl Duration { + pub const ZERO: Self = Self { + seconds: 0, + nanos: 0, + }; + + #[inline(always)] + pub fn new(seconds: i64, nanos: i32) -> Result { + if !(-(NANOS_PER_SECOND - 1)..=(NANOS_PER_SECOND - 1)).contains(&nanos) { + return Err(Error::invalid_data(format!( + "duration nanoseconds {} out of valid range [-999_999_999, 999_999_999]", + nanos + ))); + } + if nanos < 0 { + let seconds = seconds.checked_sub(1).ok_or_else(|| { + Error::invalid_data( + "duration seconds underflow while normalizing negative nanoseconds", + ) + })?; + return Ok(Self { + seconds, + nanos: (nanos + NANOS_PER_SECOND) as u32, + }); + } + Ok(Self { + seconds, + nanos: nanos as u32, + }) + } + + #[inline(always)] + pub fn from_normalized(seconds: i64, nanos: u32) -> Result { + if nanos >= NANOS_PER_SECOND as u32 { + return Err(Error::invalid_data(format!( + "duration nanoseconds {} out of valid range [0, 999_999_999]", + nanos + ))); + } + Ok(Self { seconds, nanos }) + } + + #[inline(always)] + pub const fn seconds(self) -> i64 { + self.seconds + } + + #[inline(always)] + pub const fn subsec_nanos(self) -> u32 { + self.nanos + } + + #[inline(always)] + pub fn from_micros(micros: i64) -> Self { + let seconds = micros.div_euclid(MICROS_PER_SECOND); + let micros = micros.rem_euclid(MICROS_PER_SECOND); + Self { + seconds, + nanos: (micros as u32) * 1_000, + } + } + + #[inline(always)] + pub fn to_micros(self) -> Result { + let seconds = self.seconds.checked_mul(MICROS_PER_SECOND).ok_or_else(|| { + Error::invalid_data(format!( + "duration seconds {} overflow microsecond conversion", + self.seconds + )) + })?; + seconds + .checked_add(i64::from(self.nanos / 1_000)) + .ok_or_else(|| { + Error::invalid_data(format!("{:?} overflow microsecond conversion", self)) + }) + } +} + +#[cfg(feature = "chrono")] +mod chrono_support { + use super::{Date, Duration, Timestamp}; + use crate::error::Error; + use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeDelta}; + + fn epoch() -> NaiveDate { + NaiveDate::from_ymd_opt(1970, 1, 1).expect("1970-01-01 is a valid chrono date") + } + + impl From for Date { + #[inline(always)] + fn from(value: NaiveDate) -> Self { + Self::from_epoch_days(value.signed_duration_since(epoch()).num_days()) + } + } + + impl TryFrom for NaiveDate { + type Error = Error; + + #[inline(always)] + fn try_from(value: Date) -> Result { + let duration = TimeDelta::try_days(value.epoch_days()).ok_or_else(|| { + Error::invalid_data(format!( + "date day count {} is out of chrono::TimeDelta range", + value.epoch_days() + )) + })?; + epoch().checked_add_signed(duration).ok_or_else(|| { + Error::invalid_data(format!( + "date day count {} is out of chrono::NaiveDate range", + value.epoch_days() + )) + }) + } + } + + impl From for Timestamp { + #[inline(always)] + fn from(value: NaiveDateTime) -> Self { + let value = value.and_utc(); + Self { + seconds: value.timestamp(), + nanos: value.timestamp_subsec_nanos(), + } + } + } + + impl TryFrom for NaiveDateTime { + type Error = Error; + + #[inline(always)] + fn try_from(value: Timestamp) -> Result { + DateTime::from_timestamp(value.seconds(), value.subsec_nanos()) + .map(|value| value.naive_utc()) + .ok_or_else(|| { + Error::invalid_data(format!( + "timestamp seconds {} nanoseconds {} is out of chrono::NaiveDateTime range", + value.seconds(), + value.subsec_nanos() + )) + }) + } + } + + impl TryFrom for Duration { + type Error = Error; + + #[inline(always)] + fn try_from(value: chrono::Duration) -> Result { + Self::new(value.num_seconds(), value.subsec_nanos()) + } + } + + impl TryFrom for chrono::Duration { + type Error = Error; + + #[inline(always)] + fn try_from(value: Duration) -> Result { + chrono::Duration::try_seconds(value.seconds()) + .and_then(|duration| { + duration.checked_add(&chrono::Duration::nanoseconds(i64::from( + value.subsec_nanos(), + ))) + }) + .ok_or_else(|| { + Error::invalid_data(format!( + "duration seconds {} out of chrono::Duration valid range", + value.seconds() + )) + }) + } + } +} diff --git a/rust/fory-core/src/util/mod.rs b/rust/fory-core/src/util/mod.rs index 85d3be0dc6..0d36492c01 100644 --- a/rust/fory-core/src/util/mod.rs +++ b/rust/fory-core/src/util/mod.rs @@ -24,15 +24,6 @@ pub use string_util::{ }; pub use sync::{Spinlock, SpinlockGuard}; -use chrono::NaiveDate; - -pub const EPOCH: NaiveDate = match NaiveDate::from_ymd_opt(1970, 1, 1) { - None => { - panic!("Unreachable code") - } - Some(epoch) => epoch, -}; - /// Set `ENABLE_FORY_DEBUG_OUTPUT=1` at compile time to enable debug output. #[allow(unexpected_cfgs)] pub const ENABLE_FORY_DEBUG_OUTPUT: bool = cfg!(fory_debug_output); diff --git a/rust/fory-derive/src/lib.rs b/rust/fory-derive/src/lib.rs index f8a726ef19..15ff08df77 100644 --- a/rust/fory-derive/src/lib.rs +++ b/rust/fory-derive/src/lib.rs @@ -133,8 +133,10 @@ //! - `Option` for nullable values //! //! **Date/Time:** -//! - `chrono::NaiveDate` -//! - `chrono::NaiveDateTime` +//! - `fory::Date` +//! - `fory::Timestamp` +//! - `fory::Duration` +//! - `chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration` when the `chrono` feature is enabled //! //! **Custom Types:** //! - Any type that implements `Serializer` (for `Fory`) or `Row` (for `ForyRow`) diff --git a/rust/fory-derive/src/object/util.rs b/rust/fory-derive/src/object/util.rs index 6239d5bc45..d7593fc6e2 100644 --- a/rust/fory-derive/src/object/util.rs +++ b/rust/fory-derive/src/object/util.rs @@ -294,9 +294,11 @@ pub(crate) fn get_type_id_by_name(ty: &str) -> u32 { // Check internal types match unqualified_ty { "String" => return TypeId::STRING as u32, + "Date" => return TypeId::DATE as u32, + "Timestamp" => return TypeId::TIMESTAMP as u32, + "Duration" => return TypeId::DURATION as u32, "NaiveDate" => return TypeId::DATE as u32, "NaiveDateTime" => return TypeId::TIMESTAMP as u32, - "Duration" => return TypeId::DURATION as u32, "Decimal" => return TypeId::DECIMAL as u32, "bytes" => return TypeId::BINARY as u32, _ => {} diff --git a/rust/fory/Cargo.toml b/rust/fory/Cargo.toml index a8c2645b90..bdc051c470 100644 --- a/rust/fory/Cargo.toml +++ b/rust/fory/Cargo.toml @@ -31,3 +31,7 @@ description = "Apache Fory: Blazingly fast multi-language serialization framewor [dependencies] fory-core.workspace = true fory-derive.workspace = true + +[features] +default = [] +chrono = ["fory-core/chrono"] diff --git a/rust/fory/src/lib.rs b/rust/fory/src/lib.rs index af9e019fd0..81bb59c34e 100644 --- a/rust/fory/src/lib.rs +++ b/rust/fory/src/lib.rs @@ -979,10 +979,12 @@ //! - `RefCell` - Interior mutability with runtime borrow checking //! - `Mutex` - Thread-safe interior mutability //! -//! ### Date and Time (requires `chrono` feature) +//! ### Date and Time //! -//! - `chrono::NaiveDate` - Date without timezone -//! - `chrono::NaiveDateTime` - Timestamp without timezone +//! - `Date` - Date without timezone +//! - `Timestamp` - Point in time +//! - `Duration` - Signed duration +//! - `chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration` when the `chrono` feature is enabled //! //! ### Custom Types //! @@ -1202,7 +1204,7 @@ pub use fory_core::{ error::Error, fory::Fory, fory::ForyBuilder, register_trait_type, row::from_row, row::to_row, - ArcWeak, BFloat16, Decimal, Float16, ForyDefault, RcWeak, ReadContext, Reader, RefFlag, - RefMode, Serializer, TypeId, TypeResolver, WriteContext, Writer, + ArcWeak, BFloat16, Date, Decimal, Duration, Float16, ForyDefault, RcWeak, ReadContext, Reader, + RefFlag, RefMode, Serializer, Timestamp, TypeId, TypeResolver, WriteContext, Writer, }; pub use fory_derive::{ForyEnum, ForyRow, ForyStruct, ForyUnion}; diff --git a/rust/tests/Cargo.toml b/rust/tests/Cargo.toml index 59e8546703..1b439677dd 100644 --- a/rust/tests/Cargo.toml +++ b/rust/tests/Cargo.toml @@ -26,5 +26,9 @@ publish = false fory-core = { path = "../fory-core" } fory-derive = { path = "../fory-derive" } -chrono = "0.4" +chrono = { version = "0.4", optional = true } num-bigint = "0.4" + +[features] +default = [] +chrono = ["dep:chrono", "fory-core/chrono"] diff --git a/rust/tests/tests/compatible/test_basic_type.rs b/rust/tests/tests/compatible/test_basic_type.rs index d1e1244112..5ebbe33a18 100644 --- a/rust/tests/tests/compatible/test_basic_type.rs +++ b/rust/tests/tests/compatible/test_basic_type.rs @@ -15,8 +15,7 @@ // specific language governing permissions and limitations // under the License. -use chrono::{NaiveDate, NaiveDateTime}; -use fory_core::{fory::Fory, Reader}; +use fory_core::{fory::Fory, Date, Reader, Timestamp}; // primitive_val const BOOL_VAL: bool = true; @@ -29,10 +28,13 @@ const F64_VAL: f64 = 47.0; // string const STR_LATIN1_VAL: &str = "Çüéâäàåçêëèïî"; // time -#[allow(deprecated)] -const TIMESTAMP_VAL: NaiveDateTime = NaiveDateTime::from_timestamp(100, 0); -#[allow(deprecated)] -const LOCAL_DATE_VAL: NaiveDate = NaiveDate::from_ymd(2021, 11, 23); +fn timestamp_val() -> Timestamp { + Timestamp::new(100, 0).unwrap() +} + +fn local_date_val() -> Date { + Date::from_epoch_days(18_954) +} const BOOL_ARRAY: [bool; 1] = [true]; const INT8_ARRAY: [i8; 1] = [48]; @@ -53,8 +55,8 @@ fn serialize_non_null(fory: &Fory) -> Vec { fory.serialize_to(&mut buf, &F64_VAL).unwrap(); fory.serialize_to(&mut buf, &STR_LATIN1_VAL.to_string()) .unwrap(); - fory.serialize_to(&mut buf, &LOCAL_DATE_VAL).unwrap(); - fory.serialize_to(&mut buf, &TIMESTAMP_VAL).unwrap(); + fory.serialize_to(&mut buf, &local_date_val()).unwrap(); + fory.serialize_to(&mut buf, ×tamp_val()).unwrap(); fory.serialize_to(&mut buf, &BOOL_ARRAY.to_vec()).unwrap(); fory.serialize_to(&mut buf, &INT8_ARRAY.to_vec()).unwrap(); fory.serialize_to(&mut buf, &INT16_ARRAY.to_vec()).unwrap(); @@ -78,8 +80,9 @@ fn serialize_nullable(fory: &Fory) -> Vec { fory.serialize_to(&mut buf, &Some(F64_VAL)).unwrap(); fory.serialize_to(&mut buf, &Some(STR_LATIN1_VAL.to_string())) .unwrap(); - fory.serialize_to(&mut buf, &Some(LOCAL_DATE_VAL)).unwrap(); - fory.serialize_to(&mut buf, &Some(TIMESTAMP_VAL)).unwrap(); + fory.serialize_to(&mut buf, &Some(local_date_val())) + .unwrap(); + fory.serialize_to(&mut buf, &Some(timestamp_val())).unwrap(); fory.serialize_to(&mut buf, &Some(BOOL_ARRAY.to_vec())) .unwrap(); fory.serialize_to(&mut buf, &Some(INT8_ARRAY.to_vec())) @@ -103,9 +106,8 @@ fn serialize_nullable(fory: &Fory) -> Vec { fory.serialize_to(&mut buf, &Option::::None).unwrap(); fory.serialize_to(&mut buf, &Option::::None) .unwrap(); - fory.serialize_to(&mut buf, &Option::::None) - .unwrap(); - fory.serialize_to(&mut buf, &Option::::None) + fory.serialize_to(&mut buf, &Option::::None).unwrap(); + fory.serialize_to(&mut buf, &Option::::None) .unwrap(); fory.serialize_to(&mut buf, &Option::>::None) .unwrap(); @@ -141,12 +143,12 @@ fn deserialize_non_null(fory: &Fory, bins: Vec, auto_conv: bool) { fory.deserialize_from::(&mut reader).unwrap() ); assert_eq!( - LOCAL_DATE_VAL, - fory.deserialize_from::(&mut reader).unwrap() + local_date_val(), + fory.deserialize_from::(&mut reader).unwrap() ); assert_eq!( - TIMESTAMP_VAL, - fory.deserialize_from::(&mut reader).unwrap() + timestamp_val(), + fory.deserialize_from::(&mut reader).unwrap() ); assert_eq!( @@ -211,12 +213,12 @@ fn deserialize_non_null(fory: &Fory, bins: Vec, auto_conv: bool) { fory.deserialize_from::(&mut reader).unwrap() ); assert_eq!( - NaiveDate::default(), - fory.deserialize_from::(&mut reader).unwrap() + Date::default(), + fory.deserialize_from::(&mut reader).unwrap() ); assert_eq!( - NaiveDateTime::default(), - fory.deserialize_from::(&mut reader).unwrap() + Timestamp::default(), + fory.deserialize_from::(&mut reader).unwrap() ); assert_eq!( @@ -286,13 +288,12 @@ fn deserialize_nullable(fory: &Fory, bins: Vec, auto_conv: bool) { .unwrap() ); assert_eq!( - Some(LOCAL_DATE_VAL), - fory.deserialize_from::>(&mut reader) - .unwrap() + Some(local_date_val()), + fory.deserialize_from::>(&mut reader).unwrap() ); assert_eq!( - Some(TIMESTAMP_VAL), - fory.deserialize_from::>(&mut reader) + Some(timestamp_val()), + fory.deserialize_from::>(&mut reader) .unwrap() ); assert_eq!( @@ -366,12 +367,11 @@ fn deserialize_nullable(fory: &Fory, bins: Vec, auto_conv: bool) { ); assert_eq!( None, - fory.deserialize_from::>(&mut reader) - .unwrap() + fory.deserialize_from::>(&mut reader).unwrap() ); assert_eq!( None, - fory.deserialize_from::>(&mut reader) + fory.deserialize_from::>(&mut reader) .unwrap() ); assert_eq!( diff --git a/rust/tests/tests/test_complex_struct.rs b/rust/tests/tests/test_complex_struct.rs index 55e4a4f68e..4e5fe5cd6e 100644 --- a/rust/tests/tests/test_complex_struct.rs +++ b/rust/tests/tests/test_complex_struct.rs @@ -16,9 +16,9 @@ // under the License. use fory_core::fory::Fory; +use fory_core::{Date, Timestamp}; use fory_derive::{ForyEnum, ForyStruct}; // use std::any::Any; -use chrono::{DateTime, NaiveDate, NaiveDateTime}; use std::collections::HashMap; // RUSTFLAGS="-Awarnings" cargo expand -p tests --test test_complex_struct @@ -83,8 +83,8 @@ fn complex_struct() { // age: u16, op: Option, op2: Option, - date: NaiveDate, - time: NaiveDateTime, + date: Date, + time: Timestamp, c5: f32, c6: f64, } @@ -103,8 +103,8 @@ fn complex_struct() { // age: 12, op: Some("option".to_string()), op2: None, - date: NaiveDate::from_ymd_opt(2025, 12, 12).unwrap(), - time: DateTime::from_timestamp(1689912359, 0).unwrap().naive_utc(), + date: Date::from_epoch_days(20_434), + time: Timestamp::new(1_689_912_359, 0).unwrap(), c5: 2.0, c6: 4.0, }; diff --git a/rust/tests/tests/test_cross_language.rs b/rust/tests/tests/test_cross_language.rs index 8138878fbf..1d100c3221 100644 --- a/rust/tests/tests/test_cross_language.rs +++ b/rust/tests/tests/test_cross_language.rs @@ -15,14 +15,13 @@ // specific language governing permissions and limitations // under the License. -use chrono::{NaiveDate, NaiveDateTime}; use fory_core::buffer::{Reader, Writer}; use fory_core::error::Error; use fory_core::resolver::TypeResolver; use fory_core::serializer::{ForyDefault, Serializer}; use fory_core::type_id::TypeId; use fory_core::util::murmurhash3_x64_128; -use fory_core::{read_data, write_data, BFloat16, Decimal, Float16, Fory}; +use fory_core::{read_data, write_data, BFloat16, Date, Decimal, Float16, Fory, Timestamp}; use fory_core::{ReadContext, WriteContext}; use fory_derive::{ForyEnum, ForyStruct, ForyUnion}; use num_bigint::BigInt; @@ -116,14 +115,13 @@ fn test_buffer() { } #[test] -#[allow(deprecated)] -fn test_naive_date_uses_var_i64_day_count() { +fn test_date_uses_var_i64_day_count() { let fory = Fory::builder() .xlang(true) .compatible(false) .track_ref(false) .build(); - let day = NaiveDate::from_ymd_opt(1969, 12, 31).unwrap(); + let day = Date::from_epoch_days(-1); let mut buf = Vec::new(); fory.serialize_to(&mut buf, &day).unwrap(); @@ -136,10 +134,9 @@ fn test_naive_date_uses_var_i64_day_count() { } #[test] -#[allow(deprecated)] -fn test_naive_date_uses_i32_day_count_in_native_mode() { +fn test_date_uses_i32_day_count_in_native_mode() { let fory = Fory::builder().xlang(false).track_ref(false).build(); - let day = NaiveDate::from_ymd_opt(1969, 12, 31).unwrap(); + let day = Date::from_epoch_days(-1); let mut buf = Vec::new(); fory.serialize_to(&mut buf, &day).unwrap(); @@ -330,10 +327,9 @@ macro_rules! assert_de { #[test] #[ignore] -#[allow(deprecated)] fn test_cross_language_serializer() { - let day = NaiveDate::from_ymd_opt(2021, 11, 23).unwrap(); - let instant = NaiveDateTime::from_timestamp(100, 0); + let day = Date::from_epoch_days(18_954); + let instant = Timestamp::new(100, 0).unwrap(); let str_list = vec!["hello".to_string(), "world".to_string()]; let str_set = HashSet::from(["hello".to_string(), "world".to_string()]); let str_map = HashMap::::from([ @@ -361,8 +357,8 @@ fn test_cross_language_serializer() { assert_de!(fory, reader, f32, -1f32); assert_de!(fory, reader, f64, -1f64); assert_de!(fory, reader, String, "str".to_string()); - assert_de!(fory, reader, NaiveDate, day); - assert_de!(fory, reader, NaiveDateTime, instant); + assert_de!(fory, reader, Date, day); + assert_de!(fory, reader, Timestamp, instant); assert_de!(fory, reader, Vec, [true, false]); assert_de!(fory, reader, Vec, [1, i8::MAX as u8]); assert_de!(fory, reader, Vec, [1, i16::MAX]); diff --git a/rust/tests/tests/test_fory.rs b/rust/tests/tests/test_fory.rs index 3b141a0eef..596ac11883 100644 --- a/rust/tests/tests/test_fory.rs +++ b/rust/tests/tests/test_fory.rs @@ -197,7 +197,7 @@ fn test_serialize_to_detailed() { } } -use chrono::{DateTime, NaiveDateTime, Utc}; +use fory_core::Timestamp; macro_rules! impl_value { ($record:ident, $value:ident, { $($field:ident : $ty:ty = $expr:expr),* $(,)? }) => { @@ -227,7 +227,7 @@ macro_rules! impl_value { pub struct KeyValue { feature_key: String, count: u64, - last_seen_event_time: DateTime, + last_seen_event_time: Timestamp, } impl_value!( @@ -235,7 +235,7 @@ impl_value!( Value, { count: u64 = count, - last_seen_event_time: NaiveDateTime = last_seen_event_time.naive_utc(), + last_seen_event_time: Timestamp = last_seen_event_time, } ); @@ -244,15 +244,12 @@ fn test_in_macro() { let key_value = KeyValue { feature_key: "test_key".to_string(), count: 100, - last_seen_event_time: Utc::now(), + last_seen_event_time: Timestamp::new(1_689_912_359, 123).unwrap(), }; let (key, value) = key_value.clone().to_key_value(); assert_eq!(key, "test_key"); assert_eq!(value.count, 100); - assert_eq!( - value.last_seen_event_time, - key_value.last_seen_event_time.naive_utc() - ); + assert_eq!(value.last_seen_event_time, key_value.last_seen_event_time); } #[test] From 2b97c9daaa34e340ae0251a5a2888b13cbcd5666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Fri, 15 May 2026 20:10:37 +0800 Subject: [PATCH 2/5] test(rust): define tests feature for cargo command --- rust/fory-core/Cargo.toml | 1 + rust/fory-derive/Cargo.toml | 3 ++- rust/fory/Cargo.toml | 1 + rust/tests/Cargo.toml | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/rust/fory-core/Cargo.toml b/rust/fory-core/Cargo.toml index da39378e09..d95d16e497 100644 --- a/rust/fory-core/Cargo.toml +++ b/rust/fory-core/Cargo.toml @@ -41,6 +41,7 @@ num-bigint = "0.4" [features] default = [] chrono = ["dep:chrono"] +tests = [] [[bench]] name = "simd_bench" diff --git a/rust/fory-derive/Cargo.toml b/rust/fory-derive/Cargo.toml index 1c0c9067e0..115d811423 100644 --- a/rust/fory-derive/Cargo.toml +++ b/rust/fory-derive/Cargo.toml @@ -44,4 +44,5 @@ thiserror = { default-features = false, version = "1.0" } [features] default = ["fields-loop-unroll"] -fields-loop-unroll = [] \ No newline at end of file +fields-loop-unroll = [] +tests = [] diff --git a/rust/fory/Cargo.toml b/rust/fory/Cargo.toml index bdc051c470..52782f3b85 100644 --- a/rust/fory/Cargo.toml +++ b/rust/fory/Cargo.toml @@ -35,3 +35,4 @@ fory-derive.workspace = true [features] default = [] chrono = ["fory-core/chrono"] +tests = [] diff --git a/rust/tests/Cargo.toml b/rust/tests/Cargo.toml index 1b439677dd..c8eb79fd40 100644 --- a/rust/tests/Cargo.toml +++ b/rust/tests/Cargo.toml @@ -32,3 +32,4 @@ num-bigint = "0.4" [features] default = [] chrono = ["dep:chrono", "fory-core/chrono"] +tests = [] From 7daf0d05929deccb6356a8db4c28fef30d78b5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Fri, 15 May 2026 20:43:17 +0800 Subject: [PATCH 3/5] chore(rust): remove unused direct dependencies --- rust/fory-core/Cargo.toml | 3 --- rust/fory-derive/Cargo.toml | 1 - rust/tests/Cargo.toml | 3 +-- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/rust/fory-core/Cargo.toml b/rust/fory-core/Cargo.toml index d95d16e497..9fae8c6ff1 100644 --- a/rust/fory-core/Cargo.toml +++ b/rust/fory-core/Cargo.toml @@ -28,9 +28,6 @@ keywords = ["serialization", "serde", "trait-object", "zero-copy", "schema-evolu categories = ["encoding"] [dependencies] -proc-macro2 = { default-features = false, version = "1.0" } -syn = { default-features = false, version = "2.0", features = ["full", "fold"] } -quote = { default-features = false, version = "1.0" } byteorder = { version = "1.4" } chrono = { version = "0.4", default-features = false, features = ["std"], optional = true } thiserror = { default-features = false, version = "1.0" } diff --git a/rust/fory-derive/Cargo.toml b/rust/fory-derive/Cargo.toml index 115d811423..15b4c47b79 100644 --- a/rust/fory-derive/Cargo.toml +++ b/rust/fory-derive/Cargo.toml @@ -40,7 +40,6 @@ syn = { default-features = false, version = "2.0", features = [ "printing", ] } quote = { default-features = false, version = "1.0" } -thiserror = { default-features = false, version = "1.0" } [features] default = ["fields-loop-unroll"] diff --git a/rust/tests/Cargo.toml b/rust/tests/Cargo.toml index c8eb79fd40..c2f2fdc254 100644 --- a/rust/tests/Cargo.toml +++ b/rust/tests/Cargo.toml @@ -26,10 +26,9 @@ publish = false fory-core = { path = "../fory-core" } fory-derive = { path = "../fory-derive" } -chrono = { version = "0.4", optional = true } num-bigint = "0.4" [features] default = [] -chrono = ["dep:chrono", "fory-core/chrono"] +chrono = ["fory-core/chrono"] tests = [] From f057034ef3a42081c28c2fd5d9c2cd6febca8a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Fri, 15 May 2026 22:05:00 +0800 Subject: [PATCH 4/5] feat(rust): expand temporal carrier APIs --- docs/guide/rust/basic-serialization.md | 17 + rust/fory-core/src/types/temporal.rs | 541 +++++++++++++++++++++++-- rust/fory/src/lib.rs | 6 +- 3 files changed, 530 insertions(+), 34 deletions(-) diff --git a/docs/guide/rust/basic-serialization.md b/docs/guide/rust/basic-serialization.md index db6db0b4bc..63bb9a3551 100644 --- a/docs/guide/rust/basic-serialization.md +++ b/docs/guide/rust/basic-serialization.md @@ -125,6 +125,23 @@ assert_eq!(person, decoded); | `Timestamp` | Point in time, stored as epoch seconds and nanos | | `Duration` | Signed duration, stored as seconds and normalized nanos | +The built-in carriers expose dependency-free constructors, accessors, conversions, and checked +arithmetic: + +```rust +use fory::{Date, Duration, Timestamp}; + +let date = Date::from_epoch_days(19_782); +assert_eq!(date.checked_add_days(1)?.epoch_days(), 19_783); + +let timestamp = Timestamp::from_epoch_millis(-1); +assert_eq!(timestamp.to_epoch_millis()?, -1); + +let duration = Duration::from_parts(1, 1_500_000_000)?; +assert_eq!(duration.to_millis()?, 2_500); +let later = timestamp.checked_add_duration(duration)?; +``` + `chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration` are supported when the Rust `chrono` feature is enabled: diff --git a/rust/fory-core/src/types/temporal.rs b/rust/fory-core/src/types/temporal.rs index 8b7fc84408..de3eb36bef 100644 --- a/rust/fory-core/src/types/temporal.rs +++ b/rust/fory-core/src/types/temporal.rs @@ -18,7 +18,14 @@ use crate::error::Error; const NANOS_PER_SECOND: i32 = 1_000_000_000; +const NANOS_PER_SECOND_I128: i128 = NANOS_PER_SECOND as i128; +const NANOS_PER_MILLI: i128 = 1_000_000; +const NANOS_PER_MICRO: i128 = 1_000; +const MILLIS_PER_SECOND: i64 = 1_000; const MICROS_PER_SECOND: i64 = 1_000_000; +const SECONDS_PER_MINUTE: i64 = 60; +const SECONDS_PER_HOUR: i64 = 60 * SECONDS_PER_MINUTE; +const SECONDS_PER_DAY: i64 = 24 * SECONDS_PER_HOUR; /// Date without timezone, represented as signed days since Unix epoch. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -29,15 +36,56 @@ pub struct Date { impl Date { pub const UNIX_EPOCH: Self = Self { days: 0 }; + /// Creates a date from signed days since 1970-01-01. #[inline(always)] pub const fn from_epoch_days(days: i64) -> Self { Self { days } } + /// Returns the signed day count from 1970-01-01. #[inline(always)] pub const fn epoch_days(self) -> i64 { self.days } + + /// Returns a date offset by `days`. + #[inline(always)] + pub fn checked_add_days(self, days: i64) -> Result { + self.days + .checked_add(days) + .map(Self::from_epoch_days) + .ok_or_else(|| { + Error::invalid_data(format!( + "date day count {} overflow adding {} days", + self.days, days + )) + }) + } + + /// Returns a date offset backward by `days`. + #[inline(always)] + pub fn checked_sub_days(self, days: i64) -> Result { + self.days + .checked_sub(days) + .map(Self::from_epoch_days) + .ok_or_else(|| { + Error::invalid_data(format!( + "date day count {} overflow subtracting {} days", + self.days, days + )) + }) + } + + /// Returns the signed day distance from `earlier` to this date. + #[inline(always)] + pub fn days_since(self, earlier: Self) -> Result { + self.days.checked_sub(earlier.days).ok_or_else(|| { + Error::invalid_data(format!( + "date day count {} overflow subtracting {}", + self.days, earlier.days + )) + }) + } } /// Point in time, represented as seconds and nanoseconds since Unix epoch. @@ -53,6 +101,7 @@ impl Timestamp { nanos: 0, }; + /// Creates a timestamp from epoch seconds and a nanosecond component. #[inline(always)] pub fn new(seconds: i64, nanos: u32) -> Result { if nanos >= NANOS_PER_SECOND as u32 { @@ -64,16 +113,24 @@ impl Timestamp { Ok(Self { seconds, nanos }) } + /// Creates a timestamp from whole seconds since Unix epoch. #[inline(always)] - pub const fn seconds(self) -> i64 { - self.seconds + pub const fn from_epoch_seconds(seconds: i64) -> Self { + Self { seconds, nanos: 0 } } + /// Creates a timestamp from milliseconds since Unix epoch. #[inline(always)] - pub const fn subsec_nanos(self) -> u32 { - self.nanos + pub fn from_epoch_millis(millis: i64) -> Self { + let seconds = millis.div_euclid(MILLIS_PER_SECOND); + let millis = millis.rem_euclid(MILLIS_PER_SECOND); + Self { + seconds, + nanos: (millis as u32) * 1_000_000, + } } + /// Creates a timestamp from microseconds since Unix epoch. #[inline(always)] pub fn from_epoch_micros(micros: i64) -> Self { let seconds = micros.div_euclid(MICROS_PER_SECOND); @@ -84,22 +141,59 @@ impl Timestamp { } } + /// Creates a timestamp from nanoseconds since Unix epoch. + #[inline(always)] + pub fn from_epoch_nanos(nanos: i128) -> Result { + let (seconds, nanos) = split_total_nanos(nanos, "timestamp")?; + Ok(Self { seconds, nanos }) + } + + /// Returns the whole seconds component. + #[inline(always)] + pub const fn seconds(self) -> i64 { + self.seconds + } + + /// Returns the normalized nanosecond component. + #[inline(always)] + pub const fn subsec_nanos(self) -> u32 { + self.nanos + } + + /// Returns milliseconds since Unix epoch, rounded down for sub-millisecond values. + #[inline(always)] + pub fn to_epoch_millis(self) -> Result { + total_unit(self.to_epoch_nanos(), NANOS_PER_MILLI, "timestamp") + } + + /// Returns microseconds since Unix epoch, rounded down for sub-microsecond values. #[inline(always)] pub fn to_epoch_micros(self) -> Result { - let seconds = self.seconds.checked_mul(MICROS_PER_SECOND).ok_or_else(|| { - Error::invalid_data(format!( - "timestamp seconds {} overflow microsecond conversion", - self.seconds - )) - })?; - seconds - .checked_add(i64::from(self.nanos / 1_000)) - .ok_or_else(|| { - Error::invalid_data(format!( - "timestamp {:?} overflow microsecond conversion", - self - )) - }) + total_unit(self.to_epoch_nanos(), NANOS_PER_MICRO, "timestamp") + } + + /// Returns nanoseconds since Unix epoch. + #[inline(always)] + pub fn to_epoch_nanos(self) -> i128 { + i128::from(self.seconds) * NANOS_PER_SECOND_I128 + i128::from(self.nanos) + } + + /// Adds a signed duration to this timestamp. + #[inline(always)] + pub fn checked_add_duration(self, duration: Duration) -> Result { + Self::from_epoch_nanos(self.to_epoch_nanos() + duration.to_nanos()) + } + + /// Subtracts a signed duration from this timestamp. + #[inline(always)] + pub fn checked_sub_duration(self, duration: Duration) -> Result { + Self::from_epoch_nanos(self.to_epoch_nanos() - duration.to_nanos()) + } + + /// Returns the signed duration from `earlier` to this timestamp. + #[inline(always)] + pub fn duration_since(self, earlier: Self) -> Result { + Duration::from_nanos(self.to_epoch_nanos() - earlier.to_epoch_nanos()) } } @@ -116,6 +210,7 @@ impl Duration { nanos: 0, }; + /// Creates a duration from seconds and a nanosecond adjustment. #[inline(always)] pub fn new(seconds: i64, nanos: i32) -> Result { if !(-(NANOS_PER_SECOND - 1)..=(NANOS_PER_SECOND - 1)).contains(&nanos) { @@ -141,6 +236,7 @@ impl Duration { }) } + /// Creates a duration from normalized seconds and nanoseconds. #[inline(always)] pub fn from_normalized(seconds: i64, nanos: u32) -> Result { if nanos >= NANOS_PER_SECOND as u32 { @@ -152,16 +248,48 @@ impl Duration { Ok(Self { seconds, nanos }) } + /// Creates a duration from seconds and an arbitrary nanosecond adjustment. #[inline(always)] - pub const fn seconds(self) -> i64 { - self.seconds + pub fn from_parts(seconds: i64, nanos: i64) -> Result { + Self::from_nanos(i128::from(seconds) * NANOS_PER_SECOND_I128 + i128::from(nanos)) } + /// Creates a duration from whole seconds. #[inline(always)] - pub const fn subsec_nanos(self) -> u32 { - self.nanos + pub const fn from_secs(seconds: i64) -> Self { + Self { seconds, nanos: 0 } } + /// Creates a duration from minutes. + #[inline(always)] + pub fn from_minutes(minutes: i64) -> Result { + checked_duration_seconds(minutes, SECONDS_PER_MINUTE, "minutes") + } + + /// Creates a duration from hours. + #[inline(always)] + pub fn from_hours(hours: i64) -> Result { + checked_duration_seconds(hours, SECONDS_PER_HOUR, "hours") + } + + /// Creates a duration from days. + #[inline(always)] + pub fn from_days(days: i64) -> Result { + checked_duration_seconds(days, SECONDS_PER_DAY, "days") + } + + /// Creates a duration from milliseconds. + #[inline(always)] + pub fn from_millis(millis: i64) -> Self { + let seconds = millis.div_euclid(MILLIS_PER_SECOND); + let millis = millis.rem_euclid(MILLIS_PER_SECOND); + Self { + seconds, + nanos: (millis as u32) * 1_000_000, + } + } + + /// Creates a duration from microseconds. #[inline(always)] pub fn from_micros(micros: i64) -> Self { let seconds = micros.div_euclid(MICROS_PER_SECOND); @@ -172,19 +300,345 @@ impl Duration { } } + /// Creates a duration from nanoseconds. + #[inline(always)] + pub fn from_nanos(nanos: i128) -> Result { + let (seconds, nanos) = split_total_nanos(nanos, "duration")?; + Ok(Self { seconds, nanos }) + } + + /// Returns the normalized seconds component. + #[inline(always)] + pub const fn seconds(self) -> i64 { + self.seconds + } + + /// Returns the normalized nanosecond component. + #[inline(always)] + pub const fn subsec_nanos(self) -> u32 { + self.nanos + } + + /// Returns whether this duration is zero. + #[inline(always)] + pub const fn is_zero(self) -> bool { + self.seconds == 0 && self.nanos == 0 + } + + /// Returns whether this duration is greater than zero. + #[inline(always)] + pub const fn is_positive(self) -> bool { + self.seconds > 0 || (self.seconds == 0 && self.nanos > 0) + } + + /// Returns whether this duration is less than zero. + #[inline(always)] + pub const fn is_negative(self) -> bool { + self.seconds < 0 + } + + /// Returns whole milliseconds, rounded down for sub-millisecond values. + #[inline(always)] + pub fn to_millis(self) -> Result { + total_unit(self.to_nanos(), NANOS_PER_MILLI, "duration") + } + + /// Returns whole microseconds, rounded down for sub-microsecond values. #[inline(always)] pub fn to_micros(self) -> Result { - let seconds = self.seconds.checked_mul(MICROS_PER_SECOND).ok_or_else(|| { + total_unit(self.to_nanos(), NANOS_PER_MICRO, "duration") + } + + /// Returns nanoseconds. + #[inline(always)] + pub fn to_nanos(self) -> i128 { + i128::from(self.seconds) * NANOS_PER_SECOND_I128 + i128::from(self.nanos) + } + + /// Adds two durations. + #[inline(always)] + pub fn checked_add(self, other: Self) -> Result { + Self::from_nanos(self.to_nanos() + other.to_nanos()) + } + + /// Subtracts `other` from this duration. + #[inline(always)] + pub fn checked_sub(self, other: Self) -> Result { + Self::from_nanos(self.to_nanos() - other.to_nanos()) + } + + /// Negates this duration. + #[inline(always)] + pub fn checked_neg(self) -> Result { + Self::from_nanos(-self.to_nanos()) + } + + /// Returns the absolute duration. + #[inline(always)] + pub fn abs(self) -> Result { + if self.is_negative() { + self.checked_neg() + } else { + Ok(self) + } + } +} + +#[inline(always)] +fn split_total_nanos(nanos: i128, value_name: &str) -> Result<(i64, u32), Error> { + let total_nanos = nanos; + let seconds = total_nanos.div_euclid(NANOS_PER_SECOND_I128); + let nanos = total_nanos.rem_euclid(NANOS_PER_SECOND_I128) as u32; + let seconds = i64::try_from(seconds).map_err(|_| { + Error::invalid_data(format!( + "{} nanoseconds {} out of supported seconds range", + value_name, total_nanos + )) + })?; + Ok((seconds, nanos)) +} + +#[inline(always)] +fn total_unit(nanos: i128, nanos_per_unit: i128, value_name: &str) -> Result { + let units = nanos.div_euclid(nanos_per_unit); + i64::try_from(units).map_err(|_| { + Error::invalid_data(format!( + "{} nanoseconds {} overflow conversion to requested unit", + value_name, nanos + )) + }) +} + +#[inline(always)] +fn checked_duration_seconds(value: i64, multiplier: i64, unit: &str) -> Result { + value + .checked_mul(multiplier) + .map(Duration::from_secs) + .ok_or_else(|| { Error::invalid_data(format!( - "duration seconds {} overflow microsecond conversion", - self.seconds + "duration {} {} overflow second conversion", + value, unit + )) + }) +} + +impl TryFrom for Duration { + type Error = Error; + + #[inline(always)] + fn try_from(value: std::time::Duration) -> Result { + let seconds = i64::try_from(value.as_secs()).map_err(|_| { + Error::invalid_data(format!( + "std::time::Duration seconds {} exceed Fory duration range", + value.as_secs() )) })?; - seconds - .checked_add(i64::from(self.nanos / 1_000)) - .ok_or_else(|| { - Error::invalid_data(format!("{:?} overflow microsecond conversion", self)) - }) + Self::from_normalized(seconds, value.subsec_nanos()) + } +} + +impl TryFrom for std::time::Duration { + type Error = Error; + + #[inline(always)] + fn try_from(value: Duration) -> Result { + if value.is_negative() { + return Err(Error::invalid_data(format!( + "negative Fory duration {:?} cannot convert to std::time::Duration", + value + ))); + } + Ok(std::time::Duration::new( + u64::try_from(value.seconds()).map_err(|_| { + Error::invalid_data(format!( + "duration seconds {} exceed std::time::Duration range", + value.seconds() + )) + })?, + value.subsec_nanos(), + )) + } +} + +impl TryFrom for Timestamp { + type Error = Error; + + #[inline(always)] + fn try_from(value: std::time::SystemTime) -> Result { + match value.duration_since(std::time::UNIX_EPOCH) { + Ok(duration) => { + let seconds = i64::try_from(duration.as_secs()).map_err(|_| { + Error::invalid_data(format!( + "SystemTime seconds {} exceed Fory timestamp range", + duration.as_secs() + )) + })?; + Self::new(seconds, duration.subsec_nanos()) + } + Err(error) => { + let duration = error.duration(); + let nanos = i128::from(duration.as_secs()) * NANOS_PER_SECOND_I128 + + i128::from(duration.subsec_nanos()); + Self::from_epoch_nanos(-nanos) + } + } + } +} + +impl TryFrom for std::time::SystemTime { + type Error = Error; + + #[inline(always)] + fn try_from(value: Timestamp) -> Result { + let nanos = value.to_epoch_nanos(); + if nanos >= 0 { + std::time::UNIX_EPOCH + .checked_add(std_duration_from_nanos(nanos)?) + .ok_or_else(|| { + Error::invalid_data(format!("timestamp {:?} exceeds SystemTime range", value)) + }) + } else { + std::time::UNIX_EPOCH + .checked_sub(std_duration_from_nanos(-nanos)?) + .ok_or_else(|| { + Error::invalid_data(format!("timestamp {:?} exceeds SystemTime range", value)) + }) + } + } +} + +#[inline(always)] +fn std_duration_from_nanos(nanos: i128) -> Result { + debug_assert!(nanos >= 0); + let total_nanos = nanos; + let seconds = total_nanos / NANOS_PER_SECOND_I128; + let nanos = (total_nanos % NANOS_PER_SECOND_I128) as u32; + let seconds = u64::try_from(seconds).map_err(|_| { + Error::invalid_data(format!( + "nanoseconds {} exceed std::time::Duration range", + total_nanos + )) + })?; + Ok(std::time::Duration::new(seconds, nanos)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn date_day_api() { + let date = Date::from_epoch_days(19_782); + assert_eq!(date.epoch_days(), 19_782); + assert_eq!( + date.checked_add_days(1).unwrap(), + Date::from_epoch_days(19_783) + ); + assert_eq!( + date.checked_sub_days(1).unwrap(), + Date::from_epoch_days(19_781) + ); + assert_eq!( + date.days_since(Date::UNIX_EPOCH).unwrap(), + date.epoch_days() + ); + assert!(Date::from_epoch_days(i64::MAX).checked_add_days(1).is_err()); + } + + #[test] + fn timestamp_epoch_api() { + let millis = Timestamp::from_epoch_millis(-1); + assert_eq!(millis.seconds(), -1); + assert_eq!(millis.subsec_nanos(), 999_000_000); + assert_eq!(millis.to_epoch_millis().unwrap(), -1); + + let nanos = Timestamp::from_epoch_nanos(-1).unwrap(); + assert_eq!(nanos.seconds(), -1); + assert_eq!(nanos.subsec_nanos(), 999_999_999); + assert_eq!(nanos.to_epoch_nanos(), -1); + assert_eq!(nanos.to_epoch_micros().unwrap(), -1); + + let duration = Duration::from_micros(1); + let timestamp = Timestamp::UNIX_EPOCH + .checked_add_duration(duration) + .unwrap(); + assert_eq!(timestamp.to_epoch_micros().unwrap(), 1); + assert_eq!( + timestamp.duration_since(Timestamp::UNIX_EPOCH).unwrap(), + duration + ); + assert_eq!( + Timestamp::UNIX_EPOCH + .checked_sub_duration(Duration::from_nanos(1).unwrap()) + .unwrap(), + nanos + ); + + assert_eq!(Timestamp::from_epoch_seconds(10).seconds(), 10); + } + + #[test] + fn duration_api() { + let nanos = Duration::from_nanos(-1).unwrap(); + assert_eq!(nanos.seconds(), -1); + assert_eq!(nanos.subsec_nanos(), 999_999_999); + assert!(nanos.is_negative()); + assert_eq!(nanos.to_nanos(), -1); + assert_eq!(nanos.to_micros().unwrap(), -1); + + let duration = Duration::from_parts(1, 1_500_000_000).unwrap(); + assert_eq!(duration, Duration::from_normalized(2, 500_000_000).unwrap()); + assert_eq!( + Duration::from_millis(-1), + Duration::from_normalized(-1, 999_000_000).unwrap() + ); + assert_eq!( + nanos.checked_neg().unwrap(), + Duration::from_normalized(0, 1).unwrap() + ); + assert_eq!( + Duration::from_secs(1) + .checked_add(Duration::from_millis(500)) + .unwrap(), + Duration::from_normalized(1, 500_000_000).unwrap() + ); + assert_eq!(Duration::from_minutes(2).unwrap(), Duration::from_secs(120)); + assert_eq!(Duration::from_hours(2).unwrap(), Duration::from_secs(7_200)); + assert_eq!( + Duration::from_days(2).unwrap(), + Duration::from_secs(172_800) + ); + assert_eq!(Duration::ZERO.abs().unwrap(), Duration::ZERO); + } + + #[test] + fn std_time_conversions() { + let std_duration = std::time::Duration::new(3, 4); + let duration = Duration::try_from(std_duration).unwrap(); + assert_eq!(duration, Duration::from_normalized(3, 4).unwrap()); + let roundtrip: std::time::Duration = duration.try_into().unwrap(); + assert_eq!(roundtrip, std_duration); + assert!(std::time::Duration::try_from(Duration::from_nanos(-1).unwrap()).is_err()); + + let before_epoch = Timestamp::from_epoch_nanos(-1).unwrap(); + let system_time: std::time::SystemTime = before_epoch.try_into().unwrap(); + assert_eq!(Timestamp::try_from(system_time).unwrap(), before_epoch); + + let after_epoch = Timestamp::from_epoch_nanos(1_500_000_001).unwrap(); + let system_time: std::time::SystemTime = after_epoch.try_into().unwrap(); + assert_eq!(Timestamp::try_from(system_time).unwrap(), after_epoch); + } + + #[cfg(feature = "chrono")] + #[test] + fn chrono_utc_conversion() { + use chrono::{DateTime, Utc}; + + let datetime = DateTime::::from_timestamp(1, 2).unwrap(); + let timestamp = Timestamp::from(datetime); + assert_eq!(timestamp, Timestamp::new(1, 2).unwrap()); + let roundtrip: DateTime = timestamp.try_into().unwrap(); + assert_eq!(roundtrip, datetime); } } @@ -192,7 +646,7 @@ impl Duration { mod chrono_support { use super::{Date, Duration, Timestamp}; use crate::error::Error; - use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeDelta}; + use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeDelta, Utc}; fn epoch() -> NaiveDate { NaiveDate::from_ymd_opt(1970, 1, 1).expect("1970-01-01 is a valid chrono date") @@ -236,6 +690,31 @@ mod chrono_support { } } + impl From> for Timestamp { + #[inline(always)] + fn from(value: DateTime) -> Self { + Self { + seconds: value.timestamp(), + nanos: value.timestamp_subsec_nanos(), + } + } + } + + impl TryFrom for DateTime { + type Error = Error; + + #[inline(always)] + fn try_from(value: Timestamp) -> Result { + DateTime::from_timestamp(value.seconds(), value.subsec_nanos()).ok_or_else(|| { + Error::invalid_data(format!( + "timestamp seconds {} nanoseconds {} is out of chrono::DateTime range", + value.seconds(), + value.subsec_nanos() + )) + }) + } + } + impl TryFrom for NaiveDateTime { type Error = Error; diff --git a/rust/fory/src/lib.rs b/rust/fory/src/lib.rs index 81bb59c34e..b8714853ce 100644 --- a/rust/fory/src/lib.rs +++ b/rust/fory/src/lib.rs @@ -981,9 +981,9 @@ //! //! ### Date and Time //! -//! - `Date` - Date without timezone -//! - `Timestamp` - Point in time -//! - `Duration` - Signed duration +//! - `Date` - Date without timezone, with epoch-day accessors and checked day arithmetic +//! - `Timestamp` - Point in time, with epoch unit conversions and checked duration arithmetic +//! - `Duration` - Signed duration, with normalized parts, total unit conversions, and checked arithmetic //! - `chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration` when the `chrono` feature is enabled //! //! ### Custom Types From 8d6bb2bbaf11dad07983bcfb925f909b3a5116b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Fri, 15 May 2026 22:34:00 +0800 Subject: [PATCH 5/5] feat(rust): add idl chrono temporal option --- compiler/fory_compiler/frontend/fdl/parser.py | 1 + compiler/fory_compiler/generators/rust.py | 28 +++++++- .../tests/test_generated_code.py | 25 +++++++ docs/compiler/compiler-guide.md | 9 ++- docs/compiler/schema-idl.md | 70 ++++++++++++++----- integration_tests/idl_tests/idl/example.fdl | 1 + .../idl_tests/idl/optional_types.fdl | 2 + integration_tests/idl_tests/rust/Cargo.lock | 4 -- integration_tests/idl_tests/rust/Cargo.toml | 2 +- 9 files changed, 115 insertions(+), 27 deletions(-) diff --git a/compiler/fory_compiler/frontend/fdl/parser.py b/compiler/fory_compiler/frontend/fdl/parser.py index 2a8acf9ebb..9ded037fe1 100644 --- a/compiler/fory_compiler/frontend/fdl/parser.py +++ b/compiler/fory_compiler/frontend/fdl/parser.py @@ -54,6 +54,7 @@ "enable_auto_type_id", "go_nested_type_style", "swift_namespace_style", + "rust_use_chrono_temporal_types", "evolving", } diff --git a/compiler/fory_compiler/generators/rust.py b/compiler/fory_compiler/generators/rust.py index 988b2f94b9..42fe2aae78 100644 --- a/compiler/fory_compiler/generators/rust.py +++ b/compiler/fory_compiler/generators/rust.py @@ -61,13 +61,35 @@ class RustGenerator(BaseGenerator): PrimitiveKind.FLOAT64: "f64", PrimitiveKind.STRING: "::std::string::String", PrimitiveKind.BYTES: "::std::vec::Vec", + PrimitiveKind.DECIMAL: "::fory::Decimal", + PrimitiveKind.ANY: "::std::boxed::Box", + } + + FORY_TEMPORAL_MAP = { PrimitiveKind.DATE: "::fory::Date", PrimitiveKind.TIMESTAMP: "::fory::Timestamp", PrimitiveKind.DURATION: "::fory::Duration", - PrimitiveKind.DECIMAL: "::fory::Decimal", - PrimitiveKind.ANY: "::std::boxed::Box", } + CHRONO_TEMPORAL_MAP = { + PrimitiveKind.DATE: "::chrono::NaiveDate", + PrimitiveKind.TIMESTAMP: "::chrono::NaiveDateTime", + PrimitiveKind.DURATION: "::chrono::Duration", + } + + def use_chrono_temporal_types(self) -> bool: + return self.schema.get_option("rust_use_chrono_temporal_types") is True + + def primitive_type_name(self, kind: PrimitiveKind) -> str: + if kind in self.FORY_TEMPORAL_MAP: + temporal_map = ( + self.CHRONO_TEMPORAL_MAP + if self.use_chrono_temporal_types() + else self.FORY_TEMPORAL_MAP + ) + return temporal_map[kind] + return self.PRIMITIVE_MAP[kind] + def generate(self) -> List[GeneratedFile]: """Generate Rust files for the schema.""" files = [] @@ -723,7 +745,7 @@ def generate_type( if isinstance(field_type, PrimitiveType): if field_type.kind == PrimitiveKind.ANY: return "::std::boxed::Box" - base_type = self.PRIMITIVE_MAP[field_type.kind] + base_type = self.primitive_type_name(field_type.kind) if nullable: return f"::std::option::Option<{base_type}>" return base_type diff --git a/compiler/fory_compiler/tests/test_generated_code.py b/compiler/fory_compiler/tests/test_generated_code.py index 551e6dc26d..7546124544 100644 --- a/compiler/fory_compiler/tests/test_generated_code.py +++ b/compiler/fory_compiler/tests/test_generated_code.py @@ -176,6 +176,31 @@ def test_rust_generated_code_uses_fory_temporal_carriers(): assert "chrono::" not in rust_output +def test_rust_generated_code_can_use_chrono_temporal_types(): + schema = parse_fdl( + dedent( + """ + package gen; + option rust_use_chrono_temporal_types = true; + + message TemporalTypes { + date day = 1; + timestamp instant = 2; + duration elapsed = 3; + } + """ + ) + ) + + rust_output = render_files(generate_files(schema, RustGenerator)) + assert "pub day: ::chrono::NaiveDate," in rust_output + assert "pub instant: ::chrono::NaiveDateTime," in rust_output + assert "pub elapsed: ::chrono::Duration," in rust_output + assert "::fory::Date" not in rust_output + assert "::fory::Timestamp" not in rust_output + assert "::fory::Duration" not in rust_output + + def test_generated_code_integer_encoding_variants_equivalent(): fdl = dedent( """ diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md index a1c35e0268..8cefb9abf5 100644 --- a/docs/compiler/compiler-guide.md +++ b/docs/compiler/compiler-guide.md @@ -73,7 +73,14 @@ Compile options: | `--emit-fdl` | Emit translated FDL (for non-FDL inputs) | `false` | | `--emit-fdl-path` | Write translated FDL to this path (file or directory) | (stdout) | -For both `go_nested_type_style` and `swift_namespace_style`, schema-level file options are supported (`option ... = ...;`) and the CLI flag overrides the schema option when both are present. +Schema-level file options are supported for language-specific generation choices. +For `go_nested_type_style` and `swift_namespace_style`, the CLI flag overrides +the schema option when both are present. Rust temporal codegen has no CLI flag: +set `option rust_use_chrono_temporal_types = true;` in the schema to generate +`chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration` instead of +the default `fory::Date`, `fory::Timestamp`, and `fory::Duration`. Crates that +compile generated chrono-based Rust code must depend on `chrono` and enable +Fory's `chrono` feature. Scan options (with `--scan-generated`): diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md index 5039c80efe..853ecb605e 100644 --- a/docs/compiler/schema-idl.md +++ b/docs/compiler/schema-idl.md @@ -217,6 +217,29 @@ message Payment { The CLI flag `--swift_namespace_style` overrides this schema option when both are set. +### Rust Chrono Temporal Types Option + +Rust generated code uses Fory's lightweight temporal carrier types by default: +`fory::Date`, `fory::Timestamp`, and `fory::Duration`. Set +`rust_use_chrono_temporal_types` when the generated Rust API should expose +chrono temporal types instead: + +```protobuf +package payment; +option rust_use_chrono_temporal_types = true; + +message Event { + date business_day = 1; + timestamp created_at = 2; + duration timeout = 3; +} +``` + +With this option, Rust code maps `date` to `chrono::NaiveDate`, `timestamp` to +`chrono::NaiveDateTime`, and `duration` to `chrono::Duration`. The Rust crate +that compiles the generated code must depend on `chrono` and enable Fory's +`chrono` feature. + ### Java Outer Classname Option Generate all types as inner classes of a single outer wrapper class: @@ -1150,27 +1173,38 @@ Underscore spellings for integer encoding are not FDL type names. ##### Date -| Language | Type | Notes | -| ---------- | --------------------------- | --------------------------------------------------------------- | -| Java | `java.time.LocalDate` | | -| Python | `datetime.date` | | -| Go | `time.Time` | Time portion ignored | -| Rust | `fory::Date` | `chrono::NaiveDate` is supported with the Rust `chrono` feature | -| C++ | `fory::serialization::Date` | | -| JavaScript | `Date` | | -| Dart | `LocalDate` | Fory package type | +| Language | Type | Notes | +| ---------- | --------------------------- | --------------------------------------------------------------------------- | +| Java | `java.time.LocalDate` | | +| Python | `datetime.date` | | +| Go | `time.Time` | Time portion ignored | +| Rust | `fory::Date` | Set `rust_use_chrono_temporal_types = true` to generate `chrono::NaiveDate` | +| C++ | `fory::serialization::Date` | | +| JavaScript | `Date` | | +| Dart | `LocalDate` | Fory package type | ##### Timestamp -| Language | Type | Notes | -| ---------- | -------------------------------- | ------------------------------------------------------------------- | -| Java | `java.time.Instant` | UTC-based | -| Python | `datetime.datetime` | | -| Go | `time.Time` | | -| Rust | `fory::Timestamp` | `chrono::NaiveDateTime` is supported with the Rust `chrono` feature | -| C++ | `fory::serialization::Timestamp` | | -| JavaScript | `Date` | | -| Dart | `Timestamp` | Fory package type | +| Language | Type | Notes | +| ---------- | -------------------------------- | ------------------------------------------------------------------------------- | +| Java | `java.time.Instant` | UTC-based | +| Python | `datetime.datetime` | | +| Go | `time.Time` | | +| Rust | `fory::Timestamp` | Set `rust_use_chrono_temporal_types = true` to generate `chrono::NaiveDateTime` | +| C++ | `fory::serialization::Timestamp` | | +| JavaScript | `Date` | | +| Dart | `Timestamp` | Fory package type | + +##### Duration + +| Language | Type | Notes | +| -------- | ------------------------------- | -------------------------------------------------------------------------- | +| Java | `java.time.Duration` | | +| Python | `datetime.timedelta` | | +| Go | `time.Duration` | | +| Rust | `fory::Duration` | Set `rust_use_chrono_temporal_types = true` to generate `chrono::Duration` | +| C++ | `fory::serialization::Duration` | | +| Dart | `Duration` | | #### Any diff --git a/integration_tests/idl_tests/idl/example.fdl b/integration_tests/idl_tests/idl/example.fdl index 4ade5cd856..bf635448bf 100644 --- a/integration_tests/idl_tests/idl/example.fdl +++ b/integration_tests/idl_tests/idl/example.fdl @@ -20,6 +20,7 @@ package example; option go_package = "github.com/apache/fory/integration_tests/idl_tests/go/example/generated;example"; option evolving = false; +option rust_use_chrono_temporal_types = true; enum ExampleState [id=1504] { UNKNOWN = 0; diff --git a/integration_tests/idl_tests/idl/optional_types.fdl b/integration_tests/idl_tests/idl/optional_types.fdl index fba9a28b30..fb83bfd63f 100644 --- a/integration_tests/idl_tests/idl/optional_types.fdl +++ b/integration_tests/idl_tests/idl/optional_types.fdl @@ -17,6 +17,8 @@ package optional_types; +option rust_use_chrono_temporal_types = true; + message AllOptionalTypes [id=120] { optional bool bool_value = 1; optional int8 int8_value = 2; diff --git a/integration_tests/idl_tests/rust/Cargo.lock b/integration_tests/idl_tests/rust/Cargo.lock index 72f12773ba..0eb6a8bd74 100644 --- a/integration_tests/idl_tests/rust/Cargo.lock +++ b/integration_tests/idl_tests/rust/Cargo.lock @@ -93,9 +93,6 @@ dependencies = [ "num-bigint", "num_enum", "paste", - "proc-macro2", - "quote", - "syn 2.0.114", "thiserror", ] @@ -107,7 +104,6 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.114", - "thiserror", ] [[package]] diff --git a/integration_tests/idl_tests/rust/Cargo.toml b/integration_tests/idl_tests/rust/Cargo.toml index 66f3fbd282..d15105f2b7 100644 --- a/integration_tests/idl_tests/rust/Cargo.toml +++ b/integration_tests/idl_tests/rust/Cargo.toml @@ -23,5 +23,5 @@ license = "Apache-2.0" [dependencies] chrono = "0.4" -fory = { path = "../../../rust/fory" } +fory = { path = "../../../rust/fory", features = ["chrono"] } fory-core = { path = "../../../rust/fory-core" }