1use std::fmt;
5use std::str::FromStr;
6use std::time::Duration;
7
8use super::{originate_quote, originate_split, originate_unquote};
9
10pub use super::variables::{Variables, VariablesType};
11
12const UNDEF: &str = "undef";
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
23#[non_exhaustive]
24pub enum DialplanType {
25 Inline,
27 Xml,
29}
30
31impl fmt::Display for DialplanType {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 Self::Inline => f.write_str("inline"),
35 Self::Xml => f.write_str("XML"),
36 }
37 }
38}
39
40parse_error! { ParseDialplanTypeError("dialplan type"); }
41
42impl FromStr for DialplanType {
43 type Err = ParseDialplanTypeError;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 if s.eq_ignore_ascii_case("inline") {
47 Ok(Self::Inline)
48 } else if s.eq_ignore_ascii_case("xml") {
49 Ok(Self::Xml)
50 } else {
51 Err(ParseDialplanTypeError(s.to_string()))
52 }
53 }
54}
55
56pub use super::endpoint::Endpoint;
58
59#[derive(Debug, Clone, PartialEq, Eq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct Application {
67 name: String,
68 #[cfg_attr(
69 feature = "serde",
70 serde(default, skip_serializing_if = "Option::is_none")
71 )]
72 args: Option<String>,
73}
74
75impl Application {
76 pub fn new(name: impl Into<String>, args: Option<impl Into<String>>) -> Self {
78 Self {
79 name: name.into(),
80 args: args.map(|a| a.into()),
81 }
82 }
83
84 pub fn simple(name: impl Into<String>) -> Self {
86 Self {
87 name: name.into(),
88 args: None,
89 }
90 }
91
92 pub fn park() -> Self {
94 Self::simple("park")
95 }
96
97 pub fn name(&self) -> &str {
99 &self.name
100 }
101
102 pub fn args(&self) -> Option<&str> {
104 self.args
105 .as_deref()
106 }
107
108 pub fn name_mut(&mut self) -> &mut String {
110 &mut self.name
111 }
112
113 pub fn args_mut(&mut self) -> &mut Option<String> {
115 &mut self.args
116 }
117
118 pub fn to_string_with_dialplan(&self, dialplan: &DialplanType) -> String {
120 match dialplan {
121 DialplanType::Inline => match &self.args {
122 Some(args) => format!("{}:{}", self.name, args),
123 None => self
124 .name
125 .clone(),
126 },
127 _ => {
129 let args = self
130 .args
131 .as_deref()
132 .unwrap_or("");
133 format!("&{}({})", self.name, args)
134 }
135 }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
148#[non_exhaustive]
149pub enum OriginateTarget {
150 Extension(String),
152 Application(Application),
154 InlineApplications(Vec<Application>),
156}
157
158impl From<Application> for OriginateTarget {
159 fn from(app: Application) -> Self {
160 Self::Application(app)
161 }
162}
163
164impl From<Vec<Application>> for OriginateTarget {
165 fn from(apps: Vec<Application>) -> Self {
166 Self::InlineApplications(apps)
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct Originate {
191 endpoint: Endpoint,
192 target: OriginateTarget,
193 dialplan: Option<DialplanType>,
194 context: Option<String>,
195 cid_name: Option<String>,
196 cid_num: Option<String>,
197 timeout: Option<Duration>,
198}
199
200#[cfg(feature = "serde")]
201mod serde_support {
202 use super::*;
203
204 #[derive(serde::Serialize, serde::Deserialize)]
206 pub(super) struct OriginateRaw {
207 pub endpoint: Endpoint,
208 #[serde(flatten)]
209 pub target: OriginateTarget,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub dialplan: Option<DialplanType>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub context: Option<String>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub cid_name: Option<String>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub cid_num: Option<String>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub timeout_secs: Option<u64>,
220 }
221
222 impl TryFrom<OriginateRaw> for Originate {
223 type Error = OriginateError;
224
225 fn try_from(raw: OriginateRaw) -> Result<Self, Self::Error> {
226 if matches!(raw.target, OriginateTarget::Extension(_))
227 && matches!(raw.dialplan, Some(DialplanType::Inline))
228 {
229 return Err(OriginateError::ExtensionWithInlineDialplan);
230 }
231 if let OriginateTarget::InlineApplications(ref apps) = raw.target {
232 if apps.is_empty() {
233 return Err(OriginateError::EmptyInlineApplications);
234 }
235 }
236 Ok(Self {
237 endpoint: raw.endpoint,
238 target: raw.target,
239 dialplan: raw.dialplan,
240 context: raw.context,
241 cid_name: raw.cid_name,
242 cid_num: raw.cid_num,
243 timeout: raw
244 .timeout_secs
245 .map(Duration::from_secs),
246 })
247 }
248 }
249
250 impl From<Originate> for OriginateRaw {
251 fn from(o: Originate) -> Self {
252 Self {
253 endpoint: o.endpoint,
254 target: o.target,
255 dialplan: o.dialplan,
256 context: o.context,
257 cid_name: o.cid_name,
258 cid_num: o.cid_num,
259 timeout_secs: o
260 .timeout
261 .map(|d| d.as_secs()),
262 }
263 }
264 }
265
266 impl serde::Serialize for Originate {
267 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
268 OriginateRaw::from(self.clone()).serialize(serializer)
269 }
270 }
271
272 impl<'de> serde::Deserialize<'de> for Originate {
273 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
274 let raw = OriginateRaw::deserialize(deserializer)?;
275 Originate::try_from(raw).map_err(serde::de::Error::custom)
276 }
277 }
278}
279
280impl Originate {
281 pub fn extension(endpoint: Endpoint, extension: impl Into<String>) -> Self {
283 Self {
284 endpoint,
285 target: OriginateTarget::Extension(extension.into()),
286 dialplan: None,
287 context: None,
288 cid_name: None,
289 cid_num: None,
290 timeout: None,
291 }
292 }
293
294 pub fn application(endpoint: Endpoint, app: Application) -> Self {
296 Self {
297 endpoint,
298 target: OriginateTarget::Application(app),
299 dialplan: None,
300 context: None,
301 cid_name: None,
302 cid_num: None,
303 timeout: None,
304 }
305 }
306
307 pub fn inline(
311 endpoint: Endpoint,
312 apps: impl IntoIterator<Item = Application>,
313 ) -> Result<Self, OriginateError> {
314 let apps: Vec<Application> = apps
315 .into_iter()
316 .collect();
317 if apps.is_empty() {
318 return Err(OriginateError::EmptyInlineApplications);
319 }
320 Ok(Self {
321 endpoint,
322 target: OriginateTarget::InlineApplications(apps),
323 dialplan: None,
324 context: None,
325 cid_name: None,
326 cid_num: None,
327 timeout: None,
328 })
329 }
330
331 pub fn dialplan(mut self, dp: DialplanType) -> Result<Self, OriginateError> {
335 if matches!(self.target, OriginateTarget::Extension(_)) && dp == DialplanType::Inline {
336 return Err(OriginateError::ExtensionWithInlineDialplan);
337 }
338 self.dialplan = Some(dp);
339 Ok(self)
340 }
341
342 pub fn context(mut self, ctx: impl Into<String>) -> Self {
344 self.context = Some(ctx.into());
345 self
346 }
347
348 pub fn cid_name(mut self, name: impl Into<String>) -> Self {
350 self.cid_name = Some(name.into());
351 self
352 }
353
354 pub fn cid_num(mut self, num: impl Into<String>) -> Self {
356 self.cid_num = Some(num.into());
357 self
358 }
359
360 pub fn timeout(mut self, duration: Duration) -> Self {
363 self.timeout = Some(duration);
364 self
365 }
366
367 pub fn endpoint(&self) -> &Endpoint {
369 &self.endpoint
370 }
371
372 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
374 &mut self.endpoint
375 }
376
377 pub fn target(&self) -> &OriginateTarget {
379 &self.target
380 }
381
382 pub fn target_mut(&mut self) -> &mut OriginateTarget {
384 &mut self.target
385 }
386
387 pub fn dialplan_type(&self) -> Option<&DialplanType> {
389 self.dialplan
390 .as_ref()
391 }
392
393 pub fn context_str(&self) -> Option<&str> {
395 self.context
396 .as_deref()
397 }
398
399 pub fn caller_id_name(&self) -> Option<&str> {
401 self.cid_name
402 .as_deref()
403 }
404
405 pub fn caller_id_number(&self) -> Option<&str> {
407 self.cid_num
408 .as_deref()
409 }
410
411 pub fn timeout_duration(&self) -> Option<Duration> {
413 self.timeout
414 }
415
416 pub fn timeout_seconds(&self) -> Option<u64> {
418 self.timeout
419 .map(|d| d.as_secs())
420 }
421
422 pub fn set_dialplan(&mut self, dp: Option<DialplanType>) {
424 self.dialplan = dp;
425 }
426
427 pub fn set_context(&mut self, ctx: Option<impl Into<String>>) {
429 self.context = ctx.map(|c| c.into());
430 }
431
432 pub fn set_cid_name(&mut self, name: Option<impl Into<String>>) {
434 self.cid_name = name.map(|n| n.into());
435 }
436
437 pub fn set_cid_num(&mut self, num: Option<impl Into<String>>) {
439 self.cid_num = num.map(|n| n.into());
440 }
441
442 pub fn set_timeout(&mut self, timeout: Option<Duration>) {
444 self.timeout = timeout;
445 }
446}
447
448impl fmt::Display for Originate {
449 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450 let target_str = match &self.target {
451 OriginateTarget::Extension(ext) => ext.clone(),
452 OriginateTarget::Application(app) => app.to_string_with_dialplan(&DialplanType::Xml),
453 OriginateTarget::InlineApplications(apps) => {
454 let parts: Vec<String> = apps
456 .iter()
457 .map(|a| a.to_string_with_dialplan(&DialplanType::Inline))
458 .collect();
459 parts.join(",")
460 }
461 };
462
463 write!(
464 f,
465 "originate {} {}",
466 self.endpoint,
467 originate_quote(&target_str)
468 )?;
469
470 let dialplan = match &self.target {
474 OriginateTarget::InlineApplications(_) => Some(
475 self.dialplan
476 .unwrap_or(DialplanType::Inline),
477 ),
478 _ => self.dialplan,
479 };
480 let has_ctx = self
481 .context
482 .is_some();
483 let has_name = self
484 .cid_name
485 .is_some();
486 let has_num = self
487 .cid_num
488 .is_some();
489 let has_timeout = self
490 .timeout
491 .is_some();
492
493 if dialplan.is_some() || has_ctx || has_name || has_num || has_timeout {
495 let dp = dialplan
496 .as_ref()
497 .cloned()
498 .unwrap_or(DialplanType::Xml);
499 write!(f, " {}", dp)?;
500 }
501 if has_ctx || has_name || has_num || has_timeout {
502 write!(
503 f,
504 " {}",
505 self.context
506 .as_deref()
507 .unwrap_or("default")
508 )?;
509 }
510 if has_name || has_num || has_timeout {
511 let name = self
512 .cid_name
513 .as_deref()
514 .unwrap_or(UNDEF);
515 write!(f, " {}", originate_quote(name))?;
516 }
517 if has_num || has_timeout {
518 let num = self
519 .cid_num
520 .as_deref()
521 .unwrap_or(UNDEF);
522 write!(f, " {}", originate_quote(num))?;
523 }
524 if let Some(ref timeout) = self.timeout {
525 write!(f, " {}", timeout.as_secs())?;
526 }
527 Ok(())
528 }
529}
530
531impl FromStr for Originate {
532 type Err = OriginateError;
533
534 fn from_str(s: &str) -> Result<Self, Self::Err> {
535 let s = s
536 .strip_prefix("originate")
537 .unwrap_or(s)
538 .trim();
539 let mut args = originate_split(s, ' ')?;
540
541 if args.is_empty() {
542 return Err(OriginateError::ParseError("empty originate".into()));
543 }
544
545 let endpoint_str = args.remove(0);
546 let endpoint: Endpoint = endpoint_str.parse()?;
547
548 if args.is_empty() {
549 return Err(OriginateError::ParseError(
550 "missing target in originate".into(),
551 ));
552 }
553
554 let target_str = originate_unquote(&args.remove(0));
555
556 let dialplan = args
557 .first()
558 .and_then(|s| {
559 s.parse::<DialplanType>()
560 .ok()
561 });
562 if dialplan.is_some() {
563 args.remove(0);
564 }
565
566 let target = super::parse_originate_target(&target_str, dialplan.as_ref())?;
567
568 let context = if !args.is_empty() {
569 Some(args.remove(0))
570 } else {
571 None
572 };
573 let cid_name = if !args.is_empty() {
574 let v = args.remove(0);
575 if v.eq_ignore_ascii_case(UNDEF) {
576 None
577 } else {
578 Some(v)
579 }
580 } else {
581 None
582 };
583 let cid_num = if !args.is_empty() {
584 let v = args.remove(0);
585 if v.eq_ignore_ascii_case(UNDEF) {
586 None
587 } else {
588 Some(v)
589 }
590 } else {
591 None
592 };
593 let timeout = if !args.is_empty() {
594 Some(Duration::from_secs(
595 args.remove(0)
596 .parse::<u64>()
597 .map_err(|e| OriginateError::ParseError(format!("invalid timeout: {}", e)))?,
598 ))
599 } else {
600 None
601 };
602
603 let mut orig = match target {
605 OriginateTarget::Extension(ref ext) => Self::extension(endpoint, ext.clone()),
606 OriginateTarget::Application(ref app) => Self::application(endpoint, app.clone()),
607 OriginateTarget::InlineApplications(ref apps) => Self::inline(endpoint, apps.clone())?,
608 };
609 orig.dialplan = dialplan;
610 orig.context = context;
611 orig.cid_name = cid_name;
612 orig.cid_num = cid_num;
613 orig.timeout = timeout;
614 Ok(orig)
615 }
616}
617
618#[derive(Debug, Clone, PartialEq, Eq)]
620#[non_exhaustive]
621pub enum OriginateError {
622 UnclosedQuote(String),
624 ParseError(String),
626 EmptyInlineApplications,
628 ExtensionWithInlineDialplan,
630}
631
632impl std::fmt::Display for OriginateError {
633 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
634 match self {
635 Self::UnclosedQuote(s) => write!(f, "unclosed quote at: {s}"),
636 Self::ParseError(s) => write!(f, "parse error: {s}"),
637 Self::EmptyInlineApplications => {
638 f.write_str("inline originate requires at least one application")
639 }
640 Self::ExtensionWithInlineDialplan => {
641 f.write_str("extension target is incompatible with inline dialplan")
642 }
643 }
644 }
645}
646
647impl std::error::Error for OriginateError {}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652 use crate::commands::endpoint::{LoopbackEndpoint, SofiaEndpoint, SofiaGateway};
653
654 #[test]
657 fn endpoint_uri_only() {
658 let ep = Endpoint::Sofia(SofiaEndpoint {
659 profile: "internal".into(),
660 destination: "123@example.com".into(),
661 variables: None,
662 });
663 assert_eq!(ep.to_string(), "sofia/internal/123@example.com");
664 }
665
666 #[test]
667 fn endpoint_uri_with_variable() {
668 let mut vars = Variables::new(VariablesType::Default);
669 vars.insert("one_variable", "1");
670 let ep = Endpoint::Sofia(SofiaEndpoint {
671 profile: "internal".into(),
672 destination: "123@example.com".into(),
673 variables: Some(vars),
674 });
675 assert_eq!(
676 ep.to_string(),
677 "{one_variable=1}sofia/internal/123@example.com"
678 );
679 }
680
681 #[test]
682 fn endpoint_variable_with_quote() {
683 let mut vars = Variables::new(VariablesType::Default);
684 vars.insert("one_variable", "one'quote");
685 let ep = Endpoint::Sofia(SofiaEndpoint {
686 profile: "internal".into(),
687 destination: "123@example.com".into(),
688 variables: Some(vars),
689 });
690 assert_eq!(
691 ep.to_string(),
692 "{one_variable=one\\'quote}sofia/internal/123@example.com"
693 );
694 }
695
696 #[test]
697 fn loopback_endpoint_display() {
698 let mut vars = Variables::new(VariablesType::Default);
699 vars.insert("one_variable", "1");
700 let ep = Endpoint::Loopback(
701 LoopbackEndpoint::new("aUri")
702 .with_context("aContext")
703 .with_variables(vars),
704 );
705 assert_eq!(ep.to_string(), "{one_variable=1}loopback/aUri/aContext");
706 }
707
708 #[test]
709 fn sofia_gateway_endpoint_display() {
710 let mut vars = Variables::new(VariablesType::Default);
711 vars.insert("one_variable", "1");
712 let ep = Endpoint::SofiaGateway(SofiaGateway {
713 destination: "aUri".into(),
714 profile: None,
715 gateway: "internal".into(),
716 variables: Some(vars),
717 });
718 assert_eq!(
719 ep.to_string(),
720 "{one_variable=1}sofia/gateway/internal/aUri"
721 );
722 }
723
724 #[test]
727 fn application_xml_format() {
728 let app = Application::new("testApp", Some("testArg"));
729 assert_eq!(
730 app.to_string_with_dialplan(&DialplanType::Xml),
731 "&testApp(testArg)"
732 );
733 }
734
735 #[test]
736 fn application_inline_format() {
737 let app = Application::new("testApp", Some("testArg"));
738 assert_eq!(
739 app.to_string_with_dialplan(&DialplanType::Inline),
740 "testApp:testArg"
741 );
742 }
743
744 #[test]
745 fn application_inline_no_args() {
746 let app = Application::simple("park");
747 assert_eq!(app.to_string_with_dialplan(&DialplanType::Inline), "park");
748 }
749
750 #[test]
753 fn originate_xml_display() {
754 let ep = Endpoint::Sofia(SofiaEndpoint {
755 profile: "internal".into(),
756 destination: "123@example.com".into(),
757 variables: None,
758 });
759 let orig = Originate::application(ep, Application::new("conference", Some("1")))
760 .dialplan(DialplanType::Xml)
761 .unwrap();
762 assert_eq!(
763 orig.to_string(),
764 "originate sofia/internal/123@example.com &conference(1) XML"
765 );
766 }
767
768 #[test]
769 fn originate_inline_display() {
770 let ep = Endpoint::Sofia(SofiaEndpoint {
771 profile: "internal".into(),
772 destination: "123@example.com".into(),
773 variables: None,
774 });
775 let orig = Originate::inline(ep, vec![Application::new("conference", Some("1"))])
776 .unwrap()
777 .dialplan(DialplanType::Inline)
778 .unwrap();
779 assert_eq!(
780 orig.to_string(),
781 "originate sofia/internal/123@example.com conference:1 inline"
782 );
783 }
784
785 #[test]
786 fn originate_extension_display() {
787 let ep = Endpoint::Sofia(SofiaEndpoint {
788 profile: "internal".into(),
789 destination: "123@example.com".into(),
790 variables: None,
791 });
792 let orig = Originate::extension(ep, "1000")
793 .dialplan(DialplanType::Xml)
794 .unwrap()
795 .context("default");
796 assert_eq!(
797 orig.to_string(),
798 "originate sofia/internal/123@example.com 1000 XML default"
799 );
800 }
801
802 #[test]
803 fn originate_extension_round_trip() {
804 let input = "originate sofia/internal/test@example.com 1000 XML default";
805 let parsed: Originate = input
806 .parse()
807 .unwrap();
808 assert_eq!(parsed.to_string(), input);
809 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
810 }
811
812 #[test]
813 fn originate_extension_no_dialplan() {
814 let input = "originate sofia/internal/test@example.com 1000";
815 let parsed: Originate = input
816 .parse()
817 .unwrap();
818 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
819 assert_eq!(parsed.to_string(), input);
820 }
821
822 #[test]
823 fn originate_extension_with_inline_errors() {
824 let ep = Endpoint::Sofia(SofiaEndpoint {
825 profile: "internal".into(),
826 destination: "123@example.com".into(),
827 variables: None,
828 });
829 let result = Originate::extension(ep, "1000").dialplan(DialplanType::Inline);
830 assert!(result.is_err());
831 }
832
833 #[test]
834 fn originate_empty_inline_errors() {
835 let ep = Endpoint::Sofia(SofiaEndpoint {
836 profile: "internal".into(),
837 destination: "123@example.com".into(),
838 variables: None,
839 });
840 let result = Originate::inline(ep, vec![]);
841 assert!(result.is_err());
842 }
843
844 #[test]
845 fn originate_from_string_round_trip() {
846 let input = "originate {test='variable with quote'}sofia/internal/test@example.com 123";
847 let orig: Originate = input
848 .parse()
849 .unwrap();
850 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "123"));
851 assert_eq!(orig.to_string(), input);
852 }
853
854 #[test]
855 fn originate_socket_app_quoted() {
856 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
857 let orig = Originate::application(
858 ep,
859 Application::new("socket", Some("127.0.0.1:8040 async full")),
860 );
861 assert_eq!(
862 orig.to_string(),
863 "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'"
864 );
865 }
866
867 #[test]
868 fn originate_socket_round_trip() {
869 let input = "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'";
870 let parsed: Originate = input
871 .parse()
872 .unwrap();
873 assert_eq!(parsed.to_string(), input);
874 if let OriginateTarget::Application(ref app) = parsed.target() {
875 assert_eq!(app.args(), Some("127.0.0.1:8040 async full"));
876 } else {
877 panic!("expected Application target");
878 }
879 }
880
881 #[test]
882 fn originate_display_round_trip() {
883 let ep = Endpoint::Sofia(SofiaEndpoint {
884 profile: "internal".into(),
885 destination: "123@example.com".into(),
886 variables: None,
887 });
888 let orig = Originate::application(ep, Application::new("conference", Some("1")))
889 .dialplan(DialplanType::Xml)
890 .unwrap();
891 let s = orig.to_string();
892 let parsed: Originate = s
893 .parse()
894 .unwrap();
895 assert_eq!(parsed.to_string(), s);
896 }
897
898 #[test]
899 fn originate_inline_no_args_round_trip() {
900 let input = "originate sofia/internal/123@example.com park inline";
901 let parsed: Originate = input
902 .parse()
903 .unwrap();
904 assert_eq!(parsed.to_string(), input);
905 if let OriginateTarget::InlineApplications(ref apps) = parsed.target() {
906 assert!(apps[0]
907 .args()
908 .is_none());
909 } else {
910 panic!("expected InlineApplications target");
911 }
912 }
913
914 #[test]
915 fn originate_inline_multi_app_round_trip() {
916 let input =
917 "originate sofia/internal/123@example.com playback:/tmp/test.wav,hangup:NORMAL_CLEARING inline";
918 let parsed: Originate = input
919 .parse()
920 .unwrap();
921 assert_eq!(parsed.to_string(), input);
922 }
923
924 #[test]
925 fn originate_inline_auto_dialplan() {
926 let ep = Endpoint::Sofia(SofiaEndpoint {
927 profile: "internal".into(),
928 destination: "123@example.com".into(),
929 variables: None,
930 });
931 let orig = Originate::inline(ep, vec![Application::simple("park")]).unwrap();
932 assert!(orig
933 .to_string()
934 .contains("inline"));
935 }
936
937 #[test]
940 fn dialplan_type_display() {
941 assert_eq!(DialplanType::Inline.to_string(), "inline");
942 assert_eq!(DialplanType::Xml.to_string(), "XML");
943 }
944
945 #[test]
946 fn dialplan_type_from_str() {
947 assert_eq!(
948 "inline"
949 .parse::<DialplanType>()
950 .unwrap(),
951 DialplanType::Inline
952 );
953 assert_eq!(
954 "XML"
955 .parse::<DialplanType>()
956 .unwrap(),
957 DialplanType::Xml
958 );
959 }
960
961 #[test]
962 fn dialplan_type_from_str_case_insensitive() {
963 assert_eq!(
964 "xml"
965 .parse::<DialplanType>()
966 .unwrap(),
967 DialplanType::Xml
968 );
969 assert_eq!(
970 "Xml"
971 .parse::<DialplanType>()
972 .unwrap(),
973 DialplanType::Xml
974 );
975 assert_eq!(
976 "INLINE"
977 .parse::<DialplanType>()
978 .unwrap(),
979 DialplanType::Inline
980 );
981 assert_eq!(
982 "Inline"
983 .parse::<DialplanType>()
984 .unwrap(),
985 DialplanType::Inline
986 );
987 }
988
989 #[test]
992 fn serde_dialplan_type_xml() {
993 let json = serde_json::to_string(&DialplanType::Xml).unwrap();
994 assert_eq!(json, "\"xml\"");
995 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
996 assert_eq!(parsed, DialplanType::Xml);
997 }
998
999 #[test]
1000 fn serde_dialplan_type_inline() {
1001 let json = serde_json::to_string(&DialplanType::Inline).unwrap();
1002 assert_eq!(json, "\"inline\"");
1003 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1004 assert_eq!(parsed, DialplanType::Inline);
1005 }
1006
1007 #[test]
1008 fn serde_application() {
1009 let app = Application::new("park", None::<&str>);
1010 let json = serde_json::to_string(&app).unwrap();
1011 let parsed: Application = serde_json::from_str(&json).unwrap();
1012 assert_eq!(parsed, app);
1013 }
1014
1015 #[test]
1016 fn serde_application_with_args() {
1017 let app = Application::new("conference", Some("1"));
1018 let json = serde_json::to_string(&app).unwrap();
1019 let parsed: Application = serde_json::from_str(&json).unwrap();
1020 assert_eq!(parsed, app);
1021 }
1022
1023 #[test]
1024 fn serde_application_skips_none_args() {
1025 let app = Application::new("park", None::<&str>);
1026 let json = serde_json::to_string(&app).unwrap();
1027 assert!(!json.contains("args"));
1028 }
1029
1030 #[test]
1031 fn serde_originate_application_round_trip() {
1032 let ep = Endpoint::Sofia(SofiaEndpoint {
1033 profile: "internal".into(),
1034 destination: "123@example.com".into(),
1035 variables: None,
1036 });
1037 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1038 .dialplan(DialplanType::Xml)
1039 .unwrap()
1040 .context("default")
1041 .cid_name("Test")
1042 .cid_num("5551234")
1043 .timeout(Duration::from_secs(30));
1044 let json = serde_json::to_string(&orig).unwrap();
1045 assert!(json.contains("\"application\""));
1046 let parsed: Originate = serde_json::from_str(&json).unwrap();
1047 assert_eq!(parsed, orig);
1048 }
1049
1050 #[test]
1051 fn serde_originate_extension() {
1052 let json = r#"{
1053 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1054 "extension": "1000",
1055 "dialplan": "xml",
1056 "context": "default"
1057 }"#;
1058 let orig: Originate = serde_json::from_str(json).unwrap();
1059 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1060 assert_eq!(
1061 orig.to_string(),
1062 "originate sofia/internal/123@example.com 1000 XML default"
1063 );
1064 }
1065
1066 #[test]
1067 fn serde_originate_extension_with_inline_rejected() {
1068 let json = r#"{
1069 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1070 "extension": "1000",
1071 "dialplan": "inline"
1072 }"#;
1073 let result = serde_json::from_str::<Originate>(json);
1074 assert!(result.is_err());
1075 }
1076
1077 #[test]
1078 fn serde_originate_empty_inline_rejected() {
1079 let json = r#"{
1080 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1081 "inline_applications": []
1082 }"#;
1083 let result = serde_json::from_str::<Originate>(json);
1084 assert!(result.is_err());
1085 }
1086
1087 #[test]
1088 fn serde_originate_inline_applications() {
1089 let json = r#"{
1090 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1091 "inline_applications": [
1092 {"name": "playback", "args": "/tmp/test.wav"},
1093 {"name": "hangup", "args": "NORMAL_CLEARING"}
1094 ]
1095 }"#;
1096 let orig: Originate = serde_json::from_str(json).unwrap();
1097 if let OriginateTarget::InlineApplications(ref apps) = orig.target() {
1098 assert_eq!(apps.len(), 2);
1099 } else {
1100 panic!("expected InlineApplications");
1101 }
1102 assert!(orig
1103 .to_string()
1104 .contains("inline"));
1105 }
1106
1107 #[test]
1108 fn serde_originate_skips_none_fields() {
1109 let ep = Endpoint::Sofia(SofiaEndpoint {
1110 profile: "internal".into(),
1111 destination: "123@example.com".into(),
1112 variables: None,
1113 });
1114 let orig = Originate::application(ep, Application::new("park", None::<&str>));
1115 let json = serde_json::to_string(&orig).unwrap();
1116 assert!(!json.contains("dialplan"));
1117 assert!(!json.contains("context"));
1118 assert!(!json.contains("cid_name"));
1119 assert!(!json.contains("cid_num"));
1120 assert!(!json.contains("timeout"));
1121 }
1122
1123 #[test]
1124 fn serde_originate_to_wire_format() {
1125 let json = r#"{
1126 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1127 "application": {"name": "park"},
1128 "dialplan": "xml",
1129 "context": "default"
1130 }"#;
1131 let orig: Originate = serde_json::from_str(json).unwrap();
1132 let wire = orig.to_string();
1133 assert!(wire.starts_with("originate"));
1134 assert!(wire.contains("sofia/internal/123@example.com"));
1135 assert!(wire.contains("&park()"));
1136 assert!(wire.contains("XML"));
1137 }
1138
1139 #[test]
1142 fn application_simple_no_args() {
1143 let app = Application::simple("park");
1144 assert_eq!(app.name(), "park");
1145 assert!(app
1146 .args()
1147 .is_none());
1148 }
1149
1150 #[test]
1151 fn application_simple_xml_format() {
1152 let app = Application::simple("park");
1153 assert_eq!(app.to_string_with_dialplan(&DialplanType::Xml), "&park()");
1154 }
1155
1156 #[test]
1159 fn originate_target_from_application() {
1160 let target: OriginateTarget = Application::simple("park").into();
1161 assert!(matches!(target, OriginateTarget::Application(_)));
1162 }
1163
1164 #[test]
1165 fn originate_target_from_vec() {
1166 let target: OriginateTarget = vec![
1167 Application::new("conference", Some("1")),
1168 Application::new("hangup", Some("NORMAL_CLEARING")),
1169 ]
1170 .into();
1171 if let OriginateTarget::InlineApplications(apps) = target {
1172 assert_eq!(apps.len(), 2);
1173 } else {
1174 panic!("expected InlineApplications");
1175 }
1176 }
1177
1178 #[test]
1179 fn originate_target_application_wire_format() {
1180 let ep = Endpoint::Sofia(SofiaEndpoint {
1181 profile: "internal".into(),
1182 destination: "123@example.com".into(),
1183 variables: None,
1184 });
1185 let orig = Originate::application(ep, Application::simple("park"));
1186 assert_eq!(
1187 orig.to_string(),
1188 "originate sofia/internal/123@example.com &park()"
1189 );
1190 }
1191
1192 #[test]
1193 fn originate_timeout_only_fills_positional_gaps() {
1194 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1195 let cmd = Originate::application(ep, Application::simple("park"))
1196 .timeout(Duration::from_secs(30));
1197 assert_eq!(
1200 cmd.to_string(),
1201 "originate loopback/9199/test &park() XML default undef undef 30"
1202 );
1203 }
1204
1205 #[test]
1206 fn originate_cid_num_only_fills_preceding_gaps() {
1207 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1208 let cmd = Originate::application(ep, Application::simple("park")).cid_num("5551234");
1209 assert_eq!(
1210 cmd.to_string(),
1211 "originate loopback/9199/test &park() XML default undef 5551234"
1212 );
1213 }
1214
1215 #[test]
1216 fn originate_context_only_fills_dialplan() {
1217 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1218 let cmd = Originate::extension(ep, "1000").context("myctx");
1219 assert_eq!(
1220 cmd.to_string(),
1221 "originate loopback/9199/test 1000 XML myctx"
1222 );
1223 }
1224
1225 #[test]
1231 fn originate_context_gap_filler_round_trip_asymmetry() {
1232 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1233 let cmd = Originate::application(ep, Application::simple("park")).cid_name("Alice");
1234 let wire = cmd.to_string();
1235 assert!(wire.contains("default"), "gap-filler should emit 'default'");
1236
1237 let parsed: Originate = wire
1238 .parse()
1239 .unwrap();
1240 assert_eq!(parsed.context_str(), Some("default"));
1242
1243 assert_eq!(parsed.to_string(), wire);
1245 }
1246
1247 #[test]
1250 fn serde_originate_full_round_trip_with_variables() {
1251 let mut ep_vars = Variables::new(VariablesType::Default);
1252 ep_vars.insert("originate_timeout", "30");
1253 ep_vars.insert("sip_h_X-Custom", "value with spaces");
1254 let ep = Endpoint::SofiaGateway(SofiaGateway {
1255 gateway: "my_provider".into(),
1256 destination: "18005551234".into(),
1257 profile: Some("external".into()),
1258 variables: Some(ep_vars),
1259 });
1260 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1261 .dialplan(DialplanType::Xml)
1262 .unwrap()
1263 .context("public")
1264 .cid_name("Test Caller")
1265 .cid_num("5551234")
1266 .timeout(Duration::from_secs(60));
1267 let json = serde_json::to_string(&orig).unwrap();
1268 let parsed: Originate = serde_json::from_str(&json).unwrap();
1269 assert_eq!(parsed, orig);
1270 assert_eq!(parsed.to_string(), orig.to_string());
1271 }
1272
1273 #[test]
1274 fn serde_originate_inline_round_trip_with_all_fields() {
1275 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1276 let orig = Originate::inline(
1277 ep,
1278 vec![
1279 Application::new("playback", Some("/tmp/test.wav")),
1280 Application::new("hangup", Some("NORMAL_CLEARING")),
1281 ],
1282 )
1283 .unwrap()
1284 .dialplan(DialplanType::Inline)
1285 .unwrap()
1286 .context("default")
1287 .cid_name("IVR")
1288 .cid_num("0000")
1289 .timeout(Duration::from_secs(45));
1290 let json = serde_json::to_string(&orig).unwrap();
1291 let parsed: Originate = serde_json::from_str(&json).unwrap();
1292 assert_eq!(parsed, orig);
1293 assert_eq!(parsed.to_string(), orig.to_string());
1294 }
1295
1296 #[test]
1299 fn originate_context_named_inline() {
1300 let ep = Endpoint::Sofia(SofiaEndpoint {
1301 profile: "internal".into(),
1302 destination: "123@example.com".into(),
1303 variables: None,
1304 });
1305 let orig = Originate::extension(ep, "1000")
1306 .dialplan(DialplanType::Xml)
1307 .unwrap()
1308 .context("inline");
1309 let wire = orig.to_string();
1310 assert!(wire.contains("XML inline"), "wire: {}", wire);
1311 let parsed: Originate = wire
1312 .parse()
1313 .unwrap();
1314 assert_eq!(parsed.to_string(), wire);
1317 }
1318
1319 #[test]
1320 fn originate_context_named_xml() {
1321 let ep = Endpoint::Sofia(SofiaEndpoint {
1322 profile: "internal".into(),
1323 destination: "123@example.com".into(),
1324 variables: None,
1325 });
1326 let orig = Originate::extension(ep, "1000")
1327 .dialplan(DialplanType::Xml)
1328 .unwrap()
1329 .context("XML");
1330 let wire = orig.to_string();
1331 assert!(wire.contains("XML XML"), "wire: {}", wire);
1333 let parsed: Originate = wire
1334 .parse()
1335 .unwrap();
1336 assert_eq!(parsed.to_string(), wire);
1337 }
1338
1339 #[test]
1340 fn originate_accessors() {
1341 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1342 let cmd = Originate::extension(ep, "1000")
1343 .dialplan(DialplanType::Xml)
1344 .unwrap()
1345 .context("default")
1346 .cid_name("Alice")
1347 .cid_num("5551234")
1348 .timeout(Duration::from_secs(30));
1349
1350 assert!(matches!(cmd.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1351 assert_eq!(cmd.dialplan_type(), Some(&DialplanType::Xml));
1352 assert_eq!(cmd.context_str(), Some("default"));
1353 assert_eq!(cmd.caller_id_name(), Some("Alice"));
1354 assert_eq!(cmd.caller_id_number(), Some("5551234"));
1355 assert_eq!(cmd.timeout_seconds(), Some(30));
1356 }
1357}