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