use std::collections::HashMap;
use std::sync::Arc;
use futures_util::Stream;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::client::{CallOptions, Client};
use crate::common::MessageResponse;
use crate::error::Result;
use crate::pagination::{auto_page, paginate, ListParams, Page, PageFetcher};
use crate::query::QueryBuilder;
use crate::resources::escape;
use crate::resources::sip::{SipDirection, SipIncludeHeaders, SipRoomPrefix, SipRoomType};
const PATH: &str = "/v2/sip/routing-rule";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SipRoomConfig {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub kind: Option<SipRoomType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<SipRoomPrefix>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pin: Option<String>,
}
#[derive(Debug, Clone)]
pub enum RoutingRuleTarget {
Direct {
room_id: String,
pin: Option<String>,
},
Individual {
room_prefix: Option<SipRoomPrefix>,
pin: Option<String>,
},
}
impl From<RoutingRuleTarget> for SipRoomConfig {
fn from(target: RoutingRuleTarget) -> Self {
match target {
RoutingRuleTarget::Direct { room_id, pin } => SipRoomConfig {
kind: Some(SipRoomType::STATIC),
prefix: None,
id: Some(room_id),
pin,
},
RoutingRuleTarget::Individual { room_prefix, pin } => SipRoomConfig {
kind: Some(SipRoomType::DYNAMIC),
prefix: Some(room_prefix.unwrap_or(SipRoomPrefix::BLANK)),
id: None,
pin,
},
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SipRoutingRule {
pub id: String,
pub name: Option<String>,
#[serde(rename = "type")]
pub direction: Option<SipDirection>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub numbers: Vec<String>,
pub room: Option<SipRoomConfig>,
pub agent_id: Option<String>,
pub agent_metadata: Option<Map<String, Value>>,
pub hide_phone_number: Option<bool>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub tags: Vec<String>,
pub api_key: Option<String>,
pub recording: Option<bool>,
pub dtmf: Option<bool>,
pub noise_cancellation: Option<bool>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub allowed_numbers: Vec<String>,
#[serde(
rename = "allowedIpAddresses",
default,
deserialize_with = "crate::common::null_to_default"
)]
pub allowed_ip_addresses: Vec<String>,
pub include_headers: Option<SipIncludeHeaders>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub headers: HashMap<String, String>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub headers_to_attributes: HashMap<String, String>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, Default)]
pub struct CreateRoutingRuleParams {
pub name: String,
pub direction: Option<SipDirection>,
pub phone_numbers: Vec<String>,
pub rule: Option<RoutingRuleTarget>,
pub room: Option<SipRoomConfig>,
pub agent_id: Option<String>,
pub agent_metadata: Option<Map<String, Value>>,
pub include_headers: Option<SipIncludeHeaders>,
pub headers: HashMap<String, String>,
pub headers_to_attributes: HashMap<String, String>,
pub allowed_numbers: Vec<String>,
pub allowed_ip_addresses: Vec<String>,
pub tags: Vec<String>,
pub recording: Option<bool>,
pub dtmf: Option<bool>,
pub noise_cancellation: Option<bool>,
pub hide_phone_number: Option<bool>,
pub api_key: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct UpdateRoutingRuleParams {
pub name: Option<String>,
pub phone_numbers: Vec<String>,
pub rule: Option<RoutingRuleTarget>,
pub room: Option<SipRoomConfig>,
pub agent_id: Option<String>,
pub agent_metadata: Option<Map<String, Value>>,
pub include_headers: Option<SipIncludeHeaders>,
pub headers: HashMap<String, String>,
pub headers_to_attributes: HashMap<String, String>,
pub allowed_numbers: Vec<String>,
pub allowed_ip_addresses: Vec<String>,
pub tags: Vec<String>,
pub recording: Option<bool>,
pub dtmf: Option<bool>,
pub noise_cancellation: Option<bool>,
pub hide_phone_number: Option<bool>,
pub api_key: Option<String>,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct RoutingRuleWire {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
direction: Option<SipDirection>,
#[serde(skip_serializing_if = "Vec::is_empty")]
phone_numbers: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
room: Option<SipRoomConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
agent_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
agent_metadata: Option<Map<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
include_headers: Option<SipIncludeHeaders>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
headers: HashMap<String, String>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
headers_to_attributes: HashMap<String, String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
allowed_numbers: Vec<String>,
#[serde(rename = "allowedIpAddresses", skip_serializing_if = "Vec::is_empty")]
allowed_ip_addresses: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
recording: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
dtmf: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
noise_cancellation: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
hide_phone_number: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
api_key: Option<String>,
}
fn resolve_room(
room: Option<SipRoomConfig>,
rule: Option<RoutingRuleTarget>,
) -> Option<SipRoomConfig> {
room.or_else(|| rule.map(SipRoomConfig::from))
}
impl From<CreateRoutingRuleParams> for RoutingRuleWire {
fn from(params: CreateRoutingRuleParams) -> Self {
Self {
name: Some(params.name).filter(|name| !name.is_empty()),
direction: params.direction,
phone_numbers: params.phone_numbers,
room: resolve_room(params.room, params.rule),
agent_id: params.agent_id,
agent_metadata: params.agent_metadata,
include_headers: params.include_headers,
headers: params.headers,
headers_to_attributes: params.headers_to_attributes,
allowed_numbers: params.allowed_numbers,
allowed_ip_addresses: params.allowed_ip_addresses,
tags: params.tags,
recording: params.recording,
dtmf: params.dtmf,
noise_cancellation: params.noise_cancellation,
hide_phone_number: params.hide_phone_number,
api_key: params.api_key,
}
}
}
impl From<UpdateRoutingRuleParams> for RoutingRuleWire {
fn from(params: UpdateRoutingRuleParams) -> Self {
Self {
name: params.name,
direction: None,
phone_numbers: params.phone_numbers,
room: resolve_room(params.room, params.rule),
agent_id: params.agent_id,
agent_metadata: params.agent_metadata,
include_headers: params.include_headers,
headers: params.headers,
headers_to_attributes: params.headers_to_attributes,
allowed_numbers: params.allowed_numbers,
allowed_ip_addresses: params.allowed_ip_addresses,
tags: params.tags,
recording: params.recording,
dtmf: params.dtmf,
noise_cancellation: params.noise_cancellation,
hide_phone_number: params.hide_phone_number,
api_key: params.api_key,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SipRoutingRuleListParams {
pub page: Option<u32>,
pub per_page: Option<u32>,
pub cursor: Option<String>,
pub search: Option<String>,
pub direction: Option<SipDirection>,
pub rule_ids: Vec<String>,
pub room_type: Vec<SipRoomType>,
}
impl SipRoutingRuleListParams {
fn pagination(&self) -> ListParams {
ListParams {
page: self.page,
per_page: self.per_page,
cursor: self.cursor.clone(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SipRoutingRulesResource<'a> {
client: &'a Client,
}
impl<'a> SipRoutingRulesResource<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn create(&self, params: CreateRoutingRuleParams) -> Result<SipRoutingRule> {
let body = RoutingRuleWire::from(params);
self.client
.json(Method::POST, PATH, CallOptions::json(&body)?)
.await
}
pub async fn list(&self, params: SipRoutingRuleListParams) -> Result<Page<SipRoutingRule>> {
paginate(self.fetcher(¶ms), ¶ms.pagination(), "data", None).await
}
pub fn list_stream(
&self,
params: SipRoutingRuleListParams,
) -> impl Stream<Item = Result<SipRoutingRule>> + Send {
auto_page(self.fetcher(¶ms), params.pagination(), "data", None)
}
pub async fn get(&self, rule_id: &str) -> Result<SipRoutingRule> {
let path = format!("{PATH}/{}", escape(rule_id));
self.client
.json(Method::GET, &path, CallOptions::new())
.await
}
pub async fn update(
&self,
rule_id: &str,
params: UpdateRoutingRuleParams,
) -> Result<SipRoutingRule> {
let body = RoutingRuleWire::from(params);
let path = format!("{PATH}/{}", escape(rule_id));
self.client
.json(Method::PATCH, &path, CallOptions::json(&body)?)
.await
}
pub async fn delete(&self, rule_id: &str) -> Result<MessageResponse> {
let path = format!("{PATH}/{}", escape(rule_id));
self.client
.json(Method::DELETE, &path, CallOptions::new())
.await
}
fn fetcher(&self, params: &SipRoutingRuleListParams) -> PageFetcher {
let client = self.client.clone();
let params = params.clone();
Arc::new(move |page, per_page| {
let client = client.clone();
let params = params.clone();
Box::pin(async move {
let query = QueryBuilder::new()
.opt("page", page)
.opt("perPage", per_page)
.opt_str("search", params.search.as_deref())
.opt_str("type", params.direction.as_ref().map(SipDirection::as_str))
.csv("ruleIds", ¶ms.rule_ids)
.csv("roomType", ¶ms.room_type)
.into_pairs();
client
.json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
.await
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn wire(params: CreateRoutingRuleParams) -> Value {
serde_json::to_value(RoutingRuleWire::from(params)).unwrap()
}
#[test]
fn a_direct_target_becomes_a_static_room() {
let body = wire(CreateRoutingRuleParams {
name: "rule".into(),
direction: Some(SipDirection::INBOUND),
rule: Some(RoutingRuleTarget::Direct {
room_id: "abcd-efgh".into(),
pin: Some("1234".into()),
}),
..Default::default()
});
assert_eq!(
body,
json!({
"name": "rule",
"type": "inbound",
"room": {"type": "static", "id": "abcd-efgh", "pin": "1234"},
})
);
}
#[test]
fn an_individual_target_becomes_a_dynamic_room_defaulting_to_blank() {
let body = wire(CreateRoutingRuleParams {
name: "rule".into(),
rule: Some(RoutingRuleTarget::Individual {
room_prefix: None,
pin: None,
}),
..Default::default()
});
assert_eq!(body["room"], json!({"type": "dynamic", "prefix": "blank"}));
let body = wire(CreateRoutingRuleParams {
name: "rule".into(),
rule: Some(RoutingRuleTarget::Individual {
room_prefix: Some(SipRoomPrefix::SIP_NUMBER),
pin: None,
}),
..Default::default()
});
assert_eq!(
body["room"],
json!({"type": "dynamic", "prefix": "sipNumber"})
);
}
#[test]
fn an_explicit_room_wins_over_the_rule_sugar() {
let body = wire(CreateRoutingRuleParams {
name: "rule".into(),
room: Some(SipRoomConfig {
kind: Some(SipRoomType::STATIC),
id: Some("explicit".into()),
..Default::default()
}),
rule: Some(RoutingRuleTarget::Direct {
room_id: "sugar".into(),
pin: None,
}),
..Default::default()
});
assert_eq!(body["room"]["id"], json!("explicit"));
}
#[test]
fn an_update_never_sends_the_direction() {
let body = serde_json::to_value(RoutingRuleWire::from(UpdateRoutingRuleParams {
name: Some("renamed".into()),
..Default::default()
}))
.unwrap();
assert_eq!(body, json!({"name": "renamed"}));
assert!(body.get("type").is_none(), "direction is fixed at creation");
}
#[test]
fn empty_collections_are_omitted() {
let body = wire(CreateRoutingRuleParams {
name: "rule".into(),
..Default::default()
});
assert_eq!(body, json!({"name": "rule"}));
}
#[test]
fn allowed_ip_addresses_keeps_its_lowercase_p() {
let body = wire(CreateRoutingRuleParams {
name: "rule".into(),
allowed_ip_addresses: vec!["1.2.3.4".into()],
..Default::default()
});
assert_eq!(body["allowedIpAddresses"], json!(["1.2.3.4"]));
}
}