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 park() -> Self {
380 Self::simple("park")
381 }
382
383 pub fn to_string_with_dialplan(&self, dialplan: &DialplanType) -> String {
385 match dialplan {
386 DialplanType::Inline => match &self.args {
387 Some(args) => format!("{}:{}", self.name, args),
388 None => self
389 .name
390 .clone(),
391 },
392 _ => {
394 let args = self
395 .args
396 .as_deref()
397 .unwrap_or("");
398 format!("&{}({})", self.name, args)
399 }
400 }
401 }
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
411#[serde(rename_all = "snake_case")]
412#[non_exhaustive]
413pub enum OriginateTarget {
414 Extension(String),
416 Application(Application),
418 InlineApplications(Vec<Application>),
420}
421
422impl From<Application> for OriginateTarget {
423 fn from(app: Application) -> Self {
424 Self::Application(app)
425 }
426}
427
428impl From<Vec<Application>> for OriginateTarget {
429 fn from(apps: Vec<Application>) -> Self {
430 Self::InlineApplications(apps)
431 }
432}
433
434#[derive(Debug, Clone, PartialEq, Eq)]
454pub struct Originate {
455 endpoint: Endpoint,
456 target: OriginateTarget,
457 dialplan: Option<DialplanType>,
458 context: Option<String>,
459 cid_name: Option<String>,
460 cid_num: Option<String>,
461 timeout: Option<Duration>,
462}
463
464#[derive(Serialize, Deserialize)]
466struct OriginateRaw {
467 endpoint: Endpoint,
468 #[serde(flatten)]
469 target: OriginateTarget,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 dialplan: Option<DialplanType>,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
473 context: Option<String>,
474 #[serde(default, skip_serializing_if = "Option::is_none")]
475 cid_name: Option<String>,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
477 cid_num: Option<String>,
478 #[serde(default, skip_serializing_if = "Option::is_none")]
479 timeout_secs: Option<u64>,
480}
481
482impl TryFrom<OriginateRaw> for Originate {
483 type Error = OriginateError;
484
485 fn try_from(raw: OriginateRaw) -> Result<Self, Self::Error> {
486 if matches!(raw.target, OriginateTarget::Extension(_))
487 && matches!(raw.dialplan, Some(DialplanType::Inline))
488 {
489 return Err(OriginateError::ExtensionWithInlineDialplan);
490 }
491 if let OriginateTarget::InlineApplications(ref apps) = raw.target {
492 if apps.is_empty() {
493 return Err(OriginateError::EmptyInlineApplications);
494 }
495 }
496 Ok(Self {
497 endpoint: raw.endpoint,
498 target: raw.target,
499 dialplan: raw.dialplan,
500 context: raw.context,
501 cid_name: raw.cid_name,
502 cid_num: raw.cid_num,
503 timeout: raw
504 .timeout_secs
505 .map(Duration::from_secs),
506 })
507 }
508}
509
510impl From<Originate> for OriginateRaw {
511 fn from(o: Originate) -> Self {
512 Self {
513 endpoint: o.endpoint,
514 target: o.target,
515 dialplan: o.dialplan,
516 context: o.context,
517 cid_name: o.cid_name,
518 cid_num: o.cid_num,
519 timeout_secs: o
520 .timeout
521 .map(|d| d.as_secs()),
522 }
523 }
524}
525
526impl Serialize for Originate {
527 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
528 OriginateRaw::from(self.clone()).serialize(serializer)
529 }
530}
531
532impl<'de> Deserialize<'de> for Originate {
533 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
534 let raw = OriginateRaw::deserialize(deserializer)?;
535 Originate::try_from(raw).map_err(serde::de::Error::custom)
536 }
537}
538
539impl Originate {
540 pub fn extension(endpoint: Endpoint, extension: impl Into<String>) -> Self {
542 Self {
543 endpoint,
544 target: OriginateTarget::Extension(extension.into()),
545 dialplan: None,
546 context: None,
547 cid_name: None,
548 cid_num: None,
549 timeout: None,
550 }
551 }
552
553 pub fn application(endpoint: Endpoint, app: Application) -> Self {
555 Self {
556 endpoint,
557 target: OriginateTarget::Application(app),
558 dialplan: None,
559 context: None,
560 cid_name: None,
561 cid_num: None,
562 timeout: None,
563 }
564 }
565
566 pub fn inline(
570 endpoint: Endpoint,
571 apps: impl IntoIterator<Item = Application>,
572 ) -> Result<Self, OriginateError> {
573 let apps: Vec<Application> = apps
574 .into_iter()
575 .collect();
576 if apps.is_empty() {
577 return Err(OriginateError::EmptyInlineApplications);
578 }
579 Ok(Self {
580 endpoint,
581 target: OriginateTarget::InlineApplications(apps),
582 dialplan: None,
583 context: None,
584 cid_name: None,
585 cid_num: None,
586 timeout: None,
587 })
588 }
589
590 pub fn dialplan(mut self, dp: DialplanType) -> Result<Self, OriginateError> {
594 if matches!(self.target, OriginateTarget::Extension(_)) && dp == DialplanType::Inline {
595 return Err(OriginateError::ExtensionWithInlineDialplan);
596 }
597 self.dialplan = Some(dp);
598 Ok(self)
599 }
600
601 pub fn context(mut self, ctx: impl Into<String>) -> Self {
603 self.context = Some(ctx.into());
604 self
605 }
606
607 pub fn cid_name(mut self, name: impl Into<String>) -> Self {
609 self.cid_name = Some(name.into());
610 self
611 }
612
613 pub fn cid_num(mut self, num: impl Into<String>) -> Self {
615 self.cid_num = Some(num.into());
616 self
617 }
618
619 pub fn timeout(mut self, duration: Duration) -> Self {
622 self.timeout = Some(duration);
623 self
624 }
625
626 pub fn endpoint(&self) -> &Endpoint {
628 &self.endpoint
629 }
630
631 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
633 &mut self.endpoint
634 }
635
636 pub fn target(&self) -> &OriginateTarget {
638 &self.target
639 }
640
641 pub fn target_mut(&mut self) -> &mut OriginateTarget {
643 &mut self.target
644 }
645
646 pub fn dialplan_type(&self) -> Option<&DialplanType> {
648 self.dialplan
649 .as_ref()
650 }
651
652 pub fn context_str(&self) -> Option<&str> {
654 self.context
655 .as_deref()
656 }
657
658 pub fn caller_id_name(&self) -> Option<&str> {
660 self.cid_name
661 .as_deref()
662 }
663
664 pub fn caller_id_number(&self) -> Option<&str> {
666 self.cid_num
667 .as_deref()
668 }
669
670 pub fn timeout_duration(&self) -> Option<Duration> {
672 self.timeout
673 }
674
675 pub fn timeout_seconds(&self) -> Option<u64> {
677 self.timeout
678 .map(|d| d.as_secs())
679 }
680}
681
682impl fmt::Display for Originate {
683 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684 let target_str = match &self.target {
685 OriginateTarget::Extension(ext) => ext.clone(),
686 OriginateTarget::Application(app) => app.to_string_with_dialplan(&DialplanType::Xml),
687 OriginateTarget::InlineApplications(apps) => {
688 let parts: Vec<String> = apps
690 .iter()
691 .map(|a| a.to_string_with_dialplan(&DialplanType::Inline))
692 .collect();
693 parts.join(",")
694 }
695 };
696
697 write!(
698 f,
699 "originate {} {}",
700 self.endpoint,
701 originate_quote(&target_str)
702 )?;
703
704 let dialplan = match &self.target {
708 OriginateTarget::InlineApplications(_) => Some(
709 self.dialplan
710 .unwrap_or(DialplanType::Inline),
711 ),
712 _ => self.dialplan,
713 };
714 let has_ctx = self
715 .context
716 .is_some();
717 let has_name = self
718 .cid_name
719 .is_some();
720 let has_num = self
721 .cid_num
722 .is_some();
723 let has_timeout = self
724 .timeout
725 .is_some();
726
727 if dialplan.is_some() || has_ctx || has_name || has_num || has_timeout {
728 let dp = dialplan
729 .as_ref()
730 .cloned()
731 .unwrap_or(DialplanType::Xml);
732 write!(f, " {}", dp)?;
733 }
734 if has_ctx || has_name || has_num || has_timeout {
735 write!(
736 f,
737 " {}",
738 self.context
739 .as_deref()
740 .unwrap_or("default")
741 )?;
742 }
743 if has_name || has_num || has_timeout {
744 let name = self
745 .cid_name
746 .as_deref()
747 .unwrap_or(UNDEF);
748 write!(f, " {}", originate_quote(name))?;
749 }
750 if has_num || has_timeout {
751 let num = self
752 .cid_num
753 .as_deref()
754 .unwrap_or(UNDEF);
755 write!(f, " {}", originate_quote(num))?;
756 }
757 if let Some(ref timeout) = self.timeout {
758 write!(f, " {}", timeout.as_secs())?;
759 }
760 Ok(())
761 }
762}
763
764impl FromStr for Originate {
765 type Err = OriginateError;
766
767 fn from_str(s: &str) -> Result<Self, Self::Err> {
768 let s = s
769 .strip_prefix("originate")
770 .unwrap_or(s)
771 .trim();
772 let mut args = originate_split(s, ' ')?;
773
774 if args.is_empty() {
775 return Err(OriginateError::ParseError("empty originate".into()));
776 }
777
778 let endpoint_str = args.remove(0);
779 let endpoint: Endpoint = endpoint_str.parse()?;
780
781 if args.is_empty() {
782 return Err(OriginateError::ParseError(
783 "missing target in originate".into(),
784 ));
785 }
786
787 let target_str = originate_unquote(&args.remove(0));
788
789 let dialplan = args
790 .first()
791 .and_then(|s| {
792 s.parse::<DialplanType>()
793 .ok()
794 });
795 if dialplan.is_some() {
796 args.remove(0);
797 }
798
799 let target = super::parse_originate_target(&target_str, dialplan.as_ref())?;
800
801 let context = if !args.is_empty() {
802 Some(args.remove(0))
803 } else {
804 None
805 };
806 let cid_name = if !args.is_empty() {
807 let v = args.remove(0);
808 if v.eq_ignore_ascii_case(UNDEF) {
809 None
810 } else {
811 Some(v)
812 }
813 } else {
814 None
815 };
816 let cid_num = if !args.is_empty() {
817 let v = args.remove(0);
818 if v.eq_ignore_ascii_case(UNDEF) {
819 None
820 } else {
821 Some(v)
822 }
823 } else {
824 None
825 };
826 let timeout = if !args.is_empty() {
827 Some(Duration::from_secs(
828 args.remove(0)
829 .parse::<u64>()
830 .map_err(|e| OriginateError::ParseError(format!("invalid timeout: {}", e)))?,
831 ))
832 } else {
833 None
834 };
835
836 let mut orig = match target {
838 OriginateTarget::Extension(ref ext) => Self::extension(endpoint, ext.clone()),
839 OriginateTarget::Application(ref app) => Self::application(endpoint, app.clone()),
840 OriginateTarget::InlineApplications(ref apps) => Self::inline(endpoint, apps.clone())?,
841 };
842 orig.dialplan = dialplan;
843 orig.context = context;
844 orig.cid_name = cid_name;
845 orig.cid_num = cid_num;
846 orig.timeout = timeout;
847 Ok(orig)
848 }
849}
850
851#[derive(Debug, thiserror::Error)]
853#[non_exhaustive]
854pub enum OriginateError {
855 #[error("unclosed quote at: {0}")]
857 UnclosedQuote(String),
858 #[error("parse error: {0}")]
860 ParseError(String),
861 #[error("inline originate requires at least one application")]
863 EmptyInlineApplications,
864 #[error("extension target is incompatible with inline dialplan")]
866 ExtensionWithInlineDialplan,
867}
868
869#[cfg(test)]
870mod tests {
871 use super::*;
872 use crate::commands::endpoint::{LoopbackEndpoint, SofiaEndpoint, SofiaGateway};
873
874 #[test]
877 fn variables_standard_chars() {
878 let mut vars = Variables::new(VariablesType::Default);
879 vars.insert("test_key", "this_value");
880 let result = vars.to_string();
881 assert!(result.contains("test_key"));
882 assert!(result.contains("this_value"));
883 }
884
885 #[test]
886 fn variables_comma_escaped() {
887 let mut vars = Variables::new(VariablesType::Default);
888 vars.insert("test_key", "this,is,a,value");
889 let result = vars.to_string();
890 assert!(result.contains("\\,"));
891 }
892
893 #[test]
894 fn variables_spaces_quoted() {
895 let mut vars = Variables::new(VariablesType::Default);
896 vars.insert("test_key", "this is a value");
897 let result = vars.to_string();
898 assert_eq!(
899 result
900 .matches('\'')
901 .count(),
902 2
903 );
904 }
905
906 #[test]
907 fn variables_single_quote_escaped() {
908 let mut vars = Variables::new(VariablesType::Default);
909 vars.insert("test_key", "let's_this_be_a_value");
910 let result = vars.to_string();
911 assert!(result.contains("\\'"));
912 }
913
914 #[test]
915 fn variables_enterprise_delimiters() {
916 let mut vars = Variables::new(VariablesType::Enterprise);
917 vars.insert("k", "v");
918 let result = vars.to_string();
919 assert!(result.starts_with('<'));
920 assert!(result.ends_with('>'));
921 }
922
923 #[test]
924 fn variables_channel_delimiters() {
925 let mut vars = Variables::new(VariablesType::Channel);
926 vars.insert("k", "v");
927 let result = vars.to_string();
928 assert!(result.starts_with('['));
929 assert!(result.ends_with(']'));
930 }
931
932 #[test]
933 fn variables_default_delimiters() {
934 let mut vars = Variables::new(VariablesType::Default);
935 vars.insert("k", "v");
936 let result = vars.to_string();
937 assert!(result.starts_with('{'));
938 assert!(result.ends_with('}'));
939 }
940
941 #[test]
942 fn variables_parse_round_trip() {
943 let mut vars = Variables::new(VariablesType::Default);
944 vars.insert("origination_caller_id_number", "9005551212");
945 vars.insert("sip_h_Call-Info", "<url>;meta=123,<uri>");
946 let s = vars.to_string();
947 let parsed: Variables = s
948 .parse()
949 .unwrap();
950 assert_eq!(
951 parsed.get("origination_caller_id_number"),
952 Some("9005551212")
953 );
954 assert_eq!(parsed.get("sip_h_Call-Info"), Some("<url>;meta=123,<uri>"));
955 }
956
957 #[test]
958 fn split_unescaped_commas_basic() {
959 assert_eq!(split_unescaped_commas("a,b,c"), vec!["a", "b", "c"]);
960 }
961
962 #[test]
963 fn split_unescaped_commas_escaped() {
964 assert_eq!(split_unescaped_commas(r"a\,b,c"), vec![r"a\,b", "c"]);
965 }
966
967 #[test]
968 fn split_unescaped_commas_double_backslash() {
969 assert_eq!(split_unescaped_commas(r"a\\,b"), vec![r"a\\", "b"]);
971 }
972
973 #[test]
974 fn split_unescaped_commas_triple_backslash() {
975 assert_eq!(split_unescaped_commas(r"a\\\,b"), vec![r"a\\\,b"]);
977 }
978
979 #[test]
982 fn endpoint_uri_only() {
983 let ep = Endpoint::Sofia(SofiaEndpoint {
984 profile: "internal".into(),
985 destination: "123@example.com".into(),
986 variables: None,
987 });
988 assert_eq!(ep.to_string(), "sofia/internal/123@example.com");
989 }
990
991 #[test]
992 fn endpoint_uri_with_variable() {
993 let mut vars = Variables::new(VariablesType::Default);
994 vars.insert("one_variable", "1");
995 let ep = Endpoint::Sofia(SofiaEndpoint {
996 profile: "internal".into(),
997 destination: "123@example.com".into(),
998 variables: Some(vars),
999 });
1000 assert_eq!(
1001 ep.to_string(),
1002 "{one_variable=1}sofia/internal/123@example.com"
1003 );
1004 }
1005
1006 #[test]
1007 fn endpoint_variable_with_quote() {
1008 let mut vars = Variables::new(VariablesType::Default);
1009 vars.insert("one_variable", "one'quote");
1010 let ep = Endpoint::Sofia(SofiaEndpoint {
1011 profile: "internal".into(),
1012 destination: "123@example.com".into(),
1013 variables: Some(vars),
1014 });
1015 assert_eq!(
1016 ep.to_string(),
1017 "{one_variable=one\\'quote}sofia/internal/123@example.com"
1018 );
1019 }
1020
1021 #[test]
1022 fn loopback_endpoint_display() {
1023 let mut vars = Variables::new(VariablesType::Default);
1024 vars.insert("one_variable", "1");
1025 let ep = Endpoint::Loopback(
1026 LoopbackEndpoint::new("aUri")
1027 .with_context("aContext")
1028 .with_variables(vars),
1029 );
1030 assert_eq!(ep.to_string(), "{one_variable=1}loopback/aUri/aContext");
1031 }
1032
1033 #[test]
1034 fn sofia_gateway_endpoint_display() {
1035 let mut vars = Variables::new(VariablesType::Default);
1036 vars.insert("one_variable", "1");
1037 let ep = Endpoint::SofiaGateway(SofiaGateway {
1038 destination: "aUri".into(),
1039 profile: None,
1040 gateway: "internal".into(),
1041 variables: Some(vars),
1042 });
1043 assert_eq!(
1044 ep.to_string(),
1045 "{one_variable=1}sofia/gateway/internal/aUri"
1046 );
1047 }
1048
1049 #[test]
1052 fn application_xml_format() {
1053 let app = Application::new("testApp", Some("testArg"));
1054 assert_eq!(
1055 app.to_string_with_dialplan(&DialplanType::Xml),
1056 "&testApp(testArg)"
1057 );
1058 }
1059
1060 #[test]
1061 fn application_inline_format() {
1062 let app = Application::new("testApp", Some("testArg"));
1063 assert_eq!(
1064 app.to_string_with_dialplan(&DialplanType::Inline),
1065 "testApp:testArg"
1066 );
1067 }
1068
1069 #[test]
1070 fn application_inline_no_args() {
1071 let app = Application::simple("park");
1072 assert_eq!(app.to_string_with_dialplan(&DialplanType::Inline), "park");
1073 }
1074
1075 #[test]
1078 fn originate_xml_display() {
1079 let ep = Endpoint::Sofia(SofiaEndpoint {
1080 profile: "internal".into(),
1081 destination: "123@example.com".into(),
1082 variables: None,
1083 });
1084 let orig = Originate::application(ep, Application::new("conference", Some("1")))
1085 .dialplan(DialplanType::Xml)
1086 .unwrap();
1087 assert_eq!(
1088 orig.to_string(),
1089 "originate sofia/internal/123@example.com &conference(1) XML"
1090 );
1091 }
1092
1093 #[test]
1094 fn originate_inline_display() {
1095 let ep = Endpoint::Sofia(SofiaEndpoint {
1096 profile: "internal".into(),
1097 destination: "123@example.com".into(),
1098 variables: None,
1099 });
1100 let orig = Originate::inline(ep, vec![Application::new("conference", Some("1"))])
1101 .unwrap()
1102 .dialplan(DialplanType::Inline)
1103 .unwrap();
1104 assert_eq!(
1105 orig.to_string(),
1106 "originate sofia/internal/123@example.com conference:1 inline"
1107 );
1108 }
1109
1110 #[test]
1111 fn originate_extension_display() {
1112 let ep = Endpoint::Sofia(SofiaEndpoint {
1113 profile: "internal".into(),
1114 destination: "123@example.com".into(),
1115 variables: None,
1116 });
1117 let orig = Originate::extension(ep, "1000")
1118 .dialplan(DialplanType::Xml)
1119 .unwrap()
1120 .context("default");
1121 assert_eq!(
1122 orig.to_string(),
1123 "originate sofia/internal/123@example.com 1000 XML default"
1124 );
1125 }
1126
1127 #[test]
1128 fn originate_extension_round_trip() {
1129 let input = "originate sofia/internal/test@example.com 1000 XML default";
1130 let parsed: Originate = input
1131 .parse()
1132 .unwrap();
1133 assert_eq!(parsed.to_string(), input);
1134 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1135 }
1136
1137 #[test]
1138 fn originate_extension_no_dialplan() {
1139 let input = "originate sofia/internal/test@example.com 1000";
1140 let parsed: Originate = input
1141 .parse()
1142 .unwrap();
1143 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1144 assert_eq!(parsed.to_string(), input);
1145 }
1146
1147 #[test]
1148 fn originate_extension_with_inline_errors() {
1149 let ep = Endpoint::Sofia(SofiaEndpoint {
1150 profile: "internal".into(),
1151 destination: "123@example.com".into(),
1152 variables: None,
1153 });
1154 let result = Originate::extension(ep, "1000").dialplan(DialplanType::Inline);
1155 assert!(result.is_err());
1156 }
1157
1158 #[test]
1159 fn originate_empty_inline_errors() {
1160 let ep = Endpoint::Sofia(SofiaEndpoint {
1161 profile: "internal".into(),
1162 destination: "123@example.com".into(),
1163 variables: None,
1164 });
1165 let result = Originate::inline(ep, vec![]);
1166 assert!(result.is_err());
1167 }
1168
1169 #[test]
1170 fn originate_from_string_round_trip() {
1171 let input = "originate {test='variable with quote'}sofia/internal/test@example.com 123";
1172 let orig: Originate = input
1173 .parse()
1174 .unwrap();
1175 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "123"));
1176 assert_eq!(orig.to_string(), input);
1177 }
1178
1179 #[test]
1180 fn originate_socket_app_quoted() {
1181 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1182 let orig = Originate::application(
1183 ep,
1184 Application::new("socket", Some("127.0.0.1:8040 async full")),
1185 );
1186 assert_eq!(
1187 orig.to_string(),
1188 "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'"
1189 );
1190 }
1191
1192 #[test]
1193 fn originate_socket_round_trip() {
1194 let input = "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'";
1195 let parsed: Originate = input
1196 .parse()
1197 .unwrap();
1198 assert_eq!(parsed.to_string(), input);
1199 if let OriginateTarget::Application(ref app) = parsed.target() {
1200 assert_eq!(
1201 app.args
1202 .as_deref(),
1203 Some("127.0.0.1:8040 async full")
1204 );
1205 } else {
1206 panic!("expected Application target");
1207 }
1208 }
1209
1210 #[test]
1211 fn originate_display_round_trip() {
1212 let ep = Endpoint::Sofia(SofiaEndpoint {
1213 profile: "internal".into(),
1214 destination: "123@example.com".into(),
1215 variables: None,
1216 });
1217 let orig = Originate::application(ep, Application::new("conference", Some("1")))
1218 .dialplan(DialplanType::Xml)
1219 .unwrap();
1220 let s = orig.to_string();
1221 let parsed: Originate = s
1222 .parse()
1223 .unwrap();
1224 assert_eq!(parsed.to_string(), s);
1225 }
1226
1227 #[test]
1228 fn originate_inline_no_args_round_trip() {
1229 let input = "originate sofia/internal/123@example.com park inline";
1230 let parsed: Originate = input
1231 .parse()
1232 .unwrap();
1233 assert_eq!(parsed.to_string(), input);
1234 if let OriginateTarget::InlineApplications(ref apps) = parsed.target() {
1235 assert!(apps[0]
1236 .args
1237 .is_none());
1238 } else {
1239 panic!("expected InlineApplications target");
1240 }
1241 }
1242
1243 #[test]
1244 fn originate_inline_multi_app_round_trip() {
1245 let input =
1246 "originate sofia/internal/123@example.com playback:/tmp/test.wav,hangup:NORMAL_CLEARING inline";
1247 let parsed: Originate = input
1248 .parse()
1249 .unwrap();
1250 assert_eq!(parsed.to_string(), input);
1251 }
1252
1253 #[test]
1254 fn originate_inline_auto_dialplan() {
1255 let ep = Endpoint::Sofia(SofiaEndpoint {
1256 profile: "internal".into(),
1257 destination: "123@example.com".into(),
1258 variables: None,
1259 });
1260 let orig = Originate::inline(ep, vec![Application::simple("park")]).unwrap();
1261 assert!(orig
1262 .to_string()
1263 .contains("inline"));
1264 }
1265
1266 #[test]
1269 fn dialplan_type_display() {
1270 assert_eq!(DialplanType::Inline.to_string(), "inline");
1271 assert_eq!(DialplanType::Xml.to_string(), "XML");
1272 }
1273
1274 #[test]
1275 fn dialplan_type_from_str() {
1276 assert_eq!(
1277 "inline"
1278 .parse::<DialplanType>()
1279 .unwrap(),
1280 DialplanType::Inline
1281 );
1282 assert_eq!(
1283 "XML"
1284 .parse::<DialplanType>()
1285 .unwrap(),
1286 DialplanType::Xml
1287 );
1288 }
1289
1290 #[test]
1291 fn dialplan_type_from_str_case_insensitive() {
1292 assert_eq!(
1293 "xml"
1294 .parse::<DialplanType>()
1295 .unwrap(),
1296 DialplanType::Xml
1297 );
1298 assert_eq!(
1299 "Xml"
1300 .parse::<DialplanType>()
1301 .unwrap(),
1302 DialplanType::Xml
1303 );
1304 assert_eq!(
1305 "INLINE"
1306 .parse::<DialplanType>()
1307 .unwrap(),
1308 DialplanType::Inline
1309 );
1310 assert_eq!(
1311 "Inline"
1312 .parse::<DialplanType>()
1313 .unwrap(),
1314 DialplanType::Inline
1315 );
1316 }
1317
1318 #[test]
1321 fn serde_dialplan_type_xml() {
1322 let json = serde_json::to_string(&DialplanType::Xml).unwrap();
1323 assert_eq!(json, "\"xml\"");
1324 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1325 assert_eq!(parsed, DialplanType::Xml);
1326 }
1327
1328 #[test]
1329 fn serde_dialplan_type_inline() {
1330 let json = serde_json::to_string(&DialplanType::Inline).unwrap();
1331 assert_eq!(json, "\"inline\"");
1332 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1333 assert_eq!(parsed, DialplanType::Inline);
1334 }
1335
1336 #[test]
1337 fn serde_variables_type() {
1338 let json = serde_json::to_string(&VariablesType::Enterprise).unwrap();
1339 assert_eq!(json, "\"enterprise\"");
1340 let parsed: VariablesType = serde_json::from_str(&json).unwrap();
1341 assert_eq!(parsed, VariablesType::Enterprise);
1342 }
1343
1344 #[test]
1345 fn serde_variables_flat_default() {
1346 let mut vars = Variables::new(VariablesType::Default);
1347 vars.insert("key1", "val1");
1348 vars.insert("key2", "val2");
1349 let json = serde_json::to_string(&vars).unwrap();
1350 let parsed: Variables = serde_json::from_str(&json).unwrap();
1352 assert_eq!(parsed.scope(), VariablesType::Default);
1353 assert_eq!(parsed.get("key1"), Some("val1"));
1354 assert_eq!(parsed.get("key2"), Some("val2"));
1355 }
1356
1357 #[test]
1358 fn serde_variables_scoped_enterprise() {
1359 let mut vars = Variables::new(VariablesType::Enterprise);
1360 vars.insert("key1", "val1");
1361 let json = serde_json::to_string(&vars).unwrap();
1362 assert!(json.contains("\"enterprise\""));
1364 let parsed: Variables = serde_json::from_str(&json).unwrap();
1365 assert_eq!(parsed.scope(), VariablesType::Enterprise);
1366 assert_eq!(parsed.get("key1"), Some("val1"));
1367 }
1368
1369 #[test]
1370 fn serde_variables_flat_map_deserializes_as_default() {
1371 let json = r#"{"key1":"val1","key2":"val2"}"#;
1372 let vars: Variables = serde_json::from_str(json).unwrap();
1373 assert_eq!(vars.scope(), VariablesType::Default);
1374 assert_eq!(vars.get("key1"), Some("val1"));
1375 assert_eq!(vars.get("key2"), Some("val2"));
1376 }
1377
1378 #[test]
1379 fn serde_variables_scoped_deserializes() {
1380 let json = r#"{"scope":"channel","vars":{"k":"v"}}"#;
1381 let vars: Variables = serde_json::from_str(json).unwrap();
1382 assert_eq!(vars.scope(), VariablesType::Channel);
1383 assert_eq!(vars.get("k"), Some("v"));
1384 }
1385
1386 #[test]
1387 fn serde_application() {
1388 let app = Application::new("park", None::<&str>);
1389 let json = serde_json::to_string(&app).unwrap();
1390 let parsed: Application = serde_json::from_str(&json).unwrap();
1391 assert_eq!(parsed, app);
1392 }
1393
1394 #[test]
1395 fn serde_application_with_args() {
1396 let app = Application::new("conference", Some("1"));
1397 let json = serde_json::to_string(&app).unwrap();
1398 let parsed: Application = serde_json::from_str(&json).unwrap();
1399 assert_eq!(parsed, app);
1400 }
1401
1402 #[test]
1403 fn serde_application_skips_none_args() {
1404 let app = Application::new("park", None::<&str>);
1405 let json = serde_json::to_string(&app).unwrap();
1406 assert!(!json.contains("args"));
1407 }
1408
1409 #[test]
1410 fn serde_originate_application_round_trip() {
1411 let ep = Endpoint::Sofia(SofiaEndpoint {
1412 profile: "internal".into(),
1413 destination: "123@example.com".into(),
1414 variables: None,
1415 });
1416 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1417 .dialplan(DialplanType::Xml)
1418 .unwrap()
1419 .context("default")
1420 .cid_name("Test")
1421 .cid_num("5551234")
1422 .timeout(Duration::from_secs(30));
1423 let json = serde_json::to_string(&orig).unwrap();
1424 assert!(json.contains("\"application\""));
1425 let parsed: Originate = serde_json::from_str(&json).unwrap();
1426 assert_eq!(parsed, orig);
1427 }
1428
1429 #[test]
1430 fn serde_originate_extension() {
1431 let json = r#"{
1432 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1433 "extension": "1000",
1434 "dialplan": "xml",
1435 "context": "default"
1436 }"#;
1437 let orig: Originate = serde_json::from_str(json).unwrap();
1438 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1439 assert_eq!(
1440 orig.to_string(),
1441 "originate sofia/internal/123@example.com 1000 XML default"
1442 );
1443 }
1444
1445 #[test]
1446 fn serde_originate_extension_with_inline_rejected() {
1447 let json = r#"{
1448 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1449 "extension": "1000",
1450 "dialplan": "inline"
1451 }"#;
1452 let result = serde_json::from_str::<Originate>(json);
1453 assert!(result.is_err());
1454 }
1455
1456 #[test]
1457 fn serde_originate_empty_inline_rejected() {
1458 let json = r#"{
1459 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1460 "inline_applications": []
1461 }"#;
1462 let result = serde_json::from_str::<Originate>(json);
1463 assert!(result.is_err());
1464 }
1465
1466 #[test]
1467 fn serde_originate_inline_applications() {
1468 let json = r#"{
1469 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1470 "inline_applications": [
1471 {"name": "playback", "args": "/tmp/test.wav"},
1472 {"name": "hangup", "args": "NORMAL_CLEARING"}
1473 ]
1474 }"#;
1475 let orig: Originate = serde_json::from_str(json).unwrap();
1476 if let OriginateTarget::InlineApplications(ref apps) = orig.target() {
1477 assert_eq!(apps.len(), 2);
1478 } else {
1479 panic!("expected InlineApplications");
1480 }
1481 assert!(orig
1482 .to_string()
1483 .contains("inline"));
1484 }
1485
1486 #[test]
1487 fn serde_originate_skips_none_fields() {
1488 let ep = Endpoint::Sofia(SofiaEndpoint {
1489 profile: "internal".into(),
1490 destination: "123@example.com".into(),
1491 variables: None,
1492 });
1493 let orig = Originate::application(ep, Application::new("park", None::<&str>));
1494 let json = serde_json::to_string(&orig).unwrap();
1495 assert!(!json.contains("dialplan"));
1496 assert!(!json.contains("context"));
1497 assert!(!json.contains("cid_name"));
1498 assert!(!json.contains("cid_num"));
1499 assert!(!json.contains("timeout"));
1500 }
1501
1502 #[test]
1503 fn serde_originate_to_wire_format() {
1504 let json = r#"{
1505 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1506 "application": {"name": "park"},
1507 "dialplan": "xml",
1508 "context": "default"
1509 }"#;
1510 let orig: Originate = serde_json::from_str(json).unwrap();
1511 let wire = orig.to_string();
1512 assert!(wire.starts_with("originate"));
1513 assert!(wire.contains("sofia/internal/123@example.com"));
1514 assert!(wire.contains("&park()"));
1515 assert!(wire.contains("XML"));
1516 }
1517
1518 #[test]
1521 fn application_simple_no_args() {
1522 let app = Application::simple("park");
1523 assert_eq!(app.name, "park");
1524 assert!(app
1525 .args
1526 .is_none());
1527 }
1528
1529 #[test]
1530 fn application_simple_xml_format() {
1531 let app = Application::simple("park");
1532 assert_eq!(app.to_string_with_dialplan(&DialplanType::Xml), "&park()");
1533 }
1534
1535 #[test]
1538 fn originate_target_from_application() {
1539 let target: OriginateTarget = Application::simple("park").into();
1540 assert!(matches!(target, OriginateTarget::Application(_)));
1541 }
1542
1543 #[test]
1544 fn originate_target_from_vec() {
1545 let target: OriginateTarget = vec![
1546 Application::new("conference", Some("1")),
1547 Application::new("hangup", Some("NORMAL_CLEARING")),
1548 ]
1549 .into();
1550 if let OriginateTarget::InlineApplications(apps) = target {
1551 assert_eq!(apps.len(), 2);
1552 } else {
1553 panic!("expected InlineApplications");
1554 }
1555 }
1556
1557 #[test]
1558 fn originate_target_application_wire_format() {
1559 let ep = Endpoint::Sofia(SofiaEndpoint {
1560 profile: "internal".into(),
1561 destination: "123@example.com".into(),
1562 variables: None,
1563 });
1564 let orig = Originate::application(ep, Application::simple("park"));
1565 assert_eq!(
1566 orig.to_string(),
1567 "originate sofia/internal/123@example.com &park()"
1568 );
1569 }
1570
1571 #[test]
1572 fn originate_timeout_only_fills_positional_gaps() {
1573 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1574 let cmd = Originate::application(ep, Application::simple("park"))
1575 .timeout(Duration::from_secs(30));
1576 assert_eq!(
1579 cmd.to_string(),
1580 "originate loopback/9199/test &park() XML default undef undef 30"
1581 );
1582 }
1583
1584 #[test]
1585 fn originate_cid_num_only_fills_preceding_gaps() {
1586 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1587 let cmd = Originate::application(ep, Application::simple("park")).cid_num("5551234");
1588 assert_eq!(
1589 cmd.to_string(),
1590 "originate loopback/9199/test &park() XML default undef 5551234"
1591 );
1592 }
1593
1594 #[test]
1595 fn originate_context_only_fills_dialplan() {
1596 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1597 let cmd = Originate::extension(ep, "1000").context("myctx");
1598 assert_eq!(
1599 cmd.to_string(),
1600 "originate loopback/9199/test 1000 XML myctx"
1601 );
1602 }
1603
1604 #[test]
1610 fn originate_context_gap_filler_round_trip_asymmetry() {
1611 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1612 let cmd = Originate::application(ep, Application::simple("park")).cid_name("Alice");
1613 let wire = cmd.to_string();
1614 assert!(wire.contains("default"), "gap-filler should emit 'default'");
1615
1616 let parsed: Originate = wire
1617 .parse()
1618 .unwrap();
1619 assert_eq!(parsed.context_str(), Some("default"));
1621
1622 assert_eq!(parsed.to_string(), wire);
1624 }
1625
1626 #[test]
1629 fn serde_originate_full_round_trip_with_variables() {
1630 let mut ep_vars = Variables::new(VariablesType::Default);
1631 ep_vars.insert("originate_timeout", "30");
1632 ep_vars.insert("sip_h_X-Custom", "value with spaces");
1633 let ep = Endpoint::SofiaGateway(SofiaGateway {
1634 gateway: "my_provider".into(),
1635 destination: "18005551234".into(),
1636 profile: Some("external".into()),
1637 variables: Some(ep_vars),
1638 });
1639 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1640 .dialplan(DialplanType::Xml)
1641 .unwrap()
1642 .context("public")
1643 .cid_name("Test Caller")
1644 .cid_num("5551234")
1645 .timeout(Duration::from_secs(60));
1646 let json = serde_json::to_string(&orig).unwrap();
1647 let parsed: Originate = serde_json::from_str(&json).unwrap();
1648 assert_eq!(parsed, orig);
1649 assert_eq!(parsed.to_string(), orig.to_string());
1650 }
1651
1652 #[test]
1653 fn serde_originate_inline_round_trip_with_all_fields() {
1654 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1655 let orig = Originate::inline(
1656 ep,
1657 vec![
1658 Application::new("playback", Some("/tmp/test.wav")),
1659 Application::new("hangup", Some("NORMAL_CLEARING")),
1660 ],
1661 )
1662 .unwrap()
1663 .dialplan(DialplanType::Inline)
1664 .unwrap()
1665 .context("default")
1666 .cid_name("IVR")
1667 .cid_num("0000")
1668 .timeout(Duration::from_secs(45));
1669 let json = serde_json::to_string(&orig).unwrap();
1670 let parsed: Originate = serde_json::from_str(&json).unwrap();
1671 assert_eq!(parsed, orig);
1672 assert_eq!(parsed.to_string(), orig.to_string());
1673 }
1674
1675 #[test]
1678 fn variables_from_str_empty_block() {
1679 let result = "{}".parse::<Variables>();
1680 assert!(
1681 result.is_ok(),
1682 "empty variable block should parse successfully"
1683 );
1684 let vars = result.unwrap();
1685 assert!(
1686 vars.is_empty(),
1687 "parsed empty block should have no variables"
1688 );
1689 }
1690
1691 #[test]
1692 fn variables_from_str_empty_channel_block() {
1693 let result = "[]".parse::<Variables>();
1694 assert!(result.is_ok());
1695 let vars = result.unwrap();
1696 assert!(vars.is_empty());
1697 assert_eq!(vars.scope(), VariablesType::Channel);
1698 }
1699
1700 #[test]
1701 fn variables_from_str_empty_enterprise_block() {
1702 let result = "<>".parse::<Variables>();
1703 assert!(result.is_ok());
1704 let vars = result.unwrap();
1705 assert!(vars.is_empty());
1706 assert_eq!(vars.scope(), VariablesType::Enterprise);
1707 }
1708
1709 #[test]
1712 fn originate_context_named_inline() {
1713 let ep = Endpoint::Sofia(SofiaEndpoint {
1714 profile: "internal".into(),
1715 destination: "123@example.com".into(),
1716 variables: None,
1717 });
1718 let orig = Originate::extension(ep, "1000")
1719 .dialplan(DialplanType::Xml)
1720 .unwrap()
1721 .context("inline");
1722 let wire = orig.to_string();
1723 assert!(wire.contains("XML inline"), "wire: {}", wire);
1724 let parsed: Originate = wire
1725 .parse()
1726 .unwrap();
1727 assert_eq!(parsed.to_string(), wire);
1730 }
1731
1732 #[test]
1733 fn originate_context_named_xml() {
1734 let ep = Endpoint::Sofia(SofiaEndpoint {
1735 profile: "internal".into(),
1736 destination: "123@example.com".into(),
1737 variables: None,
1738 });
1739 let orig = Originate::extension(ep, "1000")
1740 .dialplan(DialplanType::Xml)
1741 .unwrap()
1742 .context("XML");
1743 let wire = orig.to_string();
1744 assert!(wire.contains("XML XML"), "wire: {}", wire);
1746 let parsed: Originate = wire
1747 .parse()
1748 .unwrap();
1749 assert_eq!(parsed.to_string(), wire);
1750 }
1751
1752 #[test]
1753 fn originate_accessors() {
1754 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1755 let cmd = Originate::extension(ep, "1000")
1756 .dialplan(DialplanType::Xml)
1757 .unwrap()
1758 .context("default")
1759 .cid_name("Alice")
1760 .cid_num("5551234")
1761 .timeout(Duration::from_secs(30));
1762
1763 assert!(matches!(cmd.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1764 assert_eq!(cmd.dialplan_type(), Some(&DialplanType::Xml));
1765 assert_eq!(cmd.context_str(), Some("default"));
1766 assert_eq!(cmd.caller_id_name(), Some("Alice"));
1767 assert_eq!(cmd.caller_id_number(), Some("5551234"));
1768 assert_eq!(cmd.timeout_seconds(), Some(30));
1769 }
1770}