1use rust_decimal::Decimal;
8use serde::de::{Deserializer, Error as DeError, Visitor};
9use serde::ser::Serializer;
10use serde::{Deserialize, Serialize};
11use std::fmt;
12use std::str::FromStr;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41pub enum Routing {
42 None,
44 Account(String),
46 Forward(String),
48 Voicemail(String),
50 Sip(String),
52 System(String),
54 Group(String),
56 Queue(String),
58 Ivr(String),
60 Callback(String),
62 TimeCondition(String),
64 Disa(String),
66 Did(String),
68 Phone(String),
70 Unknown { tag: String, value: String },
73}
74
75impl Routing {
76 pub fn tag(&self) -> &str {
78 match self {
79 Routing::None => "none",
80 Routing::Account(_) => "account",
81 Routing::Forward(_) => "fwd",
82 Routing::Voicemail(_) => "vm",
83 Routing::Sip(_) => "sip",
84 Routing::System(_) => "sys",
85 Routing::Group(_) => "grp",
86 Routing::Queue(_) => "queue",
87 Routing::Ivr(_) => "ivr",
88 Routing::Callback(_) => "cb",
89 Routing::TimeCondition(_) => "tc",
90 Routing::Disa(_) => "disa",
91 Routing::Did(_) => "did",
92 Routing::Phone(_) => "phone",
93 Routing::Unknown { tag, .. } => tag,
94 }
95 }
96
97 pub fn value(&self) -> &str {
99 match self {
100 Routing::None => "",
101 Routing::Account(v)
102 | Routing::Forward(v)
103 | Routing::Voicemail(v)
104 | Routing::Sip(v)
105 | Routing::System(v)
106 | Routing::Group(v)
107 | Routing::Queue(v)
108 | Routing::Ivr(v)
109 | Routing::Callback(v)
110 | Routing::TimeCondition(v)
111 | Routing::Disa(v)
112 | Routing::Did(v)
113 | Routing::Phone(v) => v,
114 Routing::Unknown { value, .. } => value,
115 }
116 }
117}
118
119impl fmt::Display for Routing {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 write!(f, "{}:{}", self.tag(), self.value())
122 }
123}
124
125impl FromStr for Routing {
130 type Err = RoutingParseError;
131
132 fn from_str(s: &str) -> Result<Self, Self::Err> {
133 let (tag, value) = match s.find(':') {
134 Some(i) => (&s[..i], &s[i + 1..]),
135 None => return Err(RoutingParseError::MissingColon),
136 };
137
138 Ok(match tag {
139 "none" => Routing::None,
140 "account" => Routing::Account(value.into()),
141 "fwd" => Routing::Forward(value.into()),
142 "vm" => Routing::Voicemail(value.into()),
143 "sip" => Routing::Sip(value.into()),
144 "sys" => Routing::System(value.into()),
145 "grp" => Routing::Group(value.into()),
146 "queue" => Routing::Queue(value.into()),
147 "ivr" => Routing::Ivr(value.into()),
148 "cb" => Routing::Callback(value.into()),
149 "tc" => Routing::TimeCondition(value.into()),
150 "disa" => Routing::Disa(value.into()),
151 "did" => Routing::Did(value.into()),
152 "phone" => Routing::Phone(value.into()),
153 other => Routing::Unknown {
154 tag: other.to_string(),
155 value: value.to_string(),
156 },
157 })
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum RoutingParseError {
164 MissingColon,
166}
167
168impl fmt::Display for RoutingParseError {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 match self {
171 RoutingParseError::MissingColon => {
172 f.write_str("routing string is missing required `:` separator")
173 }
174 }
175 }
176}
177
178impl std::error::Error for RoutingParseError {}
179
180impl Serialize for Routing {
181 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182 where
183 S: Serializer,
184 {
185 serializer.collect_str(self)
186 }
187}
188
189impl<'de> Deserialize<'de> for Routing {
190 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
191 where
192 D: Deserializer<'de>,
193 {
194 struct RoutingVisitor;
195
196 impl<'de> Visitor<'de> for RoutingVisitor {
197 type Value = Routing;
198
199 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 f.write_str("a VoIP.ms routing string of the form `tag:value`")
201 }
202
203 fn visit_str<E>(self, v: &str) -> Result<Routing, E>
204 where
205 E: DeError,
206 {
207 Routing::from_str(v).map_err(E::custom)
208 }
209
210 fn visit_string<E>(self, v: String) -> Result<Routing, E>
211 where
212 E: DeError,
213 {
214 Routing::from_str(&v).map_err(E::custom)
215 }
216 }
217
218 deserializer.deserialize_str(RoutingVisitor)
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
230pub enum Seconds {
231 Value(u64),
233 Unlimited,
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
242pub enum WaitTime {
243 Value(u64),
245 Unlimited,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
254pub enum MaxMembers {
255 Value(u64),
257 Unlimited,
259}
260
261macro_rules! impl_seconds {
262 ($name:ident, $unlimited_wire:literal, $expecting:literal) => {
263 impl From<u64> for $name {
264 fn from(v: u64) -> Self {
265 $name::Value(v)
266 }
267 }
268
269 impl Serialize for $name {
270 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
271 where
272 S: Serializer,
273 {
274 match self {
275 $name::Value(v) => serializer.serialize_str(&v.to_string()),
276 $name::Unlimited => serializer.serialize_str($unlimited_wire),
277 }
278 }
279 }
280
281 impl<'de> Deserialize<'de> for $name {
282 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
283 where
284 D: Deserializer<'de>,
285 {
286 struct SecondsVisitor;
287
288 impl<'de> Visitor<'de> for SecondsVisitor {
289 type Value = $name;
290
291 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292 f.write_str($expecting)
293 }
294
295 fn visit_u64<E>(self, v: u64) -> Result<$name, E>
296 where
297 E: DeError,
298 {
299 Ok($name::Value(v))
300 }
301
302 fn visit_str<E>(self, v: &str) -> Result<$name, E>
303 where
304 E: DeError,
305 {
306 let t = v.trim();
307 match t.to_ascii_lowercase().as_str() {
308 "none" | "unlimited" => Ok($name::Unlimited),
309 _ => t
310 .parse::<u64>()
311 .map($name::Value)
312 .map_err(|_| E::custom(format!("invalid seconds value {v}"))),
313 }
314 }
315 }
316
317 deserializer.deserialize_any(SecondsVisitor)
318 }
319 }
320 };
321}
322
323impl_seconds!(Seconds, "none", "a number of seconds or `none`");
324impl_seconds!(WaitTime, "unlimited", "a number of seconds or `unlimited`");
325impl_seconds!(MaxMembers, "Unlimited", "a member count or `Unlimited`");
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
350pub struct TimezoneOffset(Decimal);
351
352impl TimezoneOffset {
353 const MIN: i64 = -12;
355 const MAX: i64 = 13;
356
357 pub fn new(hours: impl Into<Decimal>) -> Result<Self, TimezoneOffsetError> {
359 let hours = hours.into();
360 if hours < Decimal::from(Self::MIN) || hours > Decimal::from(Self::MAX) {
361 return Err(TimezoneOffsetError::OutOfRange(hours));
362 }
363
364 Ok(Self(hours))
365 }
366
367 pub fn at(tz: chrono_tz::Tz, date: chrono::NaiveDate) -> Result<Self, TimezoneOffsetError> {
377 use chrono::{Offset, TimeZone};
378
379 let noon = date
380 .and_hms_opt(12, 0, 0)
381 .expect("12:00:00 is a valid time");
382 let seconds = tz
383 .from_local_datetime(&noon)
384 .earliest()
385 .or_else(|| tz.from_local_datetime(&noon).latest())
386 .map(|dt| dt.offset().fix().local_minus_utc())
387 .ok_or(TimezoneOffsetError::UnresolvableInstant)?;
388 let hours = Decimal::from(seconds) / Decimal::from(3600);
391 Self::new(hours)
392 }
393
394 pub fn hours(&self) -> Decimal {
396 self.0
397 }
398}
399
400impl fmt::Display for TimezoneOffset {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 let sign = if self.0.is_sign_negative() { '-' } else { '+' };
404 let abs = self.0.abs();
405 let whole = abs.trunc();
406 let minutes = ((abs - whole) * Decimal::from(60)).round();
407 write!(f, "UTC{sign}{whole:02}:{minutes:02}")
408 }
409}
410
411impl FromStr for TimezoneOffset {
412 type Err = TimezoneOffsetError;
413
414 fn from_str(s: &str) -> Result<Self, Self::Err> {
415 let hours =
416 Decimal::from_str_exact(s.trim()).map_err(|_| TimezoneOffsetError::NotNumeric)?;
417 Self::new(hours)
418 }
419}
420
421impl TryFrom<i64> for TimezoneOffset {
422 type Error = TimezoneOffsetError;
423
424 fn try_from(hours: i64) -> Result<Self, Self::Error> {
425 Self::new(Decimal::from(hours))
426 }
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
431pub enum TimezoneOffsetError {
432 OutOfRange(Decimal),
436 NotNumeric,
438 UnresolvableInstant,
441 MissingStartDate,
445 InvalidStartDate,
448}
449
450impl fmt::Display for TimezoneOffsetError {
451 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 match self {
453 TimezoneOffsetError::OutOfRange(v) => {
454 write!(f, "timezone offset {v} is outside the range -12 to 13")
455 }
456
457 TimezoneOffsetError::NotNumeric => f.write_str("timezone offset is not a number"),
458 TimezoneOffsetError::UnresolvableInstant => {
459 f.write_str("timezone offset could not be resolved at the given date")
460 }
461
462 TimezoneOffsetError::MissingStartDate => f.write_str(
463 "timezone requires the query start date (date_from / from) to resolve its offset",
464 ),
465 TimezoneOffsetError::InvalidStartDate => {
466 f.write_str("query start date is not a YYYY-MM-DD date")
467 }
468 }
469 }
470}
471
472impl std::error::Error for TimezoneOffsetError {}
473
474impl Serialize for TimezoneOffset {
475 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
476 where
477 S: Serializer,
478 {
479 serializer.collect_str(&self.0)
480 }
481}
482
483impl<'de> Deserialize<'de> for TimezoneOffset {
484 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
485 where
486 D: Deserializer<'de>,
487 {
488 struct OffsetVisitor;
489
490 impl<'de> Visitor<'de> for OffsetVisitor {
491 type Value = TimezoneOffset;
492
493 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494 f.write_str("a UTC-offset number in hours between -12 and 13")
495 }
496
497 fn visit_i64<E>(self, v: i64) -> Result<TimezoneOffset, E>
498 where
499 E: DeError,
500 {
501 TimezoneOffset::try_from(v).map_err(E::custom)
502 }
503
504 fn visit_u64<E>(self, v: u64) -> Result<TimezoneOffset, E>
505 where
506 E: DeError,
507 {
508 let v = i64::try_from(v).map_err(|_| E::custom("timezone offset out of range"))?;
509 TimezoneOffset::try_from(v).map_err(E::custom)
510 }
511
512 fn visit_f64<E>(self, v: f64) -> Result<TimezoneOffset, E>
513 where
514 E: DeError,
515 {
516 let d = Decimal::try_from(v)
517 .map_err(|_| E::custom("timezone offset is not a valid number"))?;
518 TimezoneOffset::new(d).map_err(E::custom)
519 }
520
521 fn visit_str<E>(self, v: &str) -> Result<TimezoneOffset, E>
522 where
523 E: DeError,
524 {
525 TimezoneOffset::from_str(v).map_err(E::custom)
526 }
527 }
528
529 deserializer.deserialize_any(OffsetVisitor)
530 }
531}
532
533#[derive(Debug, Clone, PartialEq, Eq, Hash)]
544pub enum TimezoneName {
545 Known(chrono_tz::Tz),
547 Unrecognized(String),
549}
550
551impl TimezoneName {
552 pub fn tz(&self) -> Option<chrono_tz::Tz> {
554 match self {
555 TimezoneName::Known(tz) => Some(*tz),
556 TimezoneName::Unrecognized(_) => None,
557 }
558 }
559
560 pub fn name(&self) -> &str {
562 match self {
563 TimezoneName::Known(tz) => tz.name(),
564 TimezoneName::Unrecognized(s) => s,
565 }
566 }
567}
568
569impl fmt::Display for TimezoneName {
570 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
571 f.write_str(self.name())
572 }
573}
574
575impl FromStr for TimezoneName {
576 type Err = std::convert::Infallible;
577
578 fn from_str(s: &str) -> Result<Self, Self::Err> {
579 Ok(s.parse::<chrono_tz::Tz>()
580 .map(TimezoneName::Known)
581 .unwrap_or_else(|_| TimezoneName::Unrecognized(s.to_string())))
582 }
583}
584
585impl From<chrono_tz::Tz> for TimezoneName {
586 fn from(tz: chrono_tz::Tz) -> Self {
587 TimezoneName::Known(tz)
588 }
589}
590
591impl Serialize for TimezoneName {
592 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
593 where
594 S: Serializer,
595 {
596 serializer.serialize_str(self.name())
597 }
598}
599
600impl<'de> Deserialize<'de> for TimezoneName {
601 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
602 where
603 D: Deserializer<'de>,
604 {
605 struct NameVisitor;
606
607 impl<'de> Visitor<'de> for NameVisitor {
608 type Value = TimezoneName;
609
610 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
611 f.write_str("an IANA time zone name")
612 }
613
614 fn visit_str<E>(self, v: &str) -> Result<TimezoneName, E>
615 where
616 E: DeError,
617 {
618 let Ok(name) = TimezoneName::from_str(v);
619 Ok(name)
620 }
621 }
622
623 deserializer.deserialize_str(NameVisitor)
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 #[test]
632 fn parses_documented_tags() {
633 assert_eq!(Routing::from_str("none:").unwrap(), Routing::None);
634 assert_eq!(
635 Routing::from_str("account:100001_VoIP").unwrap(),
636 Routing::Account("100001_VoIP".into()),
637 );
638 assert_eq!(
639 Routing::from_str("fwd:15555").unwrap(),
640 Routing::Forward("15555".into()),
641 );
642 assert_eq!(
643 Routing::from_str("vm:101").unwrap(),
644 Routing::Voicemail("101".into()),
645 );
646 assert_eq!(
647 Routing::from_str("cb:2359").unwrap(),
648 Routing::Callback("2359".into()),
649 );
650 }
651
652 #[test]
653 fn preserves_unknown_tags() {
654 let r = Routing::from_str("future:abc").unwrap();
655 assert_eq!(
656 r,
657 Routing::Unknown {
658 tag: "future".into(),
659 value: "abc".into(),
660 },
661 );
662 assert_eq!(r.to_string(), "future:abc");
663 }
664
665 #[test]
666 fn sip_value_can_contain_colons() {
667 let r = Routing::from_str("sip:5552223333@sip.voip.ms:5060").unwrap();
669 assert_eq!(r, Routing::Sip("5552223333@sip.voip.ms:5060".into()));
670 assert_eq!(r.to_string(), "sip:5552223333@sip.voip.ms:5060");
671 }
672
673 #[test]
674 fn rejects_missing_colon() {
675 assert_eq!(
676 Routing::from_str("nocolon"),
677 Err(RoutingParseError::MissingColon),
678 );
679 }
680
681 #[test]
682 fn round_trips_through_serde() {
683 let r = Routing::Forward("19998887777".into());
684 let json = serde_json::to_string(&r).unwrap();
685 assert_eq!(json, "\"fwd:19998887777\"");
686 let back: Routing = serde_json::from_str(&json).unwrap();
687 assert_eq!(back, r);
688 }
689
690 #[test]
691 fn deserialize_none() {
692 let r: Routing = serde_json::from_str("\"none:\"").unwrap();
693 assert_eq!(r, Routing::None);
694 }
695
696 #[test]
697 fn seconds_serializes_value_and_sentinel() {
698 assert_eq!(
699 serde_json::to_string(&Seconds::Value(30)).unwrap(),
700 "\"30\""
701 );
702 assert_eq!(
703 serde_json::to_string(&Seconds::Unlimited).unwrap(),
704 "\"none\""
705 );
706 assert_eq!(
707 serde_json::to_string(&WaitTime::Unlimited).unwrap(),
708 "\"unlimited\""
709 );
710 }
711
712 #[test]
713 fn seconds_deserializes_number_string_and_sentinels() {
714 assert_eq!(
716 serde_json::from_str::<Seconds>("45").unwrap(),
717 Seconds::Value(45)
718 );
719 assert_eq!(
720 serde_json::from_str::<Seconds>("\"45\"").unwrap(),
721 Seconds::Value(45)
722 );
723 for s in ["\"none\"", "\"NONE\"", "\"unlimited\""] {
724 assert_eq!(
725 serde_json::from_str::<Seconds>(s).unwrap(),
726 Seconds::Unlimited,
727 "{s}"
728 );
729 }
730 assert_eq!(
732 serde_json::from_str::<WaitTime>("\"unlimited\"").unwrap(),
733 WaitTime::Unlimited
734 );
735 }
736
737 #[test]
738 fn max_members_handles_count_and_capital_unlimited() {
739 assert_eq!(
741 serde_json::from_str::<MaxMembers>("\"40\"").unwrap(),
742 MaxMembers::Value(40)
743 );
744 assert_eq!(
745 serde_json::from_str::<MaxMembers>("\"Unlimited\"").unwrap(),
746 MaxMembers::Unlimited
747 );
748 assert_eq!(
750 serde_json::to_string(&MaxMembers::Unlimited).unwrap(),
751 "\"Unlimited\""
752 );
753 assert_eq!(
754 serde_json::to_string(&MaxMembers::Value(40)).unwrap(),
755 "\"40\""
756 );
757 }
758
759 #[test]
760 fn timezone_offset_rejects_out_of_range() {
761 assert!(TimezoneOffset::try_from(-12).is_ok());
762 assert!(TimezoneOffset::try_from(13).is_ok());
763 assert_eq!(
764 TimezoneOffset::try_from(14),
765 Err(TimezoneOffsetError::OutOfRange(Decimal::from(14)))
766 );
767 assert_eq!(
768 TimezoneOffset::try_from(-13),
769 Err(TimezoneOffsetError::OutOfRange(Decimal::from(-13)))
770 );
771 }
772
773 #[test]
774 fn timezone_offset_deserializes_number_and_string() {
775 assert_eq!(
777 serde_json::from_str::<TimezoneOffset>("-5").unwrap(),
778 TimezoneOffset::try_from(-5).unwrap()
779 );
780 assert_eq!(
781 serde_json::from_str::<TimezoneOffset>("\"-5\"").unwrap(),
782 TimezoneOffset::try_from(-5).unwrap()
783 );
784 assert_eq!(
785 serde_json::from_str::<TimezoneOffset>("\"5.5\"").unwrap(),
786 TimezoneOffset::new(Decimal::from_str_exact("5.5").unwrap()).unwrap()
787 );
788 assert!(serde_json::from_str::<TimezoneOffset>("99").is_err());
790 }
791
792 #[test]
793 fn timezone_offset_serializes_as_bare_number() {
794 assert_eq!(
795 serde_json::to_string(&TimezoneOffset::try_from(-5).unwrap()).unwrap(),
796 "\"-5\""
797 );
798 }
799
800 #[test]
801 fn timezone_offset_displays_utc_label() {
802 assert_eq!(
803 TimezoneOffset::try_from(-5).unwrap().to_string(),
804 "UTC-05:00"
805 );
806 assert_eq!(
807 TimezoneOffset::try_from(13).unwrap().to_string(),
808 "UTC+13:00"
809 );
810 assert_eq!(
811 TimezoneOffset::new(Decimal::from_str_exact("5.5").unwrap())
812 .unwrap()
813 .to_string(),
814 "UTC+05:30"
815 );
816 }
817
818 #[test]
819 fn timezone_offset_at_resolves_dst() {
820 use chrono::NaiveDate;
821 let jan = NaiveDate::from_ymd_opt(2026, 1, 15).unwrap();
822 let jul = NaiveDate::from_ymd_opt(2026, 7, 15).unwrap();
823 assert_eq!(
825 TimezoneOffset::at(chrono_tz::America::New_York, jan).unwrap(),
826 TimezoneOffset::try_from(-5).unwrap()
827 );
828 assert_eq!(
829 TimezoneOffset::at(chrono_tz::America::New_York, jul).unwrap(),
830 TimezoneOffset::try_from(-4).unwrap()
831 );
832 assert_eq!(
834 TimezoneOffset::at(chrono_tz::America::Phoenix, jul).unwrap(),
835 TimezoneOffset::try_from(-7).unwrap()
836 );
837 assert_eq!(
839 TimezoneOffset::at(chrono_tz::UTC, jan).unwrap(),
840 TimezoneOffset::try_from(0).unwrap()
841 );
842 }
843
844 #[test]
845 fn timezone_name_parses_known_and_preserves_legacy() {
846 let known: TimezoneName = "America/New_York".parse().unwrap();
847 assert_eq!(known, TimezoneName::Known(chrono_tz::America::New_York));
848 assert_eq!(known.tz(), Some(chrono_tz::America::New_York));
849 assert_eq!(known.name(), "America/New_York");
850
851 let legacy: TimezoneName = "Asia/Beijing".parse().unwrap();
854 assert_eq!(legacy, TimezoneName::Unrecognized("Asia/Beijing".into()));
855 assert_eq!(legacy.tz(), None);
856 assert_eq!(legacy.name(), "Asia/Beijing");
857 assert_eq!(legacy.to_string(), "Asia/Beijing");
858 }
859
860 #[test]
861 fn timezone_name_round_trips_through_serde() {
862 for name in ["America/New_York", "US/Pacific-New"] {
863 let parsed: TimezoneName = name.parse().unwrap();
864 let json = serde_json::to_string(&parsed).unwrap();
865 assert_eq!(json, format!("\"{name}\""));
866 let back: TimezoneName = serde_json::from_str(&json).unwrap();
867 assert_eq!(back, parsed);
868 }
869 }
870
871 #[test]
872 fn timezone_offset_at_handles_sub_hour_and_out_of_range() {
873 use chrono::NaiveDate;
874 let day = NaiveDate::from_ymd_opt(2026, 1, 15).unwrap();
875 assert_eq!(
877 TimezoneOffset::at(chrono_tz::Asia::Kolkata, day).unwrap(),
878 TimezoneOffset::new(Decimal::from_str_exact("5.5").unwrap()).unwrap()
879 );
880 assert_eq!(
882 TimezoneOffset::at(chrono_tz::Pacific::Kiritimati, day),
883 Err(TimezoneOffsetError::OutOfRange(Decimal::from(14)))
884 );
885 }
886}