1use std::fmt;
5use std::str::FromStr;
6use std::time::Duration;
7
8use indexmap::IndexMap;
9use serde::de::Deserializer;
10use serde::ser::Serializer;
11use serde::{Deserialize, Serialize};
12
13use super::{originate_quote, originate_split, originate_unquote};
14
15const UNDEF: &str = "undef";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25#[non_exhaustive]
26pub enum DialplanType {
27 Inline,
29 Xml,
31}
32
33impl fmt::Display for DialplanType {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::Inline => f.write_str("inline"),
37 Self::Xml => f.write_str("XML"),
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ParseDialplanTypeError(pub String);
45
46impl fmt::Display for ParseDialplanTypeError {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "unknown dialplan type: {}", self.0)
49 }
50}
51
52impl std::error::Error for ParseDialplanTypeError {}
53
54impl FromStr for DialplanType {
55 type Err = ParseDialplanTypeError;
56
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 if s.eq_ignore_ascii_case("inline") {
59 Ok(Self::Inline)
60 } else if s.eq_ignore_ascii_case("xml") {
61 Ok(Self::Xml)
62 } else {
63 Err(ParseDialplanTypeError(s.to_string()))
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75#[non_exhaustive]
76pub enum VariablesType {
77 Enterprise,
79 Default,
81 Channel,
83}
84
85impl VariablesType {
86 fn delimiters(self) -> (char, char) {
87 match self {
88 Self::Enterprise => ('<', '>'),
89 Self::Default => ('{', '}'),
90 Self::Channel => ('[', ']'),
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct Variables {
108 vars_type: VariablesType,
109 inner: IndexMap<String, String>,
110}
111
112fn escape_value(value: &str) -> String {
113 let escaped = value
114 .replace('\'', "\\'")
115 .replace(',', "\\,");
116 if escaped.contains(' ') {
117 format!("'{}'", escaped)
118 } else {
119 escaped
120 }
121}
122
123fn unescape_value(value: &str) -> String {
124 let s = value
125 .strip_prefix('\'')
126 .and_then(|s| s.strip_suffix('\''))
127 .unwrap_or(value);
128 s.replace("\\,", ",")
129 .replace("\\'", "'")
130}
131
132impl Variables {
133 pub fn new(vars_type: VariablesType) -> Self {
135 Self {
136 vars_type,
137 inner: IndexMap::new(),
138 }
139 }
140
141 pub fn with_vars(
143 vars_type: VariablesType,
144 vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
145 ) -> Self {
146 Self {
147 vars_type,
148 inner: vars
149 .into_iter()
150 .map(|(k, v)| (k.into(), v.into()))
151 .collect(),
152 }
153 }
154
155 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
157 self.inner
158 .insert(key.into(), value.into());
159 }
160
161 pub fn remove(&mut self, key: &str) -> Option<String> {
163 self.inner
164 .shift_remove(key)
165 }
166
167 pub fn get(&self, key: &str) -> Option<&str> {
169 self.inner
170 .get(key)
171 .map(|s| s.as_str())
172 }
173
174 pub fn is_empty(&self) -> bool {
176 self.inner
177 .is_empty()
178 }
179
180 pub fn len(&self) -> usize {
182 self.inner
183 .len()
184 }
185
186 pub fn scope(&self) -> VariablesType {
188 self.vars_type
189 }
190
191 pub fn set_scope(&mut self, scope: VariablesType) {
193 self.vars_type = scope;
194 }
195
196 pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
198 self.inner
199 .iter()
200 }
201
202 pub fn iter_mut(&mut self) -> impl Iterator<Item = (&String, &mut String)> {
204 self.inner
205 .iter_mut()
206 }
207
208 pub fn values_mut(&mut self) -> impl Iterator<Item = &mut String> {
210 self.inner
211 .values_mut()
212 }
213}
214
215impl Serialize for Variables {
216 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
217 if self.vars_type == VariablesType::Default {
218 self.inner
219 .serialize(serializer)
220 } else {
221 use serde::ser::SerializeStruct;
222 let mut s = serializer.serialize_struct("Variables", 2)?;
223 s.serialize_field("scope", &self.vars_type)?;
224 s.serialize_field("vars", &self.inner)?;
225 s.end()
226 }
227 }
228}
229
230impl<'de> Deserialize<'de> for Variables {
231 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
232 #[derive(Deserialize)]
233 #[serde(untagged)]
234 enum VariablesRepr {
235 Scoped {
236 scope: VariablesType,
237 vars: IndexMap<String, String>,
238 },
239 Flat(IndexMap<String, String>),
240 }
241
242 match VariablesRepr::deserialize(deserializer)? {
243 VariablesRepr::Scoped { scope, vars } => Ok(Self {
244 vars_type: scope,
245 inner: vars,
246 }),
247 VariablesRepr::Flat(map) => Ok(Self {
248 vars_type: VariablesType::Default,
249 inner: map,
250 }),
251 }
252 }
253}
254
255impl fmt::Display for Variables {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 let (open, close) = self
258 .vars_type
259 .delimiters();
260 f.write_fmt(format_args!("{}", open))?;
261 for (i, (key, value)) in self
262 .inner
263 .iter()
264 .enumerate()
265 {
266 if i > 0 {
267 f.write_str(",")?;
268 }
269 write!(f, "{}={}", key, escape_value(value))?;
270 }
271 f.write_fmt(format_args!("{}", close))
272 }
273}
274
275impl FromStr for Variables {
276 type Err = OriginateError;
277
278 fn from_str(s: &str) -> Result<Self, Self::Err> {
279 let s = s.trim();
280 if s.len() < 2 {
281 return Err(OriginateError::ParseError(
282 "variable block too short".into(),
283 ));
284 }
285
286 let (vars_type, inner_str) = match (s.as_bytes()[0], s.as_bytes()[s.len() - 1]) {
287 (b'{', b'}') => (VariablesType::Default, &s[1..s.len() - 1]),
288 (b'<', b'>') => (VariablesType::Enterprise, &s[1..s.len() - 1]),
289 (b'[', b']') => (VariablesType::Channel, &s[1..s.len() - 1]),
290 _ => {
291 return Err(OriginateError::ParseError(format!(
292 "unknown variable delimiters: {}",
293 s
294 )));
295 }
296 };
297
298 let mut inner = IndexMap::new();
299 if !inner_str.is_empty() {
300 for part in split_unescaped_commas(inner_str) {
302 let (key, value) = part
303 .split_once('=')
304 .ok_or_else(|| {
305 OriginateError::ParseError(format!("missing = in variable: {}", part))
306 })?;
307 inner.insert(key.to_string(), unescape_value(value));
308 }
309 }
310
311 Ok(Self { vars_type, inner })
312 }
313}
314
315fn split_unescaped_commas(s: &str) -> Vec<&str> {
321 let mut parts = Vec::new();
322 let mut start = 0;
323 let bytes = s.as_bytes();
324
325 for i in 0..bytes.len() {
326 if bytes[i] == b',' {
327 let mut backslashes = 0;
328 let mut j = i;
329 while j > 0 && bytes[j - 1] == b'\\' {
330 backslashes += 1;
331 j -= 1;
332 }
333 if backslashes % 2 == 0 {
334 parts.push(&s[start..i]);
335 start = i + 1;
336 }
337 }
338 }
339 parts.push(&s[start..]);
340 parts
341}
342
343pub use super::endpoint::Endpoint;
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352#[non_exhaustive]
353pub struct Application {
354 pub name: String,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub args: Option<String>,
359}
360
361impl Application {
362 pub fn new(name: impl Into<String>, args: Option<impl Into<String>>) -> Self {
364 Self {
365 name: name.into(),
366 args: args.map(|a| a.into()),
367 }
368 }
369
370 pub fn simple(name: impl Into<String>) -> Self {
372 Self {
373 name: name.into(),
374 args: None,
375 }
376 }
377
378 pub fn to_string_with_dialplan(&self, dialplan: &DialplanType) -> String {
380 match dialplan {
381 DialplanType::Inline => match &self.args {
382 Some(args) => format!("{}:{}", self.name, args),
383 None => self
384 .name
385 .clone(),
386 },
387 _ => {
389 let args = self
390 .args
391 .as_deref()
392 .unwrap_or("");
393 format!("&{}({})", self.name, args)
394 }
395 }
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406#[serde(rename_all = "snake_case")]
407#[non_exhaustive]
408pub enum OriginateTarget {
409 Extension(String),
411 Application(Application),
413 InlineApplications(Vec<Application>),
415}
416
417impl From<Application> for OriginateTarget {
418 fn from(app: Application) -> Self {
419 Self::Application(app)
420 }
421}
422
423impl From<Vec<Application>> for OriginateTarget {
424 fn from(apps: Vec<Application>) -> Self {
425 Self::InlineApplications(apps)
426 }
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
449pub struct Originate {
450 endpoint: Endpoint,
451 target: OriginateTarget,
452 dialplan: Option<DialplanType>,
453 context: Option<String>,
454 cid_name: Option<String>,
455 cid_num: Option<String>,
456 timeout: Option<Duration>,
457}
458
459#[derive(Serialize, Deserialize)]
461struct OriginateRaw {
462 endpoint: Endpoint,
463 #[serde(flatten)]
464 target: OriginateTarget,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
466 dialplan: Option<DialplanType>,
467 #[serde(default, skip_serializing_if = "Option::is_none")]
468 context: Option<String>,
469 #[serde(default, skip_serializing_if = "Option::is_none")]
470 cid_name: Option<String>,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
472 cid_num: Option<String>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
474 timeout_secs: Option<u64>,
475}
476
477impl TryFrom<OriginateRaw> for Originate {
478 type Error = OriginateError;
479
480 fn try_from(raw: OriginateRaw) -> Result<Self, Self::Error> {
481 if matches!(raw.target, OriginateTarget::Extension(_))
482 && matches!(raw.dialplan, Some(DialplanType::Inline))
483 {
484 return Err(OriginateError::ExtensionWithInlineDialplan);
485 }
486 if let OriginateTarget::InlineApplications(ref apps) = raw.target {
487 if apps.is_empty() {
488 return Err(OriginateError::EmptyInlineApplications);
489 }
490 }
491 Ok(Self {
492 endpoint: raw.endpoint,
493 target: raw.target,
494 dialplan: raw.dialplan,
495 context: raw.context,
496 cid_name: raw.cid_name,
497 cid_num: raw.cid_num,
498 timeout: raw
499 .timeout_secs
500 .map(Duration::from_secs),
501 })
502 }
503}
504
505impl From<Originate> for OriginateRaw {
506 fn from(o: Originate) -> Self {
507 Self {
508 endpoint: o.endpoint,
509 target: o.target,
510 dialplan: o.dialplan,
511 context: o.context,
512 cid_name: o.cid_name,
513 cid_num: o.cid_num,
514 timeout_secs: o
515 .timeout
516 .map(|d| d.as_secs()),
517 }
518 }
519}
520
521impl Serialize for Originate {
522 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
523 OriginateRaw::from(self.clone()).serialize(serializer)
524 }
525}
526
527impl<'de> Deserialize<'de> for Originate {
528 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
529 let raw = OriginateRaw::deserialize(deserializer)?;
530 Originate::try_from(raw).map_err(serde::de::Error::custom)
531 }
532}
533
534impl Originate {
535 pub fn extension(endpoint: Endpoint, extension: impl Into<String>) -> Self {
537 Self {
538 endpoint,
539 target: OriginateTarget::Extension(extension.into()),
540 dialplan: None,
541 context: None,
542 cid_name: None,
543 cid_num: None,
544 timeout: None,
545 }
546 }
547
548 pub fn application(endpoint: Endpoint, app: Application) -> Self {
550 Self {
551 endpoint,
552 target: OriginateTarget::Application(app),
553 dialplan: None,
554 context: None,
555 cid_name: None,
556 cid_num: None,
557 timeout: None,
558 }
559 }
560
561 pub fn inline(
565 endpoint: Endpoint,
566 apps: impl IntoIterator<Item = Application>,
567 ) -> Result<Self, OriginateError> {
568 let apps: Vec<Application> = apps
569 .into_iter()
570 .collect();
571 if apps.is_empty() {
572 return Err(OriginateError::EmptyInlineApplications);
573 }
574 Ok(Self {
575 endpoint,
576 target: OriginateTarget::InlineApplications(apps),
577 dialplan: None,
578 context: None,
579 cid_name: None,
580 cid_num: None,
581 timeout: None,
582 })
583 }
584
585 pub fn dialplan(mut self, dp: DialplanType) -> Result<Self, OriginateError> {
589 if matches!(self.target, OriginateTarget::Extension(_)) && dp == DialplanType::Inline {
590 return Err(OriginateError::ExtensionWithInlineDialplan);
591 }
592 self.dialplan = Some(dp);
593 Ok(self)
594 }
595
596 pub fn context(mut self, ctx: impl Into<String>) -> Self {
598 self.context = Some(ctx.into());
599 self
600 }
601
602 pub fn cid_name(mut self, name: impl Into<String>) -> Self {
604 self.cid_name = Some(name.into());
605 self
606 }
607
608 pub fn cid_num(mut self, num: impl Into<String>) -> Self {
610 self.cid_num = Some(num.into());
611 self
612 }
613
614 pub fn timeout(mut self, duration: Duration) -> Self {
617 self.timeout = Some(duration);
618 self
619 }
620
621 pub fn endpoint(&self) -> &Endpoint {
623 &self.endpoint
624 }
625
626 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
628 &mut self.endpoint
629 }
630
631 pub fn target(&self) -> &OriginateTarget {
633 &self.target
634 }
635
636 pub fn target_mut(&mut self) -> &mut OriginateTarget {
638 &mut self.target
639 }
640
641 pub fn dialplan_type(&self) -> Option<&DialplanType> {
643 self.dialplan
644 .as_ref()
645 }
646
647 pub fn context_str(&self) -> Option<&str> {
649 self.context
650 .as_deref()
651 }
652
653 pub fn caller_id_name(&self) -> Option<&str> {
655 self.cid_name
656 .as_deref()
657 }
658
659 pub fn caller_id_number(&self) -> Option<&str> {
661 self.cid_num
662 .as_deref()
663 }
664
665 pub fn timeout_duration(&self) -> Option<Duration> {
667 self.timeout
668 }
669
670 pub fn timeout_seconds(&self) -> Option<u64> {
672 self.timeout
673 .map(|d| d.as_secs())
674 }
675}
676
677impl fmt::Display for Originate {
678 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
679 let target_str = match &self.target {
680 OriginateTarget::Extension(ext) => ext.clone(),
681 OriginateTarget::Application(app) => app.to_string_with_dialplan(&DialplanType::Xml),
682 OriginateTarget::InlineApplications(apps) => {
683 let parts: Vec<String> = apps
685 .iter()
686 .map(|a| a.to_string_with_dialplan(&DialplanType::Inline))
687 .collect();
688 parts.join(",")
689 }
690 };
691
692 write!(
693 f,
694 "originate {} {}",
695 self.endpoint,
696 originate_quote(&target_str)
697 )?;
698
699 let dialplan = match &self.target {
703 OriginateTarget::InlineApplications(_) => Some(
704 self.dialplan
705 .unwrap_or(DialplanType::Inline),
706 ),
707 _ => self.dialplan,
708 };
709 let has_ctx = self
710 .context
711 .is_some();
712 let has_name = self
713 .cid_name
714 .is_some();
715 let has_num = self
716 .cid_num
717 .is_some();
718 let has_timeout = self
719 .timeout
720 .is_some();
721
722 if dialplan.is_some() || has_ctx || has_name || has_num || has_timeout {
723 let dp = dialplan
724 .as_ref()
725 .cloned()
726 .unwrap_or(DialplanType::Xml);
727 write!(f, " {}", dp)?;
728 }
729 if has_ctx || has_name || has_num || has_timeout {
730 write!(
731 f,
732 " {}",
733 self.context
734 .as_deref()
735 .unwrap_or("default")
736 )?;
737 }
738 if has_name || has_num || has_timeout {
739 let name = self
740 .cid_name
741 .as_deref()
742 .unwrap_or(UNDEF);
743 write!(f, " {}", originate_quote(name))?;
744 }
745 if has_num || has_timeout {
746 let num = self
747 .cid_num
748 .as_deref()
749 .unwrap_or(UNDEF);
750 write!(f, " {}", originate_quote(num))?;
751 }
752 if let Some(ref timeout) = self.timeout {
753 write!(f, " {}", timeout.as_secs())?;
754 }
755 Ok(())
756 }
757}
758
759impl FromStr for Originate {
760 type Err = OriginateError;
761
762 fn from_str(s: &str) -> Result<Self, Self::Err> {
763 let s = s
764 .strip_prefix("originate")
765 .unwrap_or(s)
766 .trim();
767 let mut args = originate_split(s, ' ')?;
768
769 if args.is_empty() {
770 return Err(OriginateError::ParseError("empty originate".into()));
771 }
772
773 let endpoint_str = args.remove(0);
774 let endpoint: Endpoint = endpoint_str.parse()?;
775
776 if args.is_empty() {
777 return Err(OriginateError::ParseError(
778 "missing target in originate".into(),
779 ));
780 }
781
782 let target_str = originate_unquote(&args.remove(0));
783
784 let dialplan = args
785 .first()
786 .and_then(|s| {
787 s.parse::<DialplanType>()
788 .ok()
789 });
790 if dialplan.is_some() {
791 args.remove(0);
792 }
793
794 let target = super::parse_originate_target(&target_str, dialplan.as_ref())?;
795
796 let context = if !args.is_empty() {
797 Some(args.remove(0))
798 } else {
799 None
800 };
801 let cid_name = if !args.is_empty() {
802 let v = args.remove(0);
803 if v.eq_ignore_ascii_case(UNDEF) {
804 None
805 } else {
806 Some(v)
807 }
808 } else {
809 None
810 };
811 let cid_num = if !args.is_empty() {
812 let v = args.remove(0);
813 if v.eq_ignore_ascii_case(UNDEF) {
814 None
815 } else {
816 Some(v)
817 }
818 } else {
819 None
820 };
821 let timeout = if !args.is_empty() {
822 Some(Duration::from_secs(
823 args.remove(0)
824 .parse::<u64>()
825 .map_err(|e| OriginateError::ParseError(format!("invalid timeout: {}", e)))?,
826 ))
827 } else {
828 None
829 };
830
831 let mut orig = match target {
833 OriginateTarget::Extension(ref ext) => Self::extension(endpoint, ext.clone()),
834 OriginateTarget::Application(ref app) => Self::application(endpoint, app.clone()),
835 OriginateTarget::InlineApplications(ref apps) => Self::inline(endpoint, apps.clone())?,
836 };
837 orig.dialplan = dialplan;
838 orig.context = context;
839 orig.cid_name = cid_name;
840 orig.cid_num = cid_num;
841 orig.timeout = timeout;
842 Ok(orig)
843 }
844}
845
846#[derive(Debug, thiserror::Error)]
848#[non_exhaustive]
849pub enum OriginateError {
850 #[error("unclosed quote at: {0}")]
852 UnclosedQuote(String),
853 #[error("parse error: {0}")]
855 ParseError(String),
856 #[error("inline originate requires at least one application")]
858 EmptyInlineApplications,
859 #[error("extension target is incompatible with inline dialplan")]
861 ExtensionWithInlineDialplan,
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867 use crate::commands::endpoint::{LoopbackEndpoint, SofiaEndpoint, SofiaGateway};
868
869 #[test]
872 fn variables_standard_chars() {
873 let mut vars = Variables::new(VariablesType::Default);
874 vars.insert("test_key", "this_value");
875 let result = vars.to_string();
876 assert!(result.contains("test_key"));
877 assert!(result.contains("this_value"));
878 }
879
880 #[test]
881 fn variables_comma_escaped() {
882 let mut vars = Variables::new(VariablesType::Default);
883 vars.insert("test_key", "this,is,a,value");
884 let result = vars.to_string();
885 assert!(result.contains("\\,"));
886 }
887
888 #[test]
889 fn variables_spaces_quoted() {
890 let mut vars = Variables::new(VariablesType::Default);
891 vars.insert("test_key", "this is a value");
892 let result = vars.to_string();
893 assert_eq!(
894 result
895 .matches('\'')
896 .count(),
897 2
898 );
899 }
900
901 #[test]
902 fn variables_single_quote_escaped() {
903 let mut vars = Variables::new(VariablesType::Default);
904 vars.insert("test_key", "let's_this_be_a_value");
905 let result = vars.to_string();
906 assert!(result.contains("\\'"));
907 }
908
909 #[test]
910 fn variables_enterprise_delimiters() {
911 let mut vars = Variables::new(VariablesType::Enterprise);
912 vars.insert("k", "v");
913 let result = vars.to_string();
914 assert!(result.starts_with('<'));
915 assert!(result.ends_with('>'));
916 }
917
918 #[test]
919 fn variables_channel_delimiters() {
920 let mut vars = Variables::new(VariablesType::Channel);
921 vars.insert("k", "v");
922 let result = vars.to_string();
923 assert!(result.starts_with('['));
924 assert!(result.ends_with(']'));
925 }
926
927 #[test]
928 fn variables_default_delimiters() {
929 let mut vars = Variables::new(VariablesType::Default);
930 vars.insert("k", "v");
931 let result = vars.to_string();
932 assert!(result.starts_with('{'));
933 assert!(result.ends_with('}'));
934 }
935
936 #[test]
937 fn variables_parse_round_trip() {
938 let mut vars = Variables::new(VariablesType::Default);
939 vars.insert("origination_caller_id_number", "9005551212");
940 vars.insert("sip_h_Call-Info", "<url>;meta=123,<uri>");
941 let s = vars.to_string();
942 let parsed: Variables = s
943 .parse()
944 .unwrap();
945 assert_eq!(
946 parsed.get("origination_caller_id_number"),
947 Some("9005551212")
948 );
949 assert_eq!(parsed.get("sip_h_Call-Info"), Some("<url>;meta=123,<uri>"));
950 }
951
952 #[test]
953 fn split_unescaped_commas_basic() {
954 assert_eq!(split_unescaped_commas("a,b,c"), vec!["a", "b", "c"]);
955 }
956
957 #[test]
958 fn split_unescaped_commas_escaped() {
959 assert_eq!(split_unescaped_commas(r"a\,b,c"), vec![r"a\,b", "c"]);
960 }
961
962 #[test]
963 fn split_unescaped_commas_double_backslash() {
964 assert_eq!(split_unescaped_commas(r"a\\,b"), vec![r"a\\", "b"]);
966 }
967
968 #[test]
969 fn split_unescaped_commas_triple_backslash() {
970 assert_eq!(split_unescaped_commas(r"a\\\,b"), vec![r"a\\\,b"]);
972 }
973
974 #[test]
977 fn endpoint_uri_only() {
978 let ep = Endpoint::Sofia(SofiaEndpoint {
979 profile: "internal".into(),
980 destination: "123@example.com".into(),
981 variables: None,
982 });
983 assert_eq!(ep.to_string(), "sofia/internal/123@example.com");
984 }
985
986 #[test]
987 fn endpoint_uri_with_variable() {
988 let mut vars = Variables::new(VariablesType::Default);
989 vars.insert("one_variable", "1");
990 let ep = Endpoint::Sofia(SofiaEndpoint {
991 profile: "internal".into(),
992 destination: "123@example.com".into(),
993 variables: Some(vars),
994 });
995 assert_eq!(
996 ep.to_string(),
997 "{one_variable=1}sofia/internal/123@example.com"
998 );
999 }
1000
1001 #[test]
1002 fn endpoint_variable_with_quote() {
1003 let mut vars = Variables::new(VariablesType::Default);
1004 vars.insert("one_variable", "one'quote");
1005 let ep = Endpoint::Sofia(SofiaEndpoint {
1006 profile: "internal".into(),
1007 destination: "123@example.com".into(),
1008 variables: Some(vars),
1009 });
1010 assert_eq!(
1011 ep.to_string(),
1012 "{one_variable=one\\'quote}sofia/internal/123@example.com"
1013 );
1014 }
1015
1016 #[test]
1017 fn loopback_endpoint_display() {
1018 let mut vars = Variables::new(VariablesType::Default);
1019 vars.insert("one_variable", "1");
1020 let ep = Endpoint::Loopback(
1021 LoopbackEndpoint::new("aUri")
1022 .with_context("aContext")
1023 .with_variables(vars),
1024 );
1025 assert_eq!(ep.to_string(), "{one_variable=1}loopback/aUri/aContext");
1026 }
1027
1028 #[test]
1029 fn sofia_gateway_endpoint_display() {
1030 let mut vars = Variables::new(VariablesType::Default);
1031 vars.insert("one_variable", "1");
1032 let ep = Endpoint::SofiaGateway(SofiaGateway {
1033 destination: "aUri".into(),
1034 profile: None,
1035 gateway: "internal".into(),
1036 variables: Some(vars),
1037 });
1038 assert_eq!(
1039 ep.to_string(),
1040 "{one_variable=1}sofia/gateway/internal/aUri"
1041 );
1042 }
1043
1044 #[test]
1047 fn application_xml_format() {
1048 let app = Application::new("testApp", Some("testArg"));
1049 assert_eq!(
1050 app.to_string_with_dialplan(&DialplanType::Xml),
1051 "&testApp(testArg)"
1052 );
1053 }
1054
1055 #[test]
1056 fn application_inline_format() {
1057 let app = Application::new("testApp", Some("testArg"));
1058 assert_eq!(
1059 app.to_string_with_dialplan(&DialplanType::Inline),
1060 "testApp:testArg"
1061 );
1062 }
1063
1064 #[test]
1065 fn application_inline_no_args() {
1066 let app = Application::simple("park");
1067 assert_eq!(app.to_string_with_dialplan(&DialplanType::Inline), "park");
1068 }
1069
1070 #[test]
1073 fn originate_xml_display() {
1074 let ep = Endpoint::Sofia(SofiaEndpoint {
1075 profile: "internal".into(),
1076 destination: "123@example.com".into(),
1077 variables: None,
1078 });
1079 let orig = Originate::application(ep, Application::new("conference", Some("1")))
1080 .dialplan(DialplanType::Xml)
1081 .unwrap();
1082 assert_eq!(
1083 orig.to_string(),
1084 "originate sofia/internal/123@example.com &conference(1) XML"
1085 );
1086 }
1087
1088 #[test]
1089 fn originate_inline_display() {
1090 let ep = Endpoint::Sofia(SofiaEndpoint {
1091 profile: "internal".into(),
1092 destination: "123@example.com".into(),
1093 variables: None,
1094 });
1095 let orig = Originate::inline(ep, vec![Application::new("conference", Some("1"))])
1096 .unwrap()
1097 .dialplan(DialplanType::Inline)
1098 .unwrap();
1099 assert_eq!(
1100 orig.to_string(),
1101 "originate sofia/internal/123@example.com conference:1 inline"
1102 );
1103 }
1104
1105 #[test]
1106 fn originate_extension_display() {
1107 let ep = Endpoint::Sofia(SofiaEndpoint {
1108 profile: "internal".into(),
1109 destination: "123@example.com".into(),
1110 variables: None,
1111 });
1112 let orig = Originate::extension(ep, "1000")
1113 .dialplan(DialplanType::Xml)
1114 .unwrap()
1115 .context("default");
1116 assert_eq!(
1117 orig.to_string(),
1118 "originate sofia/internal/123@example.com 1000 XML default"
1119 );
1120 }
1121
1122 #[test]
1123 fn originate_extension_round_trip() {
1124 let input = "originate sofia/internal/test@example.com 1000 XML default";
1125 let parsed: Originate = input
1126 .parse()
1127 .unwrap();
1128 assert_eq!(parsed.to_string(), input);
1129 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1130 }
1131
1132 #[test]
1133 fn originate_extension_no_dialplan() {
1134 let input = "originate sofia/internal/test@example.com 1000";
1135 let parsed: Originate = input
1136 .parse()
1137 .unwrap();
1138 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1139 assert_eq!(parsed.to_string(), input);
1140 }
1141
1142 #[test]
1143 fn originate_extension_with_inline_errors() {
1144 let ep = Endpoint::Sofia(SofiaEndpoint {
1145 profile: "internal".into(),
1146 destination: "123@example.com".into(),
1147 variables: None,
1148 });
1149 let result = Originate::extension(ep, "1000").dialplan(DialplanType::Inline);
1150 assert!(result.is_err());
1151 }
1152
1153 #[test]
1154 fn originate_empty_inline_errors() {
1155 let ep = Endpoint::Sofia(SofiaEndpoint {
1156 profile: "internal".into(),
1157 destination: "123@example.com".into(),
1158 variables: None,
1159 });
1160 let result = Originate::inline(ep, vec![]);
1161 assert!(result.is_err());
1162 }
1163
1164 #[test]
1165 fn originate_from_string_round_trip() {
1166 let input = "originate {test='variable with quote'}sofia/internal/test@example.com 123";
1167 let orig: Originate = input
1168 .parse()
1169 .unwrap();
1170 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "123"));
1171 assert_eq!(orig.to_string(), input);
1172 }
1173
1174 #[test]
1175 fn originate_socket_app_quoted() {
1176 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1177 let orig = Originate::application(
1178 ep,
1179 Application::new("socket", Some("127.0.0.1:8040 async full")),
1180 );
1181 assert_eq!(
1182 orig.to_string(),
1183 "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'"
1184 );
1185 }
1186
1187 #[test]
1188 fn originate_socket_round_trip() {
1189 let input = "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'";
1190 let parsed: Originate = input
1191 .parse()
1192 .unwrap();
1193 assert_eq!(parsed.to_string(), input);
1194 if let OriginateTarget::Application(ref app) = parsed.target() {
1195 assert_eq!(
1196 app.args
1197 .as_deref(),
1198 Some("127.0.0.1:8040 async full")
1199 );
1200 } else {
1201 panic!("expected Application target");
1202 }
1203 }
1204
1205 #[test]
1206 fn originate_display_round_trip() {
1207 let ep = Endpoint::Sofia(SofiaEndpoint {
1208 profile: "internal".into(),
1209 destination: "123@example.com".into(),
1210 variables: None,
1211 });
1212 let orig = Originate::application(ep, Application::new("conference", Some("1")))
1213 .dialplan(DialplanType::Xml)
1214 .unwrap();
1215 let s = orig.to_string();
1216 let parsed: Originate = s
1217 .parse()
1218 .unwrap();
1219 assert_eq!(parsed.to_string(), s);
1220 }
1221
1222 #[test]
1223 fn originate_inline_no_args_round_trip() {
1224 let input = "originate sofia/internal/123@example.com park inline";
1225 let parsed: Originate = input
1226 .parse()
1227 .unwrap();
1228 assert_eq!(parsed.to_string(), input);
1229 if let OriginateTarget::InlineApplications(ref apps) = parsed.target() {
1230 assert!(apps[0]
1231 .args
1232 .is_none());
1233 } else {
1234 panic!("expected InlineApplications target");
1235 }
1236 }
1237
1238 #[test]
1239 fn originate_inline_multi_app_round_trip() {
1240 let input =
1241 "originate sofia/internal/123@example.com playback:/tmp/test.wav,hangup:NORMAL_CLEARING inline";
1242 let parsed: Originate = input
1243 .parse()
1244 .unwrap();
1245 assert_eq!(parsed.to_string(), input);
1246 }
1247
1248 #[test]
1249 fn originate_inline_auto_dialplan() {
1250 let ep = Endpoint::Sofia(SofiaEndpoint {
1251 profile: "internal".into(),
1252 destination: "123@example.com".into(),
1253 variables: None,
1254 });
1255 let orig = Originate::inline(ep, vec![Application::simple("park")]).unwrap();
1256 assert!(orig
1257 .to_string()
1258 .contains("inline"));
1259 }
1260
1261 #[test]
1264 fn dialplan_type_display() {
1265 assert_eq!(DialplanType::Inline.to_string(), "inline");
1266 assert_eq!(DialplanType::Xml.to_string(), "XML");
1267 }
1268
1269 #[test]
1270 fn dialplan_type_from_str() {
1271 assert_eq!(
1272 "inline"
1273 .parse::<DialplanType>()
1274 .unwrap(),
1275 DialplanType::Inline
1276 );
1277 assert_eq!(
1278 "XML"
1279 .parse::<DialplanType>()
1280 .unwrap(),
1281 DialplanType::Xml
1282 );
1283 }
1284
1285 #[test]
1286 fn dialplan_type_from_str_case_insensitive() {
1287 assert_eq!(
1288 "xml"
1289 .parse::<DialplanType>()
1290 .unwrap(),
1291 DialplanType::Xml
1292 );
1293 assert_eq!(
1294 "Xml"
1295 .parse::<DialplanType>()
1296 .unwrap(),
1297 DialplanType::Xml
1298 );
1299 assert_eq!(
1300 "INLINE"
1301 .parse::<DialplanType>()
1302 .unwrap(),
1303 DialplanType::Inline
1304 );
1305 assert_eq!(
1306 "Inline"
1307 .parse::<DialplanType>()
1308 .unwrap(),
1309 DialplanType::Inline
1310 );
1311 }
1312
1313 #[test]
1316 fn serde_dialplan_type_xml() {
1317 let json = serde_json::to_string(&DialplanType::Xml).unwrap();
1318 assert_eq!(json, "\"xml\"");
1319 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1320 assert_eq!(parsed, DialplanType::Xml);
1321 }
1322
1323 #[test]
1324 fn serde_dialplan_type_inline() {
1325 let json = serde_json::to_string(&DialplanType::Inline).unwrap();
1326 assert_eq!(json, "\"inline\"");
1327 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1328 assert_eq!(parsed, DialplanType::Inline);
1329 }
1330
1331 #[test]
1332 fn serde_variables_type() {
1333 let json = serde_json::to_string(&VariablesType::Enterprise).unwrap();
1334 assert_eq!(json, "\"enterprise\"");
1335 let parsed: VariablesType = serde_json::from_str(&json).unwrap();
1336 assert_eq!(parsed, VariablesType::Enterprise);
1337 }
1338
1339 #[test]
1340 fn serde_variables_flat_default() {
1341 let mut vars = Variables::new(VariablesType::Default);
1342 vars.insert("key1", "val1");
1343 vars.insert("key2", "val2");
1344 let json = serde_json::to_string(&vars).unwrap();
1345 let parsed: Variables = serde_json::from_str(&json).unwrap();
1347 assert_eq!(parsed.scope(), VariablesType::Default);
1348 assert_eq!(parsed.get("key1"), Some("val1"));
1349 assert_eq!(parsed.get("key2"), Some("val2"));
1350 }
1351
1352 #[test]
1353 fn serde_variables_scoped_enterprise() {
1354 let mut vars = Variables::new(VariablesType::Enterprise);
1355 vars.insert("key1", "val1");
1356 let json = serde_json::to_string(&vars).unwrap();
1357 assert!(json.contains("\"enterprise\""));
1359 let parsed: Variables = serde_json::from_str(&json).unwrap();
1360 assert_eq!(parsed.scope(), VariablesType::Enterprise);
1361 assert_eq!(parsed.get("key1"), Some("val1"));
1362 }
1363
1364 #[test]
1365 fn serde_variables_flat_map_deserializes_as_default() {
1366 let json = r#"{"key1":"val1","key2":"val2"}"#;
1367 let vars: Variables = serde_json::from_str(json).unwrap();
1368 assert_eq!(vars.scope(), VariablesType::Default);
1369 assert_eq!(vars.get("key1"), Some("val1"));
1370 assert_eq!(vars.get("key2"), Some("val2"));
1371 }
1372
1373 #[test]
1374 fn serde_variables_scoped_deserializes() {
1375 let json = r#"{"scope":"channel","vars":{"k":"v"}}"#;
1376 let vars: Variables = serde_json::from_str(json).unwrap();
1377 assert_eq!(vars.scope(), VariablesType::Channel);
1378 assert_eq!(vars.get("k"), Some("v"));
1379 }
1380
1381 #[test]
1382 fn serde_application() {
1383 let app = Application::new("park", None::<&str>);
1384 let json = serde_json::to_string(&app).unwrap();
1385 let parsed: Application = serde_json::from_str(&json).unwrap();
1386 assert_eq!(parsed, app);
1387 }
1388
1389 #[test]
1390 fn serde_application_with_args() {
1391 let app = Application::new("conference", Some("1"));
1392 let json = serde_json::to_string(&app).unwrap();
1393 let parsed: Application = serde_json::from_str(&json).unwrap();
1394 assert_eq!(parsed, app);
1395 }
1396
1397 #[test]
1398 fn serde_application_skips_none_args() {
1399 let app = Application::new("park", None::<&str>);
1400 let json = serde_json::to_string(&app).unwrap();
1401 assert!(!json.contains("args"));
1402 }
1403
1404 #[test]
1405 fn serde_originate_application_round_trip() {
1406 let ep = Endpoint::Sofia(SofiaEndpoint {
1407 profile: "internal".into(),
1408 destination: "123@example.com".into(),
1409 variables: None,
1410 });
1411 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1412 .dialplan(DialplanType::Xml)
1413 .unwrap()
1414 .context("default")
1415 .cid_name("Test")
1416 .cid_num("5551234")
1417 .timeout(Duration::from_secs(30));
1418 let json = serde_json::to_string(&orig).unwrap();
1419 assert!(json.contains("\"application\""));
1420 let parsed: Originate = serde_json::from_str(&json).unwrap();
1421 assert_eq!(parsed, orig);
1422 }
1423
1424 #[test]
1425 fn serde_originate_extension() {
1426 let json = r#"{
1427 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1428 "extension": "1000",
1429 "dialplan": "xml",
1430 "context": "default"
1431 }"#;
1432 let orig: Originate = serde_json::from_str(json).unwrap();
1433 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1434 assert_eq!(
1435 orig.to_string(),
1436 "originate sofia/internal/123@example.com 1000 XML default"
1437 );
1438 }
1439
1440 #[test]
1441 fn serde_originate_extension_with_inline_rejected() {
1442 let json = r#"{
1443 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1444 "extension": "1000",
1445 "dialplan": "inline"
1446 }"#;
1447 let result = serde_json::from_str::<Originate>(json);
1448 assert!(result.is_err());
1449 }
1450
1451 #[test]
1452 fn serde_originate_empty_inline_rejected() {
1453 let json = r#"{
1454 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1455 "inline_applications": []
1456 }"#;
1457 let result = serde_json::from_str::<Originate>(json);
1458 assert!(result.is_err());
1459 }
1460
1461 #[test]
1462 fn serde_originate_inline_applications() {
1463 let json = r#"{
1464 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1465 "inline_applications": [
1466 {"name": "playback", "args": "/tmp/test.wav"},
1467 {"name": "hangup", "args": "NORMAL_CLEARING"}
1468 ]
1469 }"#;
1470 let orig: Originate = serde_json::from_str(json).unwrap();
1471 if let OriginateTarget::InlineApplications(ref apps) = orig.target() {
1472 assert_eq!(apps.len(), 2);
1473 } else {
1474 panic!("expected InlineApplications");
1475 }
1476 assert!(orig
1477 .to_string()
1478 .contains("inline"));
1479 }
1480
1481 #[test]
1482 fn serde_originate_skips_none_fields() {
1483 let ep = Endpoint::Sofia(SofiaEndpoint {
1484 profile: "internal".into(),
1485 destination: "123@example.com".into(),
1486 variables: None,
1487 });
1488 let orig = Originate::application(ep, Application::new("park", None::<&str>));
1489 let json = serde_json::to_string(&orig).unwrap();
1490 assert!(!json.contains("dialplan"));
1491 assert!(!json.contains("context"));
1492 assert!(!json.contains("cid_name"));
1493 assert!(!json.contains("cid_num"));
1494 assert!(!json.contains("timeout"));
1495 }
1496
1497 #[test]
1498 fn serde_originate_to_wire_format() {
1499 let json = r#"{
1500 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1501 "application": {"name": "park"},
1502 "dialplan": "xml",
1503 "context": "default"
1504 }"#;
1505 let orig: Originate = serde_json::from_str(json).unwrap();
1506 let wire = orig.to_string();
1507 assert!(wire.starts_with("originate"));
1508 assert!(wire.contains("sofia/internal/123@example.com"));
1509 assert!(wire.contains("&park()"));
1510 assert!(wire.contains("XML"));
1511 }
1512
1513 #[test]
1516 fn application_simple_no_args() {
1517 let app = Application::simple("park");
1518 assert_eq!(app.name, "park");
1519 assert!(app
1520 .args
1521 .is_none());
1522 }
1523
1524 #[test]
1525 fn application_simple_xml_format() {
1526 let app = Application::simple("park");
1527 assert_eq!(app.to_string_with_dialplan(&DialplanType::Xml), "&park()");
1528 }
1529
1530 #[test]
1533 fn originate_target_from_application() {
1534 let target: OriginateTarget = Application::simple("park").into();
1535 assert!(matches!(target, OriginateTarget::Application(_)));
1536 }
1537
1538 #[test]
1539 fn originate_target_from_vec() {
1540 let target: OriginateTarget = vec![
1541 Application::new("conference", Some("1")),
1542 Application::new("hangup", Some("NORMAL_CLEARING")),
1543 ]
1544 .into();
1545 if let OriginateTarget::InlineApplications(apps) = target {
1546 assert_eq!(apps.len(), 2);
1547 } else {
1548 panic!("expected InlineApplications");
1549 }
1550 }
1551
1552 #[test]
1553 fn originate_target_application_wire_format() {
1554 let ep = Endpoint::Sofia(SofiaEndpoint {
1555 profile: "internal".into(),
1556 destination: "123@example.com".into(),
1557 variables: None,
1558 });
1559 let orig = Originate::application(ep, Application::simple("park"));
1560 assert_eq!(
1561 orig.to_string(),
1562 "originate sofia/internal/123@example.com &park()"
1563 );
1564 }
1565
1566 #[test]
1567 fn originate_timeout_only_fills_positional_gaps() {
1568 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1569 let cmd = Originate::application(ep, Application::simple("park"))
1570 .timeout(Duration::from_secs(30));
1571 assert_eq!(
1574 cmd.to_string(),
1575 "originate loopback/9199/test &park() XML default undef undef 30"
1576 );
1577 }
1578
1579 #[test]
1580 fn originate_cid_num_only_fills_preceding_gaps() {
1581 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1582 let cmd = Originate::application(ep, Application::simple("park")).cid_num("5551234");
1583 assert_eq!(
1584 cmd.to_string(),
1585 "originate loopback/9199/test &park() XML default undef 5551234"
1586 );
1587 }
1588
1589 #[test]
1590 fn originate_context_only_fills_dialplan() {
1591 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1592 let cmd = Originate::extension(ep, "1000").context("myctx");
1593 assert_eq!(
1594 cmd.to_string(),
1595 "originate loopback/9199/test 1000 XML myctx"
1596 );
1597 }
1598
1599 #[test]
1605 fn originate_context_gap_filler_round_trip_asymmetry() {
1606 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1607 let cmd = Originate::application(ep, Application::simple("park")).cid_name("Alice");
1608 let wire = cmd.to_string();
1609 assert!(wire.contains("default"), "gap-filler should emit 'default'");
1610
1611 let parsed: Originate = wire
1612 .parse()
1613 .unwrap();
1614 assert_eq!(parsed.context_str(), Some("default"));
1616
1617 assert_eq!(parsed.to_string(), wire);
1619 }
1620
1621 #[test]
1624 fn serde_originate_full_round_trip_with_variables() {
1625 let mut ep_vars = Variables::new(VariablesType::Default);
1626 ep_vars.insert("originate_timeout", "30");
1627 ep_vars.insert("sip_h_X-Custom", "value with spaces");
1628 let ep = Endpoint::SofiaGateway(SofiaGateway {
1629 gateway: "my_provider".into(),
1630 destination: "18005551234".into(),
1631 profile: Some("external".into()),
1632 variables: Some(ep_vars),
1633 });
1634 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1635 .dialplan(DialplanType::Xml)
1636 .unwrap()
1637 .context("public")
1638 .cid_name("Test Caller")
1639 .cid_num("5551234")
1640 .timeout(Duration::from_secs(60));
1641 let json = serde_json::to_string(&orig).unwrap();
1642 let parsed: Originate = serde_json::from_str(&json).unwrap();
1643 assert_eq!(parsed, orig);
1644 assert_eq!(parsed.to_string(), orig.to_string());
1645 }
1646
1647 #[test]
1648 fn serde_originate_inline_round_trip_with_all_fields() {
1649 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1650 let orig = Originate::inline(
1651 ep,
1652 vec![
1653 Application::new("playback", Some("/tmp/test.wav")),
1654 Application::new("hangup", Some("NORMAL_CLEARING")),
1655 ],
1656 )
1657 .unwrap()
1658 .dialplan(DialplanType::Inline)
1659 .unwrap()
1660 .context("default")
1661 .cid_name("IVR")
1662 .cid_num("0000")
1663 .timeout(Duration::from_secs(45));
1664 let json = serde_json::to_string(&orig).unwrap();
1665 let parsed: Originate = serde_json::from_str(&json).unwrap();
1666 assert_eq!(parsed, orig);
1667 assert_eq!(parsed.to_string(), orig.to_string());
1668 }
1669
1670 #[test]
1673 fn variables_from_str_empty_block() {
1674 let result = "{}".parse::<Variables>();
1675 assert!(
1676 result.is_ok(),
1677 "empty variable block should parse successfully"
1678 );
1679 let vars = result.unwrap();
1680 assert!(
1681 vars.is_empty(),
1682 "parsed empty block should have no variables"
1683 );
1684 }
1685
1686 #[test]
1687 fn variables_from_str_empty_channel_block() {
1688 let result = "[]".parse::<Variables>();
1689 assert!(result.is_ok());
1690 let vars = result.unwrap();
1691 assert!(vars.is_empty());
1692 assert_eq!(vars.scope(), VariablesType::Channel);
1693 }
1694
1695 #[test]
1696 fn variables_from_str_empty_enterprise_block() {
1697 let result = "<>".parse::<Variables>();
1698 assert!(result.is_ok());
1699 let vars = result.unwrap();
1700 assert!(vars.is_empty());
1701 assert_eq!(vars.scope(), VariablesType::Enterprise);
1702 }
1703
1704 #[test]
1707 fn originate_context_named_inline() {
1708 let ep = Endpoint::Sofia(SofiaEndpoint {
1709 profile: "internal".into(),
1710 destination: "123@example.com".into(),
1711 variables: None,
1712 });
1713 let orig = Originate::extension(ep, "1000")
1714 .dialplan(DialplanType::Xml)
1715 .unwrap()
1716 .context("inline");
1717 let wire = orig.to_string();
1718 assert!(wire.contains("XML inline"), "wire: {}", wire);
1719 let parsed: Originate = wire
1720 .parse()
1721 .unwrap();
1722 assert_eq!(parsed.to_string(), wire);
1725 }
1726
1727 #[test]
1728 fn originate_context_named_xml() {
1729 let ep = Endpoint::Sofia(SofiaEndpoint {
1730 profile: "internal".into(),
1731 destination: "123@example.com".into(),
1732 variables: None,
1733 });
1734 let orig = Originate::extension(ep, "1000")
1735 .dialplan(DialplanType::Xml)
1736 .unwrap()
1737 .context("XML");
1738 let wire = orig.to_string();
1739 assert!(wire.contains("XML XML"), "wire: {}", wire);
1741 let parsed: Originate = wire
1742 .parse()
1743 .unwrap();
1744 assert_eq!(parsed.to_string(), wire);
1745 }
1746
1747 #[test]
1748 fn originate_accessors() {
1749 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1750 let cmd = Originate::extension(ep, "1000")
1751 .dialplan(DialplanType::Xml)
1752 .unwrap()
1753 .context("default")
1754 .cid_name("Alice")
1755 .cid_num("5551234")
1756 .timeout(Duration::from_secs(30));
1757
1758 assert!(matches!(cmd.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1759 assert_eq!(cmd.dialplan_type(), Some(&DialplanType::Xml));
1760 assert_eq!(cmd.context_str(), Some("default"));
1761 assert_eq!(cmd.caller_id_name(), Some("Alice"));
1762 assert_eq!(cmd.caller_id_number(), Some("5551234"));
1763 assert_eq!(cmd.timeout_seconds(), Some(30));
1764 }
1765}