use rust_decimal::Decimal;
use serde::de::{Deserializer, Error as DeError, Visitor};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Routing {
None,
Account(String),
Forward(String),
Voicemail(String),
Sip(String),
System(String),
Group(String),
Queue(String),
Ivr(String),
Callback(String),
TimeCondition(String),
Disa(String),
Did(String),
Phone(String),
Unknown { tag: String, value: String },
}
impl Routing {
pub fn tag(&self) -> &str {
match self {
Routing::None => "none",
Routing::Account(_) => "account",
Routing::Forward(_) => "fwd",
Routing::Voicemail(_) => "vm",
Routing::Sip(_) => "sip",
Routing::System(_) => "sys",
Routing::Group(_) => "grp",
Routing::Queue(_) => "queue",
Routing::Ivr(_) => "ivr",
Routing::Callback(_) => "cb",
Routing::TimeCondition(_) => "tc",
Routing::Disa(_) => "disa",
Routing::Did(_) => "did",
Routing::Phone(_) => "phone",
Routing::Unknown { tag, .. } => tag,
}
}
pub fn value(&self) -> &str {
match self {
Routing::None => "",
Routing::Account(v)
| Routing::Forward(v)
| Routing::Voicemail(v)
| Routing::Sip(v)
| Routing::System(v)
| Routing::Group(v)
| Routing::Queue(v)
| Routing::Ivr(v)
| Routing::Callback(v)
| Routing::TimeCondition(v)
| Routing::Disa(v)
| Routing::Did(v)
| Routing::Phone(v) => v,
Routing::Unknown { value, .. } => value,
}
}
}
impl fmt::Display for Routing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.tag(), self.value())
}
}
impl FromStr for Routing {
type Err = RoutingParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (tag, value) = match s.find(':') {
Some(i) => (&s[..i], &s[i + 1..]),
None => return Err(RoutingParseError::MissingColon),
};
Ok(match tag {
"none" => Routing::None,
"account" => Routing::Account(value.into()),
"fwd" => Routing::Forward(value.into()),
"vm" => Routing::Voicemail(value.into()),
"sip" => Routing::Sip(value.into()),
"sys" => Routing::System(value.into()),
"grp" => Routing::Group(value.into()),
"queue" => Routing::Queue(value.into()),
"ivr" => Routing::Ivr(value.into()),
"cb" => Routing::Callback(value.into()),
"tc" => Routing::TimeCondition(value.into()),
"disa" => Routing::Disa(value.into()),
"did" => Routing::Did(value.into()),
"phone" => Routing::Phone(value.into()),
other => Routing::Unknown {
tag: other.to_string(),
value: value.to_string(),
},
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoutingParseError {
MissingColon,
}
impl fmt::Display for RoutingParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RoutingParseError::MissingColon => {
f.write_str("routing string is missing required `:` separator")
}
}
}
}
impl std::error::Error for RoutingParseError {}
impl Serialize for Routing {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for Routing {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct RoutingVisitor;
impl<'de> Visitor<'de> for RoutingVisitor {
type Value = Routing;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a VoIP.ms routing string of the form `tag:value`")
}
fn visit_str<E>(self, v: &str) -> Result<Routing, E>
where
E: DeError,
{
Routing::from_str(v).map_err(E::custom)
}
fn visit_string<E>(self, v: String) -> Result<Routing, E>
where
E: DeError,
{
Routing::from_str(&v).map_err(E::custom)
}
}
deserializer.deserialize_str(RoutingVisitor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Seconds {
Value(u64),
Unlimited,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WaitTime {
Value(u64),
Unlimited,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MaxMembers {
Value(u64),
Unlimited,
}
macro_rules! impl_seconds {
($name:ident, $unlimited_wire:literal, $expecting:literal) => {
impl From<u64> for $name {
fn from(v: u64) -> Self {
$name::Value(v)
}
}
impl Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
$name::Value(v) => serializer.serialize_str(&v.to_string()),
$name::Unlimited => serializer.serialize_str($unlimited_wire),
}
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct SecondsVisitor;
impl<'de> Visitor<'de> for SecondsVisitor {
type Value = $name;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str($expecting)
}
fn visit_u64<E>(self, v: u64) -> Result<$name, E>
where
E: DeError,
{
Ok($name::Value(v))
}
fn visit_str<E>(self, v: &str) -> Result<$name, E>
where
E: DeError,
{
let t = v.trim();
match t.to_ascii_lowercase().as_str() {
"none" | "unlimited" => Ok($name::Unlimited),
_ => t
.parse::<u64>()
.map($name::Value)
.map_err(|_| E::custom(format!("invalid seconds value {v}"))),
}
}
}
deserializer.deserialize_any(SecondsVisitor)
}
}
};
}
impl_seconds!(Seconds, "none", "a number of seconds or `none`");
impl_seconds!(WaitTime, "unlimited", "a number of seconds or `unlimited`");
impl_seconds!(MaxMembers, "Unlimited", "a member count or `Unlimited`");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TimezoneOffset(Decimal);
impl TimezoneOffset {
const MIN: i64 = -12;
const MAX: i64 = 13;
pub fn new(hours: impl Into<Decimal>) -> Result<Self, TimezoneOffsetError> {
let hours = hours.into();
if hours < Decimal::from(Self::MIN) || hours > Decimal::from(Self::MAX) {
return Err(TimezoneOffsetError::OutOfRange(hours));
}
Ok(Self(hours))
}
pub fn at(tz: chrono_tz::Tz, date: chrono::NaiveDate) -> Result<Self, TimezoneOffsetError> {
use chrono::{Offset, TimeZone};
let noon = date
.and_hms_opt(12, 0, 0)
.expect("12:00:00 is a valid time");
let seconds = tz
.from_local_datetime(&noon)
.earliest()
.or_else(|| tz.from_local_datetime(&noon).latest())
.map(|dt| dt.offset().fix().local_minus_utc())
.ok_or(TimezoneOffsetError::UnresolvableInstant)?;
let hours = Decimal::from(seconds) / Decimal::from(3600);
Self::new(hours)
}
pub fn hours(&self) -> Decimal {
self.0
}
}
impl fmt::Display for TimezoneOffset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sign = if self.0.is_sign_negative() { '-' } else { '+' };
let abs = self.0.abs();
let whole = abs.trunc();
let minutes = ((abs - whole) * Decimal::from(60)).round();
write!(f, "UTC{sign}{whole:02}:{minutes:02}")
}
}
impl FromStr for TimezoneOffset {
type Err = TimezoneOffsetError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let hours =
Decimal::from_str_exact(s.trim()).map_err(|_| TimezoneOffsetError::NotNumeric)?;
Self::new(hours)
}
}
impl TryFrom<i64> for TimezoneOffset {
type Error = TimezoneOffsetError;
fn try_from(hours: i64) -> Result<Self, Self::Error> {
Self::new(Decimal::from(hours))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimezoneOffsetError {
OutOfRange(Decimal),
NotNumeric,
UnresolvableInstant,
MissingStartDate,
InvalidStartDate,
}
impl fmt::Display for TimezoneOffsetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TimezoneOffsetError::OutOfRange(v) => {
write!(f, "timezone offset {v} is outside the range -12 to 13")
}
TimezoneOffsetError::NotNumeric => f.write_str("timezone offset is not a number"),
TimezoneOffsetError::UnresolvableInstant => {
f.write_str("timezone offset could not be resolved at the given date")
}
TimezoneOffsetError::MissingStartDate => f.write_str(
"timezone requires the query start date (date_from / from) to resolve its offset",
),
TimezoneOffsetError::InvalidStartDate => {
f.write_str("query start date is not a YYYY-MM-DD date")
}
}
}
}
impl std::error::Error for TimezoneOffsetError {}
impl Serialize for TimezoneOffset {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.0)
}
}
impl<'de> Deserialize<'de> for TimezoneOffset {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct OffsetVisitor;
impl<'de> Visitor<'de> for OffsetVisitor {
type Value = TimezoneOffset;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a UTC-offset number in hours between -12 and 13")
}
fn visit_i64<E>(self, v: i64) -> Result<TimezoneOffset, E>
where
E: DeError,
{
TimezoneOffset::try_from(v).map_err(E::custom)
}
fn visit_u64<E>(self, v: u64) -> Result<TimezoneOffset, E>
where
E: DeError,
{
let v = i64::try_from(v).map_err(|_| E::custom("timezone offset out of range"))?;
TimezoneOffset::try_from(v).map_err(E::custom)
}
fn visit_f64<E>(self, v: f64) -> Result<TimezoneOffset, E>
where
E: DeError,
{
let d = Decimal::try_from(v)
.map_err(|_| E::custom("timezone offset is not a valid number"))?;
TimezoneOffset::new(d).map_err(E::custom)
}
fn visit_str<E>(self, v: &str) -> Result<TimezoneOffset, E>
where
E: DeError,
{
TimezoneOffset::from_str(v).map_err(E::custom)
}
}
deserializer.deserialize_any(OffsetVisitor)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TimezoneName {
Known(chrono_tz::Tz),
Unrecognized(String),
}
impl TimezoneName {
pub fn tz(&self) -> Option<chrono_tz::Tz> {
match self {
TimezoneName::Known(tz) => Some(*tz),
TimezoneName::Unrecognized(_) => None,
}
}
pub fn name(&self) -> &str {
match self {
TimezoneName::Known(tz) => tz.name(),
TimezoneName::Unrecognized(s) => s,
}
}
}
impl fmt::Display for TimezoneName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
impl FromStr for TimezoneName {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.parse::<chrono_tz::Tz>()
.map(TimezoneName::Known)
.unwrap_or_else(|_| TimezoneName::Unrecognized(s.to_string())))
}
}
impl From<chrono_tz::Tz> for TimezoneName {
fn from(tz: chrono_tz::Tz) -> Self {
TimezoneName::Known(tz)
}
}
impl Serialize for TimezoneName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.name())
}
}
impl<'de> Deserialize<'de> for TimezoneName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct NameVisitor;
impl<'de> Visitor<'de> for NameVisitor {
type Value = TimezoneName;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an IANA time zone name")
}
fn visit_str<E>(self, v: &str) -> Result<TimezoneName, E>
where
E: DeError,
{
let Ok(name) = TimezoneName::from_str(v);
Ok(name)
}
}
deserializer.deserialize_str(NameVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_documented_tags() {
assert_eq!(Routing::from_str("none:").unwrap(), Routing::None);
assert_eq!(
Routing::from_str("account:100001_VoIP").unwrap(),
Routing::Account("100001_VoIP".into()),
);
assert_eq!(
Routing::from_str("fwd:15555").unwrap(),
Routing::Forward("15555".into()),
);
assert_eq!(
Routing::from_str("vm:101").unwrap(),
Routing::Voicemail("101".into()),
);
assert_eq!(
Routing::from_str("cb:2359").unwrap(),
Routing::Callback("2359".into()),
);
}
#[test]
fn preserves_unknown_tags() {
let r = Routing::from_str("future:abc").unwrap();
assert_eq!(
r,
Routing::Unknown {
tag: "future".into(),
value: "abc".into(),
},
);
assert_eq!(r.to_string(), "future:abc");
}
#[test]
fn sip_value_can_contain_colons() {
let r = Routing::from_str("sip:5552223333@sip.voip.ms:5060").unwrap();
assert_eq!(r, Routing::Sip("5552223333@sip.voip.ms:5060".into()));
assert_eq!(r.to_string(), "sip:5552223333@sip.voip.ms:5060");
}
#[test]
fn rejects_missing_colon() {
assert_eq!(
Routing::from_str("nocolon"),
Err(RoutingParseError::MissingColon),
);
}
#[test]
fn round_trips_through_serde() {
let r = Routing::Forward("19998887777".into());
let json = serde_json::to_string(&r).unwrap();
assert_eq!(json, "\"fwd:19998887777\"");
let back: Routing = serde_json::from_str(&json).unwrap();
assert_eq!(back, r);
}
#[test]
fn deserialize_none() {
let r: Routing = serde_json::from_str("\"none:\"").unwrap();
assert_eq!(r, Routing::None);
}
#[test]
fn seconds_serializes_value_and_sentinel() {
assert_eq!(
serde_json::to_string(&Seconds::Value(30)).unwrap(),
"\"30\""
);
assert_eq!(
serde_json::to_string(&Seconds::Unlimited).unwrap(),
"\"none\""
);
assert_eq!(
serde_json::to_string(&WaitTime::Unlimited).unwrap(),
"\"unlimited\""
);
}
#[test]
fn seconds_deserializes_number_string_and_sentinels() {
assert_eq!(
serde_json::from_str::<Seconds>("45").unwrap(),
Seconds::Value(45)
);
assert_eq!(
serde_json::from_str::<Seconds>("\"45\"").unwrap(),
Seconds::Value(45)
);
for s in ["\"none\"", "\"NONE\"", "\"unlimited\""] {
assert_eq!(
serde_json::from_str::<Seconds>(s).unwrap(),
Seconds::Unlimited,
"{s}"
);
}
assert_eq!(
serde_json::from_str::<WaitTime>("\"unlimited\"").unwrap(),
WaitTime::Unlimited
);
}
#[test]
fn max_members_handles_count_and_capital_unlimited() {
assert_eq!(
serde_json::from_str::<MaxMembers>("\"40\"").unwrap(),
MaxMembers::Value(40)
);
assert_eq!(
serde_json::from_str::<MaxMembers>("\"Unlimited\"").unwrap(),
MaxMembers::Unlimited
);
assert_eq!(
serde_json::to_string(&MaxMembers::Unlimited).unwrap(),
"\"Unlimited\""
);
assert_eq!(
serde_json::to_string(&MaxMembers::Value(40)).unwrap(),
"\"40\""
);
}
#[test]
fn timezone_offset_rejects_out_of_range() {
assert!(TimezoneOffset::try_from(-12).is_ok());
assert!(TimezoneOffset::try_from(13).is_ok());
assert_eq!(
TimezoneOffset::try_from(14),
Err(TimezoneOffsetError::OutOfRange(Decimal::from(14)))
);
assert_eq!(
TimezoneOffset::try_from(-13),
Err(TimezoneOffsetError::OutOfRange(Decimal::from(-13)))
);
}
#[test]
fn timezone_offset_deserializes_number_and_string() {
assert_eq!(
serde_json::from_str::<TimezoneOffset>("-5").unwrap(),
TimezoneOffset::try_from(-5).unwrap()
);
assert_eq!(
serde_json::from_str::<TimezoneOffset>("\"-5\"").unwrap(),
TimezoneOffset::try_from(-5).unwrap()
);
assert_eq!(
serde_json::from_str::<TimezoneOffset>("\"5.5\"").unwrap(),
TimezoneOffset::new(Decimal::from_str_exact("5.5").unwrap()).unwrap()
);
assert!(serde_json::from_str::<TimezoneOffset>("99").is_err());
}
#[test]
fn timezone_offset_serializes_as_bare_number() {
assert_eq!(
serde_json::to_string(&TimezoneOffset::try_from(-5).unwrap()).unwrap(),
"\"-5\""
);
}
#[test]
fn timezone_offset_displays_utc_label() {
assert_eq!(
TimezoneOffset::try_from(-5).unwrap().to_string(),
"UTC-05:00"
);
assert_eq!(
TimezoneOffset::try_from(13).unwrap().to_string(),
"UTC+13:00"
);
assert_eq!(
TimezoneOffset::new(Decimal::from_str_exact("5.5").unwrap())
.unwrap()
.to_string(),
"UTC+05:30"
);
}
#[test]
fn timezone_offset_at_resolves_dst() {
use chrono::NaiveDate;
let jan = NaiveDate::from_ymd_opt(2026, 1, 15).unwrap();
let jul = NaiveDate::from_ymd_opt(2026, 7, 15).unwrap();
assert_eq!(
TimezoneOffset::at(chrono_tz::America::New_York, jan).unwrap(),
TimezoneOffset::try_from(-5).unwrap()
);
assert_eq!(
TimezoneOffset::at(chrono_tz::America::New_York, jul).unwrap(),
TimezoneOffset::try_from(-4).unwrap()
);
assert_eq!(
TimezoneOffset::at(chrono_tz::America::Phoenix, jul).unwrap(),
TimezoneOffset::try_from(-7).unwrap()
);
assert_eq!(
TimezoneOffset::at(chrono_tz::UTC, jan).unwrap(),
TimezoneOffset::try_from(0).unwrap()
);
}
#[test]
fn timezone_name_parses_known_and_preserves_legacy() {
let known: TimezoneName = "America/New_York".parse().unwrap();
assert_eq!(known, TimezoneName::Known(chrono_tz::America::New_York));
assert_eq!(known.tz(), Some(chrono_tz::America::New_York));
assert_eq!(known.name(), "America/New_York");
let legacy: TimezoneName = "Asia/Beijing".parse().unwrap();
assert_eq!(legacy, TimezoneName::Unrecognized("Asia/Beijing".into()));
assert_eq!(legacy.tz(), None);
assert_eq!(legacy.name(), "Asia/Beijing");
assert_eq!(legacy.to_string(), "Asia/Beijing");
}
#[test]
fn timezone_name_round_trips_through_serde() {
for name in ["America/New_York", "US/Pacific-New"] {
let parsed: TimezoneName = name.parse().unwrap();
let json = serde_json::to_string(&parsed).unwrap();
assert_eq!(json, format!("\"{name}\""));
let back: TimezoneName = serde_json::from_str(&json).unwrap();
assert_eq!(back, parsed);
}
}
#[test]
fn timezone_offset_at_handles_sub_hour_and_out_of_range() {
use chrono::NaiveDate;
let day = NaiveDate::from_ymd_opt(2026, 1, 15).unwrap();
assert_eq!(
TimezoneOffset::at(chrono_tz::Asia::Kolkata, day).unwrap(),
TimezoneOffset::new(Decimal::from_str_exact("5.5").unwrap()).unwrap()
);
assert_eq!(
TimezoneOffset::at(chrono_tz::Pacific::Kiritimati, day),
Err(TimezoneOffsetError::OutOfRange(Decimal::from(14)))
);
}
}