1use crate::models::expression::is_strict_expr;
2use serde::{de, Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
7pub struct Duration {
8 #[serde(skip_serializing_if = "Option::is_none")]
10 pub days: Option<u64>,
11
12 #[serde(skip_serializing_if = "Option::is_none")]
14 pub hours: Option<u64>,
15
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub minutes: Option<u64>,
19
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub seconds: Option<u64>,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub milliseconds: Option<u64>,
27}
28
29impl<'de> de::Deserialize<'de> for Duration {
30 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
31 where
32 D: serde::Deserializer<'de>,
33 {
34 struct DurationVisitor;
35
36 impl<'de> de::Visitor<'de> for DurationVisitor {
37 type Value = Duration;
38
39 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
40 formatter.write_str("a duration object with at least one property")
41 }
42
43 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
44 where
45 A: de::MapAccess<'de>,
46 {
47 let mut days: Option<u64> = None;
48 let mut hours: Option<u64> = None;
49 let mut minutes: Option<u64> = None;
50 let mut seconds: Option<u64> = None;
51 let mut milliseconds: Option<u64> = None;
52 let mut has_key = false;
53
54 while let Some(key) = map.next_key::<String>()? {
55 has_key = true;
56 match key.as_str() {
57 "days" => {
58 days = Some(map.next_value()?);
59 }
60 "hours" => {
61 hours = Some(map.next_value()?);
62 }
63 "minutes" => {
64 minutes = Some(map.next_value()?);
65 }
66 "seconds" => {
67 seconds = Some(map.next_value()?);
68 }
69 "milliseconds" => {
70 milliseconds = Some(map.next_value()?);
71 }
72 other => {
73 return Err(de::Error::custom(format!(
74 "unexpected key '{}' in duration object",
75 other
76 )));
77 }
78 }
79 }
80
81 if !has_key {
82 return Err(de::Error::custom(
83 "duration object must include at least one property",
84 ));
85 }
86
87 Ok(Duration {
88 days,
89 hours,
90 minutes,
91 seconds,
92 milliseconds,
93 })
94 }
95 }
96
97 deserializer.deserialize_map(DurationVisitor)
98 }
99}
100macro_rules! from_unit {
101 ($name:ident, $field:ident) => {
102 pub fn $name(v: u64) -> Self {
103 Self {
104 $field: Some(v),
105 ..Self::default()
106 }
107 }
108 };
109}
110
111macro_rules! total_as {
112 ($name:ident, $divisor:expr) => {
113 pub fn $name(&self) -> f64 {
114 self.total_milliseconds() as f64 / $divisor
115 }
116 };
117}
118
119impl Duration {
120 from_unit!(from_days, days);
121 from_unit!(from_hours, hours);
122 from_unit!(from_minutes, minutes);
123 from_unit!(from_seconds, seconds);
124 from_unit!(from_milliseconds, milliseconds);
125
126 total_as!(total_days, 24.0 * 60.0 * 60.0 * 1000.0);
127 total_as!(total_hours, 60.0 * 60.0 * 1000.0);
128 total_as!(total_minutes, 60.0 * 1000.0);
129 total_as!(total_seconds, 1000.0);
130
131 pub fn total_milliseconds(&self) -> u64 {
133 let total: u128 = (self.days.unwrap_or(0) as u128) * 86_400_000
134 + (self.hours.unwrap_or(0) as u128) * 3_600_000
135 + (self.minutes.unwrap_or(0) as u128) * 60_000
136 + (self.seconds.unwrap_or(0) as u128) * 1_000
137 + self.milliseconds.unwrap_or(0) as u128;
138 total.try_into().unwrap_or(u64::MAX)
139 }
140}
141impl fmt::Display for Duration {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 let mut parts = Vec::new();
144 if let Some(days) = self.days {
145 parts.push(format!("{} days", days));
146 }
147 if let Some(hours) = self.hours {
148 parts.push(format!("{} hours", hours));
149 }
150 if let Some(minutes) = self.minutes {
151 parts.push(format!("{} minutes", minutes));
152 }
153 if let Some(seconds) = self.seconds {
154 parts.push(format!("{} seconds", seconds));
155 }
156 if let Some(milliseconds) = self.milliseconds {
157 parts.push(format!("{} milliseconds", milliseconds));
158 }
159 write!(f, "{}", parts.join(" "))
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
165pub enum OneOfDurationOrIso8601Expression {
166 Duration(Duration),
168 Iso8601Expression(String),
170}
171
172impl<'de> de::Deserialize<'de> for OneOfDurationOrIso8601Expression {
173 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174 where
175 D: serde::Deserializer<'de>,
176 {
177 struct OneOfDurationVisitor;
179
180 impl<'de> de::Visitor<'de> for OneOfDurationVisitor {
181 type Value = OneOfDurationOrIso8601Expression;
182
183 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
184 formatter.write_str("a duration object or an ISO 8601 duration string")
185 }
186
187 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
188 where
189 A: de::MapAccess<'de>,
190 {
191 let duration = Duration::deserialize(de::value::MapAccessDeserializer::new(map))?;
193 Ok(OneOfDurationOrIso8601Expression::Duration(duration))
194 }
195
196 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
197 where
198 E: de::Error,
199 {
200 if is_strict_expr(v) {
202 return Ok(OneOfDurationOrIso8601Expression::Iso8601Expression(
203 v.to_string(),
204 ));
205 }
206 if !is_iso8601_duration_valid(v) {
208 return Err(de::Error::custom(format!(
209 "invalid ISO 8601 duration expression: '{}'",
210 v
211 )));
212 }
213 Ok(OneOfDurationOrIso8601Expression::Iso8601Expression(
214 v.to_string(),
215 ))
216 }
217 }
218
219 deserializer.deserialize_any(OneOfDurationVisitor)
220 }
221}
222
223impl serde::Serialize for OneOfDurationOrIso8601Expression {
224 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
225 where
226 S: serde::Serializer,
227 {
228 match self {
229 OneOfDurationOrIso8601Expression::Duration(d) => d.serialize(serializer),
230 OneOfDurationOrIso8601Expression::Iso8601Expression(s) => serializer.serialize_str(s),
231 }
232 }
233}
234
235impl Default for OneOfDurationOrIso8601Expression {
236 fn default() -> Self {
237 OneOfDurationOrIso8601Expression::Duration(Duration::default())
239 }
240}
241impl fmt::Display for OneOfDurationOrIso8601Expression {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 match self {
244 OneOfDurationOrIso8601Expression::Duration(duration) => write!(f, "{}", duration),
245 OneOfDurationOrIso8601Expression::Iso8601Expression(expr) => write!(f, "{}", expr),
246 }
247 }
248}
249
250impl From<Duration> for OneOfDurationOrIso8601Expression {
251 fn from(duration: Duration) -> Self {
252 OneOfDurationOrIso8601Expression::Duration(duration)
253 }
254}
255
256impl OneOfDurationOrIso8601Expression {
257 pub fn total_milliseconds(&self) -> u64 {
260 match self {
261 OneOfDurationOrIso8601Expression::Duration(d) => d.total_milliseconds(),
262 OneOfDurationOrIso8601Expression::Iso8601Expression(_) => 0,
263 }
264 }
265
266 pub fn is_duration(&self) -> bool {
268 matches!(self, OneOfDurationOrIso8601Expression::Duration(_))
269 }
270
271 pub fn is_iso8601(&self) -> bool {
273 matches!(self, OneOfDurationOrIso8601Expression::Iso8601Expression(_))
274 }
275
276 pub fn as_iso8601(&self) -> Option<&str> {
278 match self {
279 OneOfDurationOrIso8601Expression::Iso8601Expression(s) => Some(s),
280 _ => None,
281 }
282 }
283}
284
285pub fn is_iso8601_duration_valid(s: &str) -> bool {
290 if !s.starts_with('P') {
291 return false;
292 }
293 let rest = &s[1..];
294 if rest.is_empty() {
295 return false; }
297
298 let (date_part, time_part) = if let Some(t_idx) = rest.find('T') {
299 let date = &rest[..t_idx];
300 let time = &rest[t_idx + 1..];
301 if time.is_empty() {
302 return false; }
304 (date, Some(time))
305 } else {
306 (rest, None)
307 };
308
309 if !date_part.is_empty() {
311 let days_str = date_part.trim_end_matches('D');
312 if days_str.is_empty() && date_part.ends_with('D') {
313 return false; }
315 if !days_str.is_empty() {
316 if days_str.parse::<u64>().is_err() {
318 return false;
319 }
320 }
321 if date_part.contains('Y') || date_part.contains('W') || date_part.contains('M') {
323 return false;
324 }
325 }
326
327 if let Some(time) = time_part {
329 let remaining = parse_time_components(time);
330 if remaining.is_none() {
331 return false;
332 }
333 if let Some(remaining) = remaining {
335 if !remaining.is_empty() {
336 return false;
337 }
338 }
339 }
340
341 true
342}
343
344fn parse_time_components(mut s: &str) -> Option<&str> {
347 while !s.is_empty() {
350 let (num_str, rest) = split_number_prefix(s)?;
352 if num_str.is_empty() {
353 return None; }
355 if num_str.parse::<f64>().is_err() {
357 return None;
358 }
359 if let Some(rest_after_ms) = rest.strip_prefix("MS") {
361 s = rest_after_ms;
362 } else if let Some(rest_after_h) = rest.strip_prefix('H') {
363 s = rest_after_h;
364 } else if let Some(rest_after_m) = rest.strip_prefix('M') {
365 s = rest_after_m;
366 } else if let Some(rest_after_s) = rest.strip_prefix('S') {
367 s = rest_after_s;
368 } else {
369 return Some(s);
371 }
372 }
373 Some(s) }
375
376fn split_number_prefix(s: &str) -> Option<(&str, &str)> {
378 let mut i = 0;
379 let bytes = s.as_bytes();
380 if i < bytes.len() && bytes[i] == b'-' {
382 i += 1;
383 }
384 while i < bytes.len() && bytes[i].is_ascii_digit() {
386 i += 1;
387 }
388 if i < bytes.len() && bytes[i] == b'.' {
390 i += 1;
391 while i < bytes.len() && bytes[i].is_ascii_digit() {
392 i += 1;
393 }
394 }
395 if i == 0 {
396 return None;
397 }
398 Some((&s[..i], &s[i..]))
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_duration_from_days() {
407 let d = Duration::from_days(1);
408 assert_eq!(d.days, Some(1));
409 assert_eq!(d.total_milliseconds(), 86400000);
410 }
411
412 #[test]
413 fn test_duration_from_hours() {
414 let d = Duration::from_hours(2);
415 assert_eq!(d.hours, Some(2));
416 assert_eq!(d.total_milliseconds(), 7200000);
417 }
418
419 #[test]
420 fn test_duration_from_minutes() {
421 let d = Duration::from_minutes(5);
422 assert_eq!(d.minutes, Some(5));
423 assert_eq!(d.total_milliseconds(), 300000);
424 }
425
426 #[test]
427 fn test_duration_from_seconds() {
428 let d = Duration::from_seconds(30);
429 assert_eq!(d.seconds, Some(30));
430 assert_eq!(d.total_milliseconds(), 30000);
431 }
432
433 #[test]
434 fn test_duration_from_milliseconds() {
435 let d = Duration::from_milliseconds(500);
436 assert_eq!(d.milliseconds, Some(500));
437 assert_eq!(d.total_milliseconds(), 500);
438 }
439
440 #[test]
441 fn test_duration_composite() {
442 let d = Duration {
443 days: Some(1),
444 hours: Some(2),
445 minutes: Some(30),
446 seconds: Some(45),
447 milliseconds: Some(500),
448 };
449 let expected = 86400000 + 7200000 + 1800000 + 45000 + 500;
450 assert_eq!(d.total_milliseconds(), expected);
451 }
452
453 #[test]
454 fn test_duration_total_conversions() {
455 let d = Duration::from_minutes(90);
456 assert_eq!(d.total_hours(), 1.5);
457 assert_eq!(d.total_minutes(), 90.0);
458 assert_eq!(d.total_seconds(), 5400.0);
459 }
460
461 #[test]
462 fn test_duration_serialize() {
463 let d = Duration::from_seconds(30);
464 let json = serde_json::to_string(&d).unwrap();
465 assert_eq!(json, r#"{"seconds":30}"#);
466 }
467
468 #[test]
469 fn test_duration_deserialize() {
470 let json = r#"{"minutes": 5, "seconds": 30}"#;
471 let d: Duration = serde_json::from_str(json).unwrap();
472 assert_eq!(d.minutes, Some(5));
473 assert_eq!(d.seconds, Some(30));
474 }
475
476 #[test]
477 fn test_duration_empty_object_rejected() {
478 let json = r#"{}"#;
480 let result: Result<Duration, _> = serde_json::from_str(json);
481 assert!(result.is_err(), "empty duration object should be rejected");
482 let err = result.unwrap_err().to_string();
483 assert!(
484 err.contains("at least one property"),
485 "expected 'at least one property' error, got: {}",
486 err
487 );
488 }
489
490 #[test]
491 fn test_duration_unknown_key_rejected() {
492 let json = r#"{"after": "PT1S"}"#;
494 let result: Result<Duration, _> = serde_json::from_str(json);
495 assert!(
496 result.is_err(),
497 "unknown key in duration object should be rejected"
498 );
499 let err = result.unwrap_err().to_string();
500 assert!(
501 err.contains("unexpected key"),
502 "expected 'unexpected key' error, got: {}",
503 err
504 );
505 }
506
507 #[test]
508 fn test_duration_unknown_key_mixed_rejected() {
509 let json = r#"{"seconds": 30, "duration": "PT1S"}"#;
511 let result: Result<Duration, _> = serde_json::from_str(json);
512 assert!(
513 result.is_err(),
514 "unknown key mixed with valid keys should be rejected"
515 );
516 }
517
518 #[test]
519 fn test_duration_default() {
520 let d = Duration::default();
521 assert_eq!(d.total_milliseconds(), 0);
522 }
523
524 #[test]
525 fn test_oneof_duration_serialize_struct() {
526 let oneof = OneOfDurationOrIso8601Expression::Duration(Duration::from_seconds(30));
527 let json = serde_json::to_string(&oneof).unwrap();
528 assert_eq!(json, r#"{"seconds":30}"#);
529 }
530
531 #[test]
532 fn test_oneof_duration_serialize_iso8601() {
533 let oneof = OneOfDurationOrIso8601Expression::Iso8601Expression("PT5M".to_string());
534 let json = serde_json::to_string(&oneof).unwrap();
535 assert_eq!(json, r#""PT5M""#);
536 }
537
538 #[test]
539 fn test_oneof_duration_deserialize_struct() {
540 let json = r#"{"seconds": 30}"#;
541 let oneof: OneOfDurationOrIso8601Expression = serde_json::from_str(json).unwrap();
542 match oneof {
543 OneOfDurationOrIso8601Expression::Duration(d) => {
544 assert_eq!(d.seconds, Some(30));
545 }
546 _ => panic!("Expected Duration variant"),
547 }
548 }
549
550 #[test]
551 fn test_oneof_duration_deserialize_iso8601() {
552 let json = r#""PT5M""#;
553 let oneof: OneOfDurationOrIso8601Expression = serde_json::from_str(json).unwrap();
554 match oneof {
555 OneOfDurationOrIso8601Expression::Iso8601Expression(s) => {
556 assert_eq!(s, "PT5M");
557 }
558 _ => panic!("Expected Iso8601Expression variant"),
559 }
560 }
561
562 #[test]
563 fn test_duration_display() {
564 let d = Duration {
565 hours: Some(2),
566 minutes: Some(30),
567 ..Default::default()
568 };
569 let display = format!("{}", d);
570 assert!(display.contains("2 hours"));
571 assert!(display.contains("30 minutes"));
572 }
573
574 #[test]
577 fn test_oneof_iso8601_valid_patterns() {
578 let valid_cases = vec![
580 ("\"P1D\"", "P1D"),
581 ("\"P1DT12H30M\"", "P1DT12H30M"),
582 ("\"PT1H\"", "PT1H"),
583 ("\"PT250MS\"", "PT250MS"),
584 ("\"P3DT4H5M6S250MS\"", "P3DT4H5M6S250MS"),
585 ];
586 for (json, expected) in valid_cases {
587 let oneof: OneOfDurationOrIso8601Expression = serde_json::from_str(json).unwrap();
588 match &oneof {
589 OneOfDurationOrIso8601Expression::Iso8601Expression(s) => {
590 assert_eq!(s, expected, "expected ISO expression {}", expected);
591 }
592 _ => panic!("Expected Iso8601Expression variant for {}", expected),
593 }
594 }
595 }
596
597 #[test]
598 fn test_oneof_iso8601_rejected_patterns() {
599 let rejected = vec![
602 "\"P2Y\"", "\"P1Y2M3D\"", "\"P1W\"", "\"1Y\"", ];
607 for json in rejected {
608 let result: Result<OneOfDurationOrIso8601Expression, _> = serde_json::from_str(json);
610 assert!(result.is_err(), "expected {} to fail deserialization", json);
611 }
612 }
613
614 #[test]
615 fn test_duration_composite_with_all_fields() {
616 let d = Duration {
617 days: Some(3),
618 hours: Some(4),
619 minutes: Some(5),
620 seconds: Some(6),
621 milliseconds: Some(250),
622 };
623 let expected = 3 * 86400000 + 4 * 3600000 + 5 * 60000 + 6 * 1000 + 250;
624 assert_eq!(d.total_milliseconds(), expected);
625 }
626
627 #[test]
630 fn test_iso8601_duration_valid_patterns() {
631 assert!(is_iso8601_duration_valid("P1D"), "P1D should be valid");
632 assert!(
633 is_iso8601_duration_valid("P1DT12H30M"),
634 "P1DT12H30M should be valid"
635 );
636 assert!(is_iso8601_duration_valid("PT1H"), "PT1H should be valid");
637 assert!(
638 is_iso8601_duration_valid("PT250MS"),
639 "PT250MS should be valid"
640 );
641 assert!(
642 is_iso8601_duration_valid("P3DT4H5M6S250MS"),
643 "P3DT4H5M6S250MS should be valid"
644 );
645 assert!(is_iso8601_duration_valid("PT30S"), "PT30S should be valid");
646 assert!(
647 is_iso8601_duration_valid("PT0.1S"),
648 "PT0.1S should be valid"
649 );
650 assert!(
651 is_iso8601_duration_valid("P1DT2H30M"),
652 "P1DT2H30M should be valid"
653 );
654 }
655
656 #[test]
657 fn test_iso8601_duration_invalid_patterns() {
658 assert!(!is_iso8601_duration_valid("P2Y"), "years not supported");
660 assert!(
661 !is_iso8601_duration_valid("P1Y2M3D"),
662 "months not supported in date part"
663 );
664 assert!(!is_iso8601_duration_valid("P1W"), "weeks not supported");
665 assert!(
666 !is_iso8601_duration_valid("P1Y2M3D4H"),
667 "years+months not supported"
668 );
669 assert!(
670 !is_iso8601_duration_valid("P1Y2M3D4H5M6S"),
671 "years+months not supported"
672 );
673 assert!(!is_iso8601_duration_valid("P"), "bare P is invalid");
674 assert!(!is_iso8601_duration_valid("P1DT"), "bare PT is invalid");
675 assert!(!is_iso8601_duration_valid("1Y"), "missing P prefix");
676 assert!(!is_iso8601_duration_valid(""), "empty string is invalid");
677 assert!(
678 !is_iso8601_duration_valid("P1.5D"),
679 "fractional days not supported"
680 );
681 assert!(
682 !is_iso8601_duration_valid("P1M"),
683 "months (M in date part) not supported"
684 );
685 assert!(
687 !is_iso8601_duration_valid("P1DT2H3M4S5MS7"),
688 "trailing garbage after MS not valid"
689 );
690 }
691
692 #[test]
695 fn test_iso8601_non_iso_rejected_patterns() {
696 assert!(
698 !is_iso8601_duration_valid("10s"),
699 "non-ISO '10s' should be rejected"
700 );
701 assert!(
702 !is_iso8601_duration_valid("150ms"),
703 "non-ISO '150ms' should be rejected"
704 );
705 assert!(
706 !is_iso8601_duration_valid("1Y"),
707 "non-ISO '1Y' should be rejected"
708 );
709 assert!(
710 !is_iso8601_duration_valid("PT"),
711 "bare 'PT' should be rejected"
712 );
713 }
714
715 #[test]
716 fn test_iso8601_p1dt1h_valid() {
717 assert!(
719 is_iso8601_duration_valid("P1DT1H"),
720 "P1DT1H should be valid"
721 );
722 }
723
724 #[test]
725 fn test_iso8601_pt1s250ms_valid() {
726 assert!(
728 is_iso8601_duration_valid("PT1S250MS"),
729 "PT1S250MS should be valid"
730 );
731 }
732
733 #[test]
734 fn test_iso8601_rejected_year() {
735 assert!(
737 !is_iso8601_duration_valid("P1Y"),
738 "P1Y should be rejected (years)"
739 );
740 }
741
742 #[test]
743 fn test_iso8601_rejected_week() {
744 assert!(
746 !is_iso8601_duration_valid("P1W"),
747 "P1W should be rejected (weeks)"
748 );
749 }
750
751 #[test]
752 fn test_iso8601_rejected_fractional_day() {
753 assert!(
755 !is_iso8601_duration_valid("P1.5D"),
756 "P1.5D should be rejected (fractional days)"
757 );
758 }
759
760 #[test]
761 fn test_iso8601_rejected_month() {
762 assert!(
764 !is_iso8601_duration_valid("P1M"),
765 "P1M should be rejected (months)"
766 );
767 }
768
769 #[test]
770 fn test_iso8601_rejected_bare_pt() {
771 assert!(
773 !is_iso8601_duration_valid("PT"),
774 "bare PT should be rejected"
775 );
776 }
777
778 #[test]
779 fn test_iso8601_rejected_invalid_expression() {
780 assert!(
782 !is_iso8601_duration_valid("1Y"),
783 "1Y without P prefix should be rejected"
784 );
785 }
786
787 #[test]
788 fn test_oneof_duration_roundtrip_struct() {
789 let duration = OneOfDurationOrIso8601Expression::Duration(Duration {
790 days: Some(1),
791 hours: Some(2),
792 minutes: Some(30),
793 ..Default::default()
794 });
795 let serialized = serde_json::to_string(&duration).unwrap();
796 let deserialized: OneOfDurationOrIso8601Expression =
797 serde_json::from_str(&serialized).unwrap();
798 assert_eq!(duration, deserialized);
799 }
800
801 #[test]
802 fn test_oneof_duration_roundtrip_iso8601() {
803 let duration =
804 OneOfDurationOrIso8601Expression::Iso8601Expression("P3DT4H5M6S250MS".to_string());
805 let serialized = serde_json::to_string(&duration).unwrap();
806 let deserialized: OneOfDurationOrIso8601Expression =
807 serde_json::from_str(&serialized).unwrap();
808 assert_eq!(duration, deserialized);
809 }
810}