1use std::collections::HashMap;
4use std::sync::Arc;
5
6use futures_util::Stream;
7use reqwest::Method;
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11use crate::client::{CallOptions, Client};
12use crate::common::MessageResponse;
13use crate::error::Result;
14use crate::pagination::{auto_page, paginate, ListParams, Page, PageFetcher};
15use crate::query::QueryBuilder;
16use crate::resources::escape;
17use crate::resources::sip::{SipDirection, SipIncludeHeaders, SipRoomPrefix, SipRoomType};
18
19const PATH: &str = "/v2/sip/routing-rule";
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct SipRoomConfig {
25 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
27 pub kind: Option<SipRoomType>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub prefix: Option<SipRoomPrefix>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub id: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub pin: Option<String>,
37}
38
39#[derive(Debug, Clone)]
45pub enum RoutingRuleTarget {
46 Direct {
48 room_id: String,
50 pin: Option<String>,
52 },
53 Individual {
55 room_prefix: Option<SipRoomPrefix>,
58 pin: Option<String>,
60 },
61}
62
63impl From<RoutingRuleTarget> for SipRoomConfig {
64 fn from(target: RoutingRuleTarget) -> Self {
65 match target {
66 RoutingRuleTarget::Direct { room_id, pin } => SipRoomConfig {
67 kind: Some(SipRoomType::STATIC),
68 prefix: None,
69 id: Some(room_id),
70 pin,
71 },
72 RoutingRuleTarget::Individual { room_prefix, pin } => SipRoomConfig {
73 kind: Some(SipRoomType::DYNAMIC),
74 prefix: Some(room_prefix.unwrap_or(SipRoomPrefix::BLANK)),
75 id: None,
76 pin,
77 },
78 }
79 }
80}
81
82#[derive(Debug, Clone, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct SipRoutingRule {
86 pub id: String,
88 pub name: Option<String>,
90 #[serde(rename = "type")]
92 pub direction: Option<SipDirection>,
93 #[serde(default, deserialize_with = "crate::common::null_to_default")]
95 pub numbers: Vec<String>,
96 pub room: Option<SipRoomConfig>,
98 pub agent_id: Option<String>,
100 pub agent_metadata: Option<Map<String, Value>>,
102 pub hide_phone_number: Option<bool>,
104 #[serde(default, deserialize_with = "crate::common::null_to_default")]
106 pub tags: Vec<String>,
107 pub api_key: Option<String>,
109 pub recording: Option<bool>,
111 pub dtmf: Option<bool>,
113 pub noise_cancellation: Option<bool>,
115 #[serde(default, deserialize_with = "crate::common::null_to_default")]
117 pub allowed_numbers: Vec<String>,
118 #[serde(
120 rename = "allowedIpAddresses",
121 default,
122 deserialize_with = "crate::common::null_to_default"
123 )]
124 pub allowed_ip_addresses: Vec<String>,
125 pub include_headers: Option<SipIncludeHeaders>,
127 #[serde(default, deserialize_with = "crate::common::null_to_default")]
129 pub headers: HashMap<String, String>,
130 #[serde(default, deserialize_with = "crate::common::null_to_default")]
132 pub headers_to_attributes: HashMap<String, String>,
133 pub created_at: Option<String>,
135 pub updated_at: Option<String>,
137 #[serde(flatten)]
139 pub extra: Map<String, Value>,
140}
141
142#[derive(Debug, Clone, Default)]
144pub struct CreateRoutingRuleParams {
145 pub name: String,
147 pub direction: Option<SipDirection>,
149 pub phone_numbers: Vec<String>,
151 pub rule: Option<RoutingRuleTarget>,
153 pub room: Option<SipRoomConfig>,
155 pub agent_id: Option<String>,
157 pub agent_metadata: Option<Map<String, Value>>,
160 pub include_headers: Option<SipIncludeHeaders>,
162 pub headers: HashMap<String, String>,
165 pub headers_to_attributes: HashMap<String, String>,
168 pub allowed_numbers: Vec<String>,
170 pub allowed_ip_addresses: Vec<String>,
172 pub tags: Vec<String>,
174 pub recording: Option<bool>,
176 pub dtmf: Option<bool>,
178 pub noise_cancellation: Option<bool>,
180 pub hide_phone_number: Option<bool>,
182 pub api_key: Option<String>,
184}
185
186#[derive(Debug, Clone, Default)]
189pub struct UpdateRoutingRuleParams {
190 pub name: Option<String>,
192 pub phone_numbers: Vec<String>,
194 pub rule: Option<RoutingRuleTarget>,
196 pub room: Option<SipRoomConfig>,
198 pub agent_id: Option<String>,
200 pub agent_metadata: Option<Map<String, Value>>,
202 pub include_headers: Option<SipIncludeHeaders>,
204 pub headers: HashMap<String, String>,
206 pub headers_to_attributes: HashMap<String, String>,
208 pub allowed_numbers: Vec<String>,
210 pub allowed_ip_addresses: Vec<String>,
212 pub tags: Vec<String>,
214 pub recording: Option<bool>,
216 pub dtmf: Option<bool>,
218 pub noise_cancellation: Option<bool>,
220 pub hide_phone_number: Option<bool>,
222 pub api_key: Option<String>,
224}
225
226#[derive(Debug, Default, Serialize)]
228#[serde(rename_all = "camelCase")]
229struct RoutingRuleWire {
230 #[serde(skip_serializing_if = "Option::is_none")]
231 name: Option<String>,
232 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
233 direction: Option<SipDirection>,
234 #[serde(skip_serializing_if = "Vec::is_empty")]
235 phone_numbers: Vec<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 room: Option<SipRoomConfig>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 agent_id: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 agent_metadata: Option<Map<String, Value>>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 include_headers: Option<SipIncludeHeaders>,
244 #[serde(skip_serializing_if = "HashMap::is_empty")]
245 headers: HashMap<String, String>,
246 #[serde(skip_serializing_if = "HashMap::is_empty")]
247 headers_to_attributes: HashMap<String, String>,
248 #[serde(skip_serializing_if = "Vec::is_empty")]
249 allowed_numbers: Vec<String>,
250 #[serde(rename = "allowedIpAddresses", skip_serializing_if = "Vec::is_empty")]
251 allowed_ip_addresses: Vec<String>,
252 #[serde(skip_serializing_if = "Vec::is_empty")]
253 tags: Vec<String>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 recording: Option<bool>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 dtmf: Option<bool>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 noise_cancellation: Option<bool>,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 hide_phone_number: Option<bool>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 api_key: Option<String>,
264}
265
266fn resolve_room(
268 room: Option<SipRoomConfig>,
269 rule: Option<RoutingRuleTarget>,
270) -> Option<SipRoomConfig> {
271 room.or_else(|| rule.map(SipRoomConfig::from))
272}
273
274impl From<CreateRoutingRuleParams> for RoutingRuleWire {
275 fn from(params: CreateRoutingRuleParams) -> Self {
276 Self {
277 name: Some(params.name).filter(|name| !name.is_empty()),
278 direction: params.direction,
279 phone_numbers: params.phone_numbers,
280 room: resolve_room(params.room, params.rule),
281 agent_id: params.agent_id,
282 agent_metadata: params.agent_metadata,
283 include_headers: params.include_headers,
284 headers: params.headers,
285 headers_to_attributes: params.headers_to_attributes,
286 allowed_numbers: params.allowed_numbers,
287 allowed_ip_addresses: params.allowed_ip_addresses,
288 tags: params.tags,
289 recording: params.recording,
290 dtmf: params.dtmf,
291 noise_cancellation: params.noise_cancellation,
292 hide_phone_number: params.hide_phone_number,
293 api_key: params.api_key,
294 }
295 }
296}
297
298impl From<UpdateRoutingRuleParams> for RoutingRuleWire {
299 fn from(params: UpdateRoutingRuleParams) -> Self {
300 Self {
301 name: params.name,
302 direction: None,
304 phone_numbers: params.phone_numbers,
305 room: resolve_room(params.room, params.rule),
306 agent_id: params.agent_id,
307 agent_metadata: params.agent_metadata,
308 include_headers: params.include_headers,
309 headers: params.headers,
310 headers_to_attributes: params.headers_to_attributes,
311 allowed_numbers: params.allowed_numbers,
312 allowed_ip_addresses: params.allowed_ip_addresses,
313 tags: params.tags,
314 recording: params.recording,
315 dtmf: params.dtmf,
316 noise_cancellation: params.noise_cancellation,
317 hide_phone_number: params.hide_phone_number,
318 api_key: params.api_key,
319 }
320 }
321}
322
323#[derive(Debug, Clone, Default)]
325pub struct SipRoutingRuleListParams {
326 pub page: Option<u32>,
328 pub per_page: Option<u32>,
330 pub cursor: Option<String>,
332 pub search: Option<String>,
334 pub direction: Option<SipDirection>,
336 pub rule_ids: Vec<String>,
338 pub room_type: Vec<SipRoomType>,
340}
341
342impl SipRoutingRuleListParams {
343 fn pagination(&self) -> ListParams {
344 ListParams {
345 page: self.page,
346 per_page: self.per_page,
347 cursor: self.cursor.clone(),
348 }
349 }
350}
351
352#[derive(Debug, Clone, Copy)]
354pub struct SipRoutingRulesResource<'a> {
355 client: &'a Client,
356}
357
358impl<'a> SipRoutingRulesResource<'a> {
359 pub(crate) fn new(client: &'a Client) -> Self {
360 Self { client }
361 }
362
363 pub async fn create(&self, params: CreateRoutingRuleParams) -> Result<SipRoutingRule> {
365 let body = RoutingRuleWire::from(params);
366 self.client
367 .json(Method::POST, PATH, CallOptions::json(&body)?)
368 .await
369 }
370
371 pub async fn list(&self, params: SipRoutingRuleListParams) -> Result<Page<SipRoutingRule>> {
373 paginate(self.fetcher(¶ms), ¶ms.pagination(), "data", None).await
374 }
375
376 pub fn list_stream(
378 &self,
379 params: SipRoutingRuleListParams,
380 ) -> impl Stream<Item = Result<SipRoutingRule>> + Send {
381 auto_page(self.fetcher(¶ms), params.pagination(), "data", None)
382 }
383
384 pub async fn get(&self, rule_id: &str) -> Result<SipRoutingRule> {
386 let path = format!("{PATH}/{}", escape(rule_id));
387 self.client
388 .json(Method::GET, &path, CallOptions::new())
389 .await
390 }
391
392 pub async fn update(
394 &self,
395 rule_id: &str,
396 params: UpdateRoutingRuleParams,
397 ) -> Result<SipRoutingRule> {
398 let body = RoutingRuleWire::from(params);
399 let path = format!("{PATH}/{}", escape(rule_id));
400 self.client
401 .json(Method::PATCH, &path, CallOptions::json(&body)?)
402 .await
403 }
404
405 pub async fn delete(&self, rule_id: &str) -> Result<MessageResponse> {
407 let path = format!("{PATH}/{}", escape(rule_id));
408 self.client
409 .json(Method::DELETE, &path, CallOptions::new())
410 .await
411 }
412
413 fn fetcher(&self, params: &SipRoutingRuleListParams) -> PageFetcher {
414 let client = self.client.clone();
415 let params = params.clone();
416 Arc::new(move |page, per_page| {
417 let client = client.clone();
418 let params = params.clone();
419 Box::pin(async move {
420 let query = QueryBuilder::new()
421 .opt("page", page)
422 .opt("perPage", per_page)
423 .opt_str("search", params.search.as_deref())
424 .opt_str("type", params.direction.as_ref().map(SipDirection::as_str))
425 .csv("ruleIds", ¶ms.rule_ids)
426 .csv("roomType", ¶ms.room_type)
427 .into_pairs();
428 client
429 .json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
430 .await
431 })
432 })
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use serde_json::json;
440
441 fn wire(params: CreateRoutingRuleParams) -> Value {
442 serde_json::to_value(RoutingRuleWire::from(params)).unwrap()
443 }
444
445 #[test]
446 fn a_direct_target_becomes_a_static_room() {
447 let body = wire(CreateRoutingRuleParams {
448 name: "rule".into(),
449 direction: Some(SipDirection::INBOUND),
450 rule: Some(RoutingRuleTarget::Direct {
451 room_id: "abcd-efgh".into(),
452 pin: Some("1234".into()),
453 }),
454 ..Default::default()
455 });
456 assert_eq!(
457 body,
458 json!({
459 "name": "rule",
460 "type": "inbound",
461 "room": {"type": "static", "id": "abcd-efgh", "pin": "1234"},
462 })
463 );
464 }
465
466 #[test]
467 fn an_individual_target_becomes_a_dynamic_room_defaulting_to_blank() {
468 let body = wire(CreateRoutingRuleParams {
469 name: "rule".into(),
470 rule: Some(RoutingRuleTarget::Individual {
471 room_prefix: None,
472 pin: None,
473 }),
474 ..Default::default()
475 });
476 assert_eq!(body["room"], json!({"type": "dynamic", "prefix": "blank"}));
477
478 let body = wire(CreateRoutingRuleParams {
479 name: "rule".into(),
480 rule: Some(RoutingRuleTarget::Individual {
481 room_prefix: Some(SipRoomPrefix::SIP_NUMBER),
482 pin: None,
483 }),
484 ..Default::default()
485 });
486 assert_eq!(
487 body["room"],
488 json!({"type": "dynamic", "prefix": "sipNumber"})
489 );
490 }
491
492 #[test]
493 fn an_explicit_room_wins_over_the_rule_sugar() {
494 let body = wire(CreateRoutingRuleParams {
495 name: "rule".into(),
496 room: Some(SipRoomConfig {
497 kind: Some(SipRoomType::STATIC),
498 id: Some("explicit".into()),
499 ..Default::default()
500 }),
501 rule: Some(RoutingRuleTarget::Direct {
502 room_id: "sugar".into(),
503 pin: None,
504 }),
505 ..Default::default()
506 });
507 assert_eq!(body["room"]["id"], json!("explicit"));
508 }
509
510 #[test]
511 fn an_update_never_sends_the_direction() {
512 let body = serde_json::to_value(RoutingRuleWire::from(UpdateRoutingRuleParams {
513 name: Some("renamed".into()),
514 ..Default::default()
515 }))
516 .unwrap();
517 assert_eq!(body, json!({"name": "renamed"}));
518 assert!(body.get("type").is_none(), "direction is fixed at creation");
519 }
520
521 #[test]
522 fn empty_collections_are_omitted() {
523 let body = wire(CreateRoutingRuleParams {
524 name: "rule".into(),
525 ..Default::default()
526 });
527 assert_eq!(body, json!({"name": "rule"}));
528 }
529
530 #[test]
531 fn allowed_ip_addresses_keeps_its_lowercase_p() {
532 let body = wire(CreateRoutingRuleParams {
533 name: "rule".into(),
534 allowed_ip_addresses: vec!["1.2.3.4".into()],
535 ..Default::default()
536 });
537 assert_eq!(body["allowedIpAddresses"], json!(["1.2.3.4"]));
538 }
539}