|
| 1 | +//! Duration conversion utilities for ISO 8601 and chrono compatibility. |
| 2 | +//! |
| 3 | +//! This module provides functions to convert between [`iso8601::Duration`] and [`chrono::Duration`], |
| 4 | +//! along with serde support for serializing/deserializing durations in ISO 8601 format. |
| 5 | +//! |
| 6 | +//! # Examples |
| 7 | +//! |
| 8 | +//! ``` |
| 9 | +//! use std::str::FromStr; |
| 10 | +//! use lakekeeper::utils::time_conversion::iso_8601_duration_to_chrono; |
| 11 | +//! |
| 12 | +//! let iso_duration = iso8601::Duration::from_str("P3DT4H5M").unwrap(); |
| 13 | +//! let chrono_duration = iso_8601_duration_to_chrono(&iso_duration).unwrap(); |
| 14 | +//! assert_eq!(chrono_duration.num_days(), 3); |
| 15 | +//! ``` |
| 16 | +//! |
| 17 | +//! # Limitations |
| 18 | +//! |
| 19 | +//! - Years and months are not supported (will return an error) |
| 20 | +//! - Negative durations are not supported |
| 21 | +//! - The serde modules assume ISO 8601 duration string format |
| 22 | +
|
1 | 23 | use iceberg_ext::catalog::rest::ErrorModel; |
2 | 24 |
|
3 | | -pub(crate) fn iso_8601_duration_to_chrono( |
| 25 | +/// Serde visitors for deserializing ISO 8601 duration strings. |
| 26 | +/// |
| 27 | +/// This module provides [`Visitor`](serde::de::Visitor) implementations that convert |
| 28 | +/// ISO 8601 duration strings into both [`iso8601::Duration`] and [`chrono::Duration`] types. |
| 29 | +pub mod duration_serde_visitor; |
| 30 | + |
| 31 | +/// Serialization support for `chrono::Duration` as ISO 8601 duration strings. |
| 32 | +/// |
| 33 | +/// Use this module in your struct definitions with the `#[serde(with = "...")]` attribute |
| 34 | +/// to automatically serialize/deserialize `Duration` fields as ISO 8601 strings. |
| 35 | +/// |
| 36 | +/// # Examples |
| 37 | +/// |
| 38 | +/// ```ignore |
| 39 | +/// use chrono::Duration; |
| 40 | +/// use serde::{Deserialize, Serialize}; |
| 41 | +/// use lakekeeper::utils::time_conversion::iso8601_duration_serde; |
| 42 | +/// |
| 43 | +/// #[derive(Serialize, Deserialize)] |
| 44 | +/// struct Task { |
| 45 | +/// #[serde(with = "iso8601_duration_serde")] |
| 46 | +/// timeout: Duration, |
| 47 | +/// } |
| 48 | +/// ``` |
| 49 | +pub mod iso8601_duration_serde; |
| 50 | + |
| 51 | +/// Serialization support for `Option<chrono::Duration>` as ISO 8601 duration strings. |
| 52 | +/// |
| 53 | +/// Similar to [`iso8601_duration_serde`], but handles `Option<Duration>` fields. |
| 54 | +/// `None` values are serialized as `null`. |
| 55 | +/// |
| 56 | +/// # Examples |
| 57 | +/// |
| 58 | +/// ```ignore |
| 59 | +/// use chrono::Duration; |
| 60 | +/// use serde::{Deserialize, Serialize}; |
| 61 | +/// use lakekeeper::utils::time_conversion::iso8601_option_duration_serde; |
| 62 | +/// |
| 63 | +/// #[derive(Serialize, Deserialize)] |
| 64 | +/// struct Config { |
| 65 | +/// #[serde(with = "iso8601_option_duration_serde")] |
| 66 | +/// optional_timeout: Option<Duration>, |
| 67 | +/// } |
| 68 | +/// ``` |
| 69 | +pub mod iso8601_option_duration_serde; |
| 70 | + |
| 71 | +/// Converts an ISO 8601 duration to a `chrono::Duration`. |
| 72 | +/// |
| 73 | +/// # Arguments |
| 74 | +/// |
| 75 | +/// * `duration` - An ISO 8601 duration in either weeks (`P<n>W`) or date-time format (`P<d>DT<h>H<m>M<s>S`) |
| 76 | +/// |
| 77 | +/// # Returns |
| 78 | +/// |
| 79 | +/// * `Ok(chrono::Duration)` - Successfully converted duration |
| 80 | +/// * `Err(ErrorModel)` - If the duration contains years or months (not supported) |
| 81 | +/// |
| 82 | +/// # Examples |
| 83 | +/// |
| 84 | +/// ``` |
| 85 | +/// use std::str::FromStr; |
| 86 | +/// use lakekeeper::utils::time_conversion::iso_8601_duration_to_chrono; |
| 87 | +/// |
| 88 | +/// // Parse ISO 8601 duration string |
| 89 | +/// let iso_duration = iso8601::Duration::from_str("P3DT4H5M6S").unwrap(); |
| 90 | +/// let chrono_duration = iso_8601_duration_to_chrono(&iso_duration).unwrap(); |
| 91 | +/// assert_eq!(chrono_duration.num_days(), 3); |
| 92 | +/// assert_eq!(chrono_duration.num_hours() % 24, 4); |
| 93 | +/// ``` |
| 94 | +pub fn iso_8601_duration_to_chrono( |
4 | 95 | duration: &iso8601::Duration, |
5 | 96 | ) -> Result<chrono::Duration, ErrorModel> { |
6 | 97 | match duration { |
@@ -30,10 +121,39 @@ pub(crate) fn iso_8601_duration_to_chrono( |
30 | 121 | } |
31 | 122 | } |
32 | 123 |
|
33 | | -pub(crate) fn chrono_to_iso_8601_duration( |
| 124 | +/// Converts a `chrono::Duration` to an ISO 8601 duration. |
| 125 | +/// |
| 126 | +/// The conversion prefers the weeks representation (`P<n>W`) if the duration is divisible by 7 days, |
| 127 | +/// otherwise uses the YMDHMS format (`P<d>DT<h>H<m>M<s>S`). |
| 128 | +/// |
| 129 | +/// # Arguments |
| 130 | +/// |
| 131 | +/// * `duration` - A chrono duration (must be non-negative) |
| 132 | +/// |
| 133 | +/// # Returns |
| 134 | +/// |
| 135 | +/// * `Ok(iso8601::Duration)` - Successfully converted duration |
| 136 | +/// * `Err(ErrorModel)` - If the duration is negative or conversion would overflow |
| 137 | +/// |
| 138 | +/// # Examples |
| 139 | +/// |
| 140 | +/// ``` |
| 141 | +/// use chrono::Duration; |
| 142 | +/// use lakekeeper::utils::time_conversion::chrono_to_iso_8601_duration; |
| 143 | +/// |
| 144 | +/// // Convert duration with multiple components |
| 145 | +/// let duration = Duration::days(3) + Duration::hours(4) + Duration::minutes(5); |
| 146 | +/// let iso_duration = chrono_to_iso_8601_duration(&duration).unwrap(); |
| 147 | +/// assert_eq!(iso_duration.to_string(), "P3DT4H5M"); |
| 148 | +/// |
| 149 | +/// // Convert week-divisible duration (uses weeks representation) |
| 150 | +/// let weeks = Duration::weeks(2); |
| 151 | +/// let iso_duration = chrono_to_iso_8601_duration(&weeks).unwrap(); |
| 152 | +/// assert_eq!(iso_duration.to_string(), "P2W"); |
| 153 | +/// ``` |
| 154 | +pub fn chrono_to_iso_8601_duration( |
34 | 155 | duration: &chrono::Duration, |
35 | 156 | ) -> Result<iso8601::Duration, crate::api::ErrorModel> { |
36 | | - // Check for negative duration |
37 | 157 | if duration.num_milliseconds() < 0 { |
38 | 158 | return Err(crate::api::ErrorModel::bad_request( |
39 | 159 | "Negative durations not supported for ISO8601 format".to_string(), |
@@ -111,79 +231,6 @@ pub(crate) fn chrono_to_iso_8601_duration( |
111 | 231 | }) |
112 | 232 | } |
113 | 233 |
|
114 | | -/// Module for serializing `chrono::Duration` as ISO8601 duration strings |
115 | | -pub(crate) mod iso8601_duration_serde { |
116 | | - use std::str::FromStr; |
117 | | - |
118 | | - use chrono::Duration; |
119 | | - use serde::{Deserialize, Deserializer, Serializer}; |
120 | | - |
121 | | - use super::{chrono_to_iso_8601_duration, iso_8601_duration_to_chrono}; |
122 | | - |
123 | | - pub(crate) fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error> |
124 | | - where |
125 | | - S: Serializer, |
126 | | - { |
127 | | - // Convert chrono::Duration to iso8601::Duration |
128 | | - let iso_duration = |
129 | | - chrono_to_iso_8601_duration(duration).map_err(serde::ser::Error::custom)?; |
130 | | - |
131 | | - // Serialize to string |
132 | | - serializer.serialize_str(&iso_duration.to_string()) |
133 | | - } |
134 | | - |
135 | | - pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error> |
136 | | - where |
137 | | - D: Deserializer<'de>, |
138 | | - { |
139 | | - let duration_str = String::deserialize(deserializer)?; |
140 | | - |
141 | | - // Parse string into iso8601::Duration |
142 | | - let iso_duration = iso8601::Duration::from_str(&duration_str) |
143 | | - .map_err(|e| serde::de::Error::custom(format!("Invalid ISO8601 duration: {e}")))?; |
144 | | - |
145 | | - // Convert to chrono::Duration |
146 | | - iso_8601_duration_to_chrono(&iso_duration).map_err(|e| serde::de::Error::custom(e.message)) |
147 | | - } |
148 | | -} |
149 | | - |
150 | | -pub(crate) mod iso8601_option_duration_serde { |
151 | | - use chrono::Duration; |
152 | | - use serde::{Deserialize, Deserializer, Serializer}; |
153 | | - |
154 | | - use super::iso8601_duration_serde; |
155 | | - |
156 | | - #[allow(clippy::ref_option)] |
157 | | - pub(crate) fn serialize<S>( |
158 | | - duration: &Option<Duration>, |
159 | | - serializer: S, |
160 | | - ) -> Result<S::Ok, S::Error> |
161 | | - where |
162 | | - S: Serializer, |
163 | | - { |
164 | | - match duration { |
165 | | - Some(d) => iso8601_duration_serde::serialize(d, serializer), |
166 | | - None => serializer.serialize_none(), |
167 | | - } |
168 | | - } |
169 | | - |
170 | | - pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error> |
171 | | - where |
172 | | - D: Deserializer<'de>, |
173 | | - { |
174 | | - let opt: Option<String> = Option::deserialize(deserializer)?; |
175 | | - match opt { |
176 | | - Some(duration_str) => { |
177 | | - let duration = iso8601_duration_serde::deserialize( |
178 | | - serde::de::value::StrDeserializer::new(&duration_str), |
179 | | - )?; |
180 | | - Ok(Some(duration)) |
181 | | - } |
182 | | - None => Ok(None), |
183 | | - } |
184 | | - } |
185 | | -} |
186 | | - |
187 | 234 | #[cfg(test)] |
188 | 235 | mod test { |
189 | 236 | use std::str::FromStr; |
|
0 commit comments