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 = (&str, &str)> {
197 self.inner
198 .iter()
199 .map(|(k, v)| (k.as_str(), v.as_str()))
200 }
201
202 pub fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut String)> {
204 self.inner
205 .iter_mut()
206 .map(|(k, v)| (k.as_str(), v))
207 }
208
209 pub fn values_mut(&mut self) -> impl Iterator<Item = &mut String> {
211 self.inner
212 .values_mut()
213 }
214}
215
216#[cfg(feature = "serde")]
217impl serde::Serialize for Variables {
218 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
219 if self.vars_type == VariablesType::Default {
220 self.inner
221 .serialize(serializer)
222 } else {
223 use serde::ser::SerializeStruct;
224 let mut s = serializer.serialize_struct("Variables", 2)?;
225 s.serialize_field("scope", &self.vars_type)?;
226 s.serialize_field("vars", &self.inner)?;
227 s.end()
228 }
229 }
230}
231
232#[cfg(feature = "serde")]
233impl<'de> serde::Deserialize<'de> for Variables {
234 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
235 #[derive(serde::Deserialize)]
236 #[serde(untagged)]
237 enum VariablesRepr {
238 Scoped {
239 scope: VariablesType,
240 vars: IndexMap<String, String>,
241 },
242 Flat(IndexMap<String, String>),
243 }
244
245 match VariablesRepr::deserialize(deserializer)? {
246 VariablesRepr::Scoped { scope, vars } => Ok(Self {
247 vars_type: scope,
248 inner: vars,
249 }),
250 VariablesRepr::Flat(map) => Ok(Self {
251 vars_type: VariablesType::Default,
252 inner: map,
253 }),
254 }
255 }
256}
257
258impl fmt::Display for Variables {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 let (open, close) = self
261 .vars_type
262 .delimiters();
263 f.write_fmt(format_args!("{}", open))?;
264 for (i, (key, value)) in self
265 .inner
266 .iter()
267 .enumerate()
268 {
269 if i > 0 {
270 f.write_str(",")?;
271 }
272 write!(f, "{}={}", key, escape_value(value))?;
273 }
274 f.write_fmt(format_args!("{}", close))
275 }
276}
277
278impl FromStr for Variables {
279 type Err = OriginateError;
280
281 fn from_str(s: &str) -> Result<Self, Self::Err> {
282 let s = s.trim();
283 if s.len() < 2 {
284 return Err(OriginateError::ParseError(
285 "variable block too short".into(),
286 ));
287 }
288
289 let (vars_type, inner_str) = match (s.as_bytes()[0], s.as_bytes()[s.len() - 1]) {
290 (b'{', b'}') => (VariablesType::Default, &s[1..s.len() - 1]),
291 (b'<', b'>') => (VariablesType::Enterprise, &s[1..s.len() - 1]),
292 (b'[', b']') => (VariablesType::Channel, &s[1..s.len() - 1]),
293 _ => {
294 return Err(OriginateError::ParseError(format!(
295 "unknown variable delimiters: {}",
296 s
297 )));
298 }
299 };
300
301 let mut inner = IndexMap::new();
302 if !inner_str.is_empty() {
303 if let Some(rest) = inner_str.strip_prefix("^^") {
304 let sep = rest
305 .chars()
306 .next()
307 .ok_or_else(|| {
308 OriginateError::ParseError("^^ without separator character".into())
309 })?;
310 let (_, close) = vars_type.delimiters();
311 if sep == close || sep == '=' {
312 return Err(OriginateError::ParseError(format!(
313 "invalid ^^ separator: '{sep}'"
314 )));
315 }
316 let var_str = &rest[sep.len_utf8()..];
317 if !var_str.is_empty() {
318 for part in var_str.split(sep) {
319 let (key, value) = part
320 .split_once('=')
321 .ok_or_else(|| {
322 OriginateError::ParseError(format!("missing = in variable: {part}"))
323 })?;
324 inner.insert(key.to_string(), value.to_string());
325 }
326 }
327 } else {
328 for part in split_unescaped_commas(inner_str) {
329 let (key, value) = part
330 .split_once('=')
331 .ok_or_else(|| {
332 OriginateError::ParseError(format!("missing = in variable: {part}"))
333 })?;
334 inner.insert(key.to_string(), unescape_value(value));
335 }
336 }
337 }
338
339 Ok(Self { vars_type, inner })
340 }
341}
342
343fn split_unescaped_commas(s: &str) -> Vec<&str> {
349 let mut parts = Vec::new();
350 let mut start = 0;
351 let bytes = s.as_bytes();
352
353 for i in 0..bytes.len() {
354 if bytes[i] == b',' {
355 let mut backslashes = 0;
356 let mut j = i;
357 while j > 0 && bytes[j - 1] == b'\\' {
358 backslashes += 1;
359 j -= 1;
360 }
361 if backslashes % 2 == 0 {
362 parts.push(&s[start..i]);
363 start = i + 1;
364 }
365 }
366 }
367 parts.push(&s[start..]);
368 parts
369}
370
371pub use super::endpoint::Endpoint;
373
374#[derive(Debug, Clone, PartialEq, Eq)]
380#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
381pub struct Application {
382 name: String,
383 #[cfg_attr(
384 feature = "serde",
385 serde(default, skip_serializing_if = "Option::is_none")
386 )]
387 args: Option<String>,
388}
389
390impl Application {
391 pub fn new(name: impl Into<String>, args: Option<impl Into<String>>) -> Self {
393 Self {
394 name: name.into(),
395 args: args.map(|a| a.into()),
396 }
397 }
398
399 pub fn simple(name: impl Into<String>) -> Self {
401 Self {
402 name: name.into(),
403 args: None,
404 }
405 }
406
407 pub fn park() -> Self {
409 Self::simple("park")
410 }
411
412 pub fn name(&self) -> &str {
414 &self.name
415 }
416
417 pub fn args(&self) -> Option<&str> {
419 self.args
420 .as_deref()
421 }
422
423 pub fn name_mut(&mut self) -> &mut String {
425 &mut self.name
426 }
427
428 pub fn args_mut(&mut self) -> &mut Option<String> {
430 &mut self.args
431 }
432
433 pub fn to_string_with_dialplan(&self, dialplan: &DialplanType) -> String {
435 match dialplan {
436 DialplanType::Inline => match &self.args {
437 Some(args) => format!("{}:{}", self.name, args),
438 None => self
439 .name
440 .clone(),
441 },
442 _ => {
444 let args = self
445 .args
446 .as_deref()
447 .unwrap_or("");
448 format!("&{}({})", self.name, args)
449 }
450 }
451 }
452}
453
454#[derive(Debug, Clone, PartialEq, Eq)]
461#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
462#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
463#[non_exhaustive]
464pub enum OriginateTarget {
465 Extension(String),
467 Application(Application),
469 InlineApplications(Vec<Application>),
471}
472
473impl From<Application> for OriginateTarget {
474 fn from(app: Application) -> Self {
475 Self::Application(app)
476 }
477}
478
479impl From<Vec<Application>> for OriginateTarget {
480 fn from(apps: Vec<Application>) -> Self {
481 Self::InlineApplications(apps)
482 }
483}
484
485#[derive(Debug, Clone, PartialEq, Eq)]
505pub struct Originate {
506 endpoint: Endpoint,
507 target: OriginateTarget,
508 dialplan: Option<DialplanType>,
509 context: Option<String>,
510 cid_name: Option<String>,
511 cid_num: Option<String>,
512 timeout: Option<Duration>,
513}
514
515#[cfg(feature = "serde")]
516mod serde_support {
517 use super::*;
518
519 #[derive(serde::Serialize, serde::Deserialize)]
521 pub(super) struct OriginateRaw {
522 pub endpoint: Endpoint,
523 #[serde(flatten)]
524 pub target: OriginateTarget,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub dialplan: Option<DialplanType>,
527 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub context: Option<String>,
529 #[serde(default, skip_serializing_if = "Option::is_none")]
530 pub cid_name: Option<String>,
531 #[serde(default, skip_serializing_if = "Option::is_none")]
532 pub cid_num: Option<String>,
533 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub timeout_secs: Option<u64>,
535 }
536
537 impl TryFrom<OriginateRaw> for Originate {
538 type Error = OriginateError;
539
540 fn try_from(raw: OriginateRaw) -> Result<Self, Self::Error> {
541 if matches!(raw.target, OriginateTarget::Extension(_))
542 && matches!(raw.dialplan, Some(DialplanType::Inline))
543 {
544 return Err(OriginateError::ExtensionWithInlineDialplan);
545 }
546 if let OriginateTarget::InlineApplications(ref apps) = raw.target {
547 if apps.is_empty() {
548 return Err(OriginateError::EmptyInlineApplications);
549 }
550 }
551 Ok(Self {
552 endpoint: raw.endpoint,
553 target: raw.target,
554 dialplan: raw.dialplan,
555 context: raw.context,
556 cid_name: raw.cid_name,
557 cid_num: raw.cid_num,
558 timeout: raw
559 .timeout_secs
560 .map(Duration::from_secs),
561 })
562 }
563 }
564
565 impl From<Originate> for OriginateRaw {
566 fn from(o: Originate) -> Self {
567 Self {
568 endpoint: o.endpoint,
569 target: o.target,
570 dialplan: o.dialplan,
571 context: o.context,
572 cid_name: o.cid_name,
573 cid_num: o.cid_num,
574 timeout_secs: o
575 .timeout
576 .map(|d| d.as_secs()),
577 }
578 }
579 }
580
581 impl serde::Serialize for Originate {
582 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
583 OriginateRaw::from(self.clone()).serialize(serializer)
584 }
585 }
586
587 impl<'de> serde::Deserialize<'de> for Originate {
588 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
589 let raw = OriginateRaw::deserialize(deserializer)?;
590 Originate::try_from(raw).map_err(serde::de::Error::custom)
591 }
592 }
593}
594
595impl Originate {
596 pub fn extension(endpoint: Endpoint, extension: impl Into<String>) -> Self {
598 Self {
599 endpoint,
600 target: OriginateTarget::Extension(extension.into()),
601 dialplan: None,
602 context: None,
603 cid_name: None,
604 cid_num: None,
605 timeout: None,
606 }
607 }
608
609 pub fn application(endpoint: Endpoint, app: Application) -> Self {
611 Self {
612 endpoint,
613 target: OriginateTarget::Application(app),
614 dialplan: None,
615 context: None,
616 cid_name: None,
617 cid_num: None,
618 timeout: None,
619 }
620 }
621
622 pub fn inline(
626 endpoint: Endpoint,
627 apps: impl IntoIterator<Item = Application>,
628 ) -> Result<Self, OriginateError> {
629 let apps: Vec<Application> = apps
630 .into_iter()
631 .collect();
632 if apps.is_empty() {
633 return Err(OriginateError::EmptyInlineApplications);
634 }
635 Ok(Self {
636 endpoint,
637 target: OriginateTarget::InlineApplications(apps),
638 dialplan: None,
639 context: None,
640 cid_name: None,
641 cid_num: None,
642 timeout: None,
643 })
644 }
645
646 pub fn dialplan(mut self, dp: DialplanType) -> Result<Self, OriginateError> {
650 if matches!(self.target, OriginateTarget::Extension(_)) && dp == DialplanType::Inline {
651 return Err(OriginateError::ExtensionWithInlineDialplan);
652 }
653 self.dialplan = Some(dp);
654 Ok(self)
655 }
656
657 pub fn context(mut self, ctx: impl Into<String>) -> Self {
659 self.context = Some(ctx.into());
660 self
661 }
662
663 pub fn cid_name(mut self, name: impl Into<String>) -> Self {
665 self.cid_name = Some(name.into());
666 self
667 }
668
669 pub fn cid_num(mut self, num: impl Into<String>) -> Self {
671 self.cid_num = Some(num.into());
672 self
673 }
674
675 pub fn timeout(mut self, duration: Duration) -> Self {
678 self.timeout = Some(duration);
679 self
680 }
681
682 pub fn endpoint(&self) -> &Endpoint {
684 &self.endpoint
685 }
686
687 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
689 &mut self.endpoint
690 }
691
692 pub fn target(&self) -> &OriginateTarget {
694 &self.target
695 }
696
697 pub fn target_mut(&mut self) -> &mut OriginateTarget {
699 &mut self.target
700 }
701
702 pub fn dialplan_type(&self) -> Option<&DialplanType> {
704 self.dialplan
705 .as_ref()
706 }
707
708 pub fn context_str(&self) -> Option<&str> {
710 self.context
711 .as_deref()
712 }
713
714 pub fn caller_id_name(&self) -> Option<&str> {
716 self.cid_name
717 .as_deref()
718 }
719
720 pub fn caller_id_number(&self) -> Option<&str> {
722 self.cid_num
723 .as_deref()
724 }
725
726 pub fn timeout_duration(&self) -> Option<Duration> {
728 self.timeout
729 }
730
731 pub fn timeout_seconds(&self) -> Option<u64> {
733 self.timeout
734 .map(|d| d.as_secs())
735 }
736
737 pub fn set_dialplan(&mut self, dp: Option<DialplanType>) {
739 self.dialplan = dp;
740 }
741
742 pub fn set_context(&mut self, ctx: Option<impl Into<String>>) {
744 self.context = ctx.map(|c| c.into());
745 }
746
747 pub fn set_cid_name(&mut self, name: Option<impl Into<String>>) {
749 self.cid_name = name.map(|n| n.into());
750 }
751
752 pub fn set_cid_num(&mut self, num: Option<impl Into<String>>) {
754 self.cid_num = num.map(|n| n.into());
755 }
756
757 pub fn set_timeout(&mut self, timeout: Option<Duration>) {
759 self.timeout = timeout;
760 }
761}
762
763impl fmt::Display for Originate {
764 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
765 let target_str = match &self.target {
766 OriginateTarget::Extension(ext) => ext.clone(),
767 OriginateTarget::Application(app) => app.to_string_with_dialplan(&DialplanType::Xml),
768 OriginateTarget::InlineApplications(apps) => {
769 let parts: Vec<String> = apps
771 .iter()
772 .map(|a| a.to_string_with_dialplan(&DialplanType::Inline))
773 .collect();
774 parts.join(",")
775 }
776 };
777
778 write!(
779 f,
780 "originate {} {}",
781 self.endpoint,
782 originate_quote(&target_str)
783 )?;
784
785 let dialplan = match &self.target {
789 OriginateTarget::InlineApplications(_) => Some(
790 self.dialplan
791 .unwrap_or(DialplanType::Inline),
792 ),
793 _ => self.dialplan,
794 };
795 let has_ctx = self
796 .context
797 .is_some();
798 let has_name = self
799 .cid_name
800 .is_some();
801 let has_num = self
802 .cid_num
803 .is_some();
804 let has_timeout = self
805 .timeout
806 .is_some();
807
808 if dialplan.is_some() || has_ctx || has_name || has_num || has_timeout {
810 let dp = dialplan
811 .as_ref()
812 .cloned()
813 .unwrap_or(DialplanType::Xml);
814 write!(f, " {}", dp)?;
815 }
816 if has_ctx || has_name || has_num || has_timeout {
817 write!(
818 f,
819 " {}",
820 self.context
821 .as_deref()
822 .unwrap_or("default")
823 )?;
824 }
825 if has_name || has_num || has_timeout {
826 let name = self
827 .cid_name
828 .as_deref()
829 .unwrap_or(UNDEF);
830 write!(f, " {}", originate_quote(name))?;
831 }
832 if has_num || has_timeout {
833 let num = self
834 .cid_num
835 .as_deref()
836 .unwrap_or(UNDEF);
837 write!(f, " {}", originate_quote(num))?;
838 }
839 if let Some(ref timeout) = self.timeout {
840 write!(f, " {}", timeout.as_secs())?;
841 }
842 Ok(())
843 }
844}
845
846impl FromStr for Originate {
847 type Err = OriginateError;
848
849 fn from_str(s: &str) -> Result<Self, Self::Err> {
850 let s = s
851 .strip_prefix("originate")
852 .unwrap_or(s)
853 .trim();
854 let mut args = originate_split(s, ' ')?;
855
856 if args.is_empty() {
857 return Err(OriginateError::ParseError("empty originate".into()));
858 }
859
860 let endpoint_str = args.remove(0);
861 let endpoint: Endpoint = endpoint_str.parse()?;
862
863 if args.is_empty() {
864 return Err(OriginateError::ParseError(
865 "missing target in originate".into(),
866 ));
867 }
868
869 let target_str = originate_unquote(&args.remove(0));
870
871 let dialplan = args
872 .first()
873 .and_then(|s| {
874 s.parse::<DialplanType>()
875 .ok()
876 });
877 if dialplan.is_some() {
878 args.remove(0);
879 }
880
881 let target = super::parse_originate_target(&target_str, dialplan.as_ref())?;
882
883 let context = if !args.is_empty() {
884 Some(args.remove(0))
885 } else {
886 None
887 };
888 let cid_name = if !args.is_empty() {
889 let v = args.remove(0);
890 if v.eq_ignore_ascii_case(UNDEF) {
891 None
892 } else {
893 Some(v)
894 }
895 } else {
896 None
897 };
898 let cid_num = if !args.is_empty() {
899 let v = args.remove(0);
900 if v.eq_ignore_ascii_case(UNDEF) {
901 None
902 } else {
903 Some(v)
904 }
905 } else {
906 None
907 };
908 let timeout = if !args.is_empty() {
909 Some(Duration::from_secs(
910 args.remove(0)
911 .parse::<u64>()
912 .map_err(|e| OriginateError::ParseError(format!("invalid timeout: {}", e)))?,
913 ))
914 } else {
915 None
916 };
917
918 let mut orig = match target {
920 OriginateTarget::Extension(ref ext) => Self::extension(endpoint, ext.clone()),
921 OriginateTarget::Application(ref app) => Self::application(endpoint, app.clone()),
922 OriginateTarget::InlineApplications(ref apps) => Self::inline(endpoint, apps.clone())?,
923 };
924 orig.dialplan = dialplan;
925 orig.context = context;
926 orig.cid_name = cid_name;
927 orig.cid_num = cid_num;
928 orig.timeout = timeout;
929 Ok(orig)
930 }
931}
932
933#[derive(Debug, Clone, PartialEq, Eq)]
935#[non_exhaustive]
936pub enum OriginateError {
937 UnclosedQuote(String),
939 ParseError(String),
941 EmptyInlineApplications,
943 ExtensionWithInlineDialplan,
945}
946
947impl std::fmt::Display for OriginateError {
948 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
949 match self {
950 Self::UnclosedQuote(s) => write!(f, "unclosed quote at: {s}"),
951 Self::ParseError(s) => write!(f, "parse error: {s}"),
952 Self::EmptyInlineApplications => {
953 f.write_str("inline originate requires at least one application")
954 }
955 Self::ExtensionWithInlineDialplan => {
956 f.write_str("extension target is incompatible with inline dialplan")
957 }
958 }
959 }
960}
961
962impl std::error::Error for OriginateError {}
963
964#[cfg(test)]
965mod tests {
966 use super::*;
967 use crate::commands::endpoint::{LoopbackEndpoint, SofiaEndpoint, SofiaGateway};
968
969 #[test]
972 fn variables_standard_chars() {
973 let mut vars = Variables::new(VariablesType::Default);
974 vars.insert("test_key", "this_value");
975 let result = vars.to_string();
976 assert!(result.contains("test_key"));
977 assert!(result.contains("this_value"));
978 }
979
980 #[test]
981 fn variables_comma_escaped() {
982 let mut vars = Variables::new(VariablesType::Default);
983 vars.insert("test_key", "this,is,a,value");
984 let result = vars.to_string();
985 assert!(result.contains("\\,"));
986 }
987
988 #[test]
989 fn variables_spaces_quoted() {
990 let mut vars = Variables::new(VariablesType::Default);
991 vars.insert("test_key", "this is a value");
992 let result = vars.to_string();
993 assert_eq!(
994 result
995 .matches('\'')
996 .count(),
997 2
998 );
999 }
1000
1001 #[test]
1002 fn variables_single_quote_escaped() {
1003 let mut vars = Variables::new(VariablesType::Default);
1004 vars.insert("test_key", "let's_this_be_a_value");
1005 let result = vars.to_string();
1006 assert!(result.contains("\\'"));
1007 }
1008
1009 #[test]
1010 fn variables_enterprise_delimiters() {
1011 let mut vars = Variables::new(VariablesType::Enterprise);
1012 vars.insert("k", "v");
1013 let result = vars.to_string();
1014 assert!(result.starts_with('<'));
1015 assert!(result.ends_with('>'));
1016 }
1017
1018 #[test]
1019 fn variables_channel_delimiters() {
1020 let mut vars = Variables::new(VariablesType::Channel);
1021 vars.insert("k", "v");
1022 let result = vars.to_string();
1023 assert!(result.starts_with('['));
1024 assert!(result.ends_with(']'));
1025 }
1026
1027 #[test]
1028 fn variables_default_delimiters() {
1029 let mut vars = Variables::new(VariablesType::Default);
1030 vars.insert("k", "v");
1031 let result = vars.to_string();
1032 assert!(result.starts_with('{'));
1033 assert!(result.ends_with('}'));
1034 }
1035
1036 #[test]
1037 fn variables_parse_round_trip() {
1038 let mut vars = Variables::new(VariablesType::Default);
1039 vars.insert("origination_caller_id_number", "9005551212");
1040 vars.insert("sip_h_Call-Info", "<url>;meta=123,<uri>");
1041 let s = vars.to_string();
1042 let parsed: Variables = s
1043 .parse()
1044 .unwrap();
1045 assert_eq!(
1046 parsed.get("origination_caller_id_number"),
1047 Some("9005551212")
1048 );
1049 assert_eq!(parsed.get("sip_h_Call-Info"), Some("<url>;meta=123,<uri>"));
1050 }
1051
1052 #[test]
1053 fn split_unescaped_commas_basic() {
1054 assert_eq!(split_unescaped_commas("a,b,c"), vec!["a", "b", "c"]);
1055 }
1056
1057 #[test]
1058 fn split_unescaped_commas_escaped() {
1059 assert_eq!(split_unescaped_commas(r"a\,b,c"), vec![r"a\,b", "c"]);
1060 }
1061
1062 #[test]
1063 fn split_unescaped_commas_double_backslash() {
1064 assert_eq!(split_unescaped_commas(r"a\\,b"), vec![r"a\\", "b"]);
1066 }
1067
1068 #[test]
1069 fn split_unescaped_commas_triple_backslash() {
1070 assert_eq!(split_unescaped_commas(r"a\\\,b"), vec![r"a\\\,b"]);
1072 }
1073
1074 #[test]
1075 fn variables_caret_caret_separator() {
1076 let vars: Variables =
1077 "[^^:sip_invite_domain=pbx.example.com:presence_id=1211@pbx.example.com]"
1078 .parse()
1079 .unwrap();
1080 assert_eq!(vars.scope(), VariablesType::Channel);
1081 assert_eq!(vars.get("sip_invite_domain"), Some("pbx.example.com"));
1082 assert_eq!(vars.get("presence_id"), Some("1211@pbx.example.com"));
1083 }
1084
1085 #[test]
1086 fn variables_caret_caret_display_uses_canonical_comma() {
1087 let vars: Variables = "[^^:a=1:b=2]"
1088 .parse()
1089 .unwrap();
1090 assert_eq!(vars.to_string(), "[a=1,b=2]");
1091 }
1092
1093 #[test]
1094 fn variables_caret_caret_default_scope() {
1095 let vars: Variables = "{^^|x=1|y=2}"
1096 .parse()
1097 .unwrap();
1098 assert_eq!(vars.scope(), VariablesType::Default);
1099 assert_eq!(vars.get("x"), Some("1"));
1100 assert_eq!(vars.get("y"), Some("2"));
1101 }
1102
1103 #[test]
1104 fn variables_caret_caret_enterprise_scope() {
1105 let vars: Variables = "<^^;a=1;b=2>"
1106 .parse()
1107 .unwrap();
1108 assert_eq!(vars.scope(), VariablesType::Enterprise);
1109 assert_eq!(vars.get("a"), Some("1"));
1110 }
1111
1112 #[test]
1113 fn variables_caret_caret_no_unescape() {
1114 let vars: Variables = r"[^^:key=val\,ue:other=x]"
1115 .parse()
1116 .unwrap();
1117 assert_eq!(vars.get("key"), Some(r"val\,ue"));
1118 }
1119
1120 #[test]
1121 fn variables_caret_caret_values_with_commas() {
1122 let vars: Variables = "[^^|sip_h_X-Call-Info=<urn:foo>;purpose=bar,<urn:baz>|other=val]"
1123 .parse()
1124 .unwrap();
1125 assert_eq!(
1126 vars.get("sip_h_X-Call-Info"),
1127 Some("<urn:foo>;purpose=bar,<urn:baz>")
1128 );
1129 assert_eq!(vars.get("other"), Some("val"));
1130 }
1131
1132 #[test]
1133 fn variables_caret_caret_empty_vars() {
1134 let vars: Variables = "[^^:]"
1135 .parse()
1136 .unwrap();
1137 assert!(vars.is_empty());
1138 assert_eq!(vars.scope(), VariablesType::Channel);
1139 }
1140
1141 #[test]
1142 fn variables_caret_caret_missing_separator() {
1143 assert!("[^^]"
1144 .parse::<Variables>()
1145 .is_err());
1146 }
1147
1148 #[test]
1149 fn variables_caret_caret_closing_bracket_as_sep() {
1150 assert!("[^^]]"
1151 .parse::<Variables>()
1152 .is_err());
1153 }
1154
1155 #[test]
1156 fn variables_caret_caret_equals_as_sep() {
1157 assert!("[^^=a=1]"
1158 .parse::<Variables>()
1159 .is_err());
1160 }
1161
1162 #[test]
1165 fn endpoint_uri_only() {
1166 let ep = Endpoint::Sofia(SofiaEndpoint {
1167 profile: "internal".into(),
1168 destination: "123@example.com".into(),
1169 variables: None,
1170 });
1171 assert_eq!(ep.to_string(), "sofia/internal/123@example.com");
1172 }
1173
1174 #[test]
1175 fn endpoint_uri_with_variable() {
1176 let mut vars = Variables::new(VariablesType::Default);
1177 vars.insert("one_variable", "1");
1178 let ep = Endpoint::Sofia(SofiaEndpoint {
1179 profile: "internal".into(),
1180 destination: "123@example.com".into(),
1181 variables: Some(vars),
1182 });
1183 assert_eq!(
1184 ep.to_string(),
1185 "{one_variable=1}sofia/internal/123@example.com"
1186 );
1187 }
1188
1189 #[test]
1190 fn endpoint_variable_with_quote() {
1191 let mut vars = Variables::new(VariablesType::Default);
1192 vars.insert("one_variable", "one'quote");
1193 let ep = Endpoint::Sofia(SofiaEndpoint {
1194 profile: "internal".into(),
1195 destination: "123@example.com".into(),
1196 variables: Some(vars),
1197 });
1198 assert_eq!(
1199 ep.to_string(),
1200 "{one_variable=one\\'quote}sofia/internal/123@example.com"
1201 );
1202 }
1203
1204 #[test]
1205 fn loopback_endpoint_display() {
1206 let mut vars = Variables::new(VariablesType::Default);
1207 vars.insert("one_variable", "1");
1208 let ep = Endpoint::Loopback(
1209 LoopbackEndpoint::new("aUri")
1210 .with_context("aContext")
1211 .with_variables(vars),
1212 );
1213 assert_eq!(ep.to_string(), "{one_variable=1}loopback/aUri/aContext");
1214 }
1215
1216 #[test]
1217 fn sofia_gateway_endpoint_display() {
1218 let mut vars = Variables::new(VariablesType::Default);
1219 vars.insert("one_variable", "1");
1220 let ep = Endpoint::SofiaGateway(SofiaGateway {
1221 destination: "aUri".into(),
1222 profile: None,
1223 gateway: "internal".into(),
1224 variables: Some(vars),
1225 });
1226 assert_eq!(
1227 ep.to_string(),
1228 "{one_variable=1}sofia/gateway/internal/aUri"
1229 );
1230 }
1231
1232 #[test]
1235 fn application_xml_format() {
1236 let app = Application::new("testApp", Some("testArg"));
1237 assert_eq!(
1238 app.to_string_with_dialplan(&DialplanType::Xml),
1239 "&testApp(testArg)"
1240 );
1241 }
1242
1243 #[test]
1244 fn application_inline_format() {
1245 let app = Application::new("testApp", Some("testArg"));
1246 assert_eq!(
1247 app.to_string_with_dialplan(&DialplanType::Inline),
1248 "testApp:testArg"
1249 );
1250 }
1251
1252 #[test]
1253 fn application_inline_no_args() {
1254 let app = Application::simple("park");
1255 assert_eq!(app.to_string_with_dialplan(&DialplanType::Inline), "park");
1256 }
1257
1258 #[test]
1261 fn originate_xml_display() {
1262 let ep = Endpoint::Sofia(SofiaEndpoint {
1263 profile: "internal".into(),
1264 destination: "123@example.com".into(),
1265 variables: None,
1266 });
1267 let orig = Originate::application(ep, Application::new("conference", Some("1")))
1268 .dialplan(DialplanType::Xml)
1269 .unwrap();
1270 assert_eq!(
1271 orig.to_string(),
1272 "originate sofia/internal/123@example.com &conference(1) XML"
1273 );
1274 }
1275
1276 #[test]
1277 fn originate_inline_display() {
1278 let ep = Endpoint::Sofia(SofiaEndpoint {
1279 profile: "internal".into(),
1280 destination: "123@example.com".into(),
1281 variables: None,
1282 });
1283 let orig = Originate::inline(ep, vec![Application::new("conference", Some("1"))])
1284 .unwrap()
1285 .dialplan(DialplanType::Inline)
1286 .unwrap();
1287 assert_eq!(
1288 orig.to_string(),
1289 "originate sofia/internal/123@example.com conference:1 inline"
1290 );
1291 }
1292
1293 #[test]
1294 fn originate_extension_display() {
1295 let ep = Endpoint::Sofia(SofiaEndpoint {
1296 profile: "internal".into(),
1297 destination: "123@example.com".into(),
1298 variables: None,
1299 });
1300 let orig = Originate::extension(ep, "1000")
1301 .dialplan(DialplanType::Xml)
1302 .unwrap()
1303 .context("default");
1304 assert_eq!(
1305 orig.to_string(),
1306 "originate sofia/internal/123@example.com 1000 XML default"
1307 );
1308 }
1309
1310 #[test]
1311 fn originate_extension_round_trip() {
1312 let input = "originate sofia/internal/test@example.com 1000 XML default";
1313 let parsed: Originate = input
1314 .parse()
1315 .unwrap();
1316 assert_eq!(parsed.to_string(), input);
1317 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1318 }
1319
1320 #[test]
1321 fn originate_extension_no_dialplan() {
1322 let input = "originate sofia/internal/test@example.com 1000";
1323 let parsed: Originate = input
1324 .parse()
1325 .unwrap();
1326 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1327 assert_eq!(parsed.to_string(), input);
1328 }
1329
1330 #[test]
1331 fn originate_extension_with_inline_errors() {
1332 let ep = Endpoint::Sofia(SofiaEndpoint {
1333 profile: "internal".into(),
1334 destination: "123@example.com".into(),
1335 variables: None,
1336 });
1337 let result = Originate::extension(ep, "1000").dialplan(DialplanType::Inline);
1338 assert!(result.is_err());
1339 }
1340
1341 #[test]
1342 fn originate_empty_inline_errors() {
1343 let ep = Endpoint::Sofia(SofiaEndpoint {
1344 profile: "internal".into(),
1345 destination: "123@example.com".into(),
1346 variables: None,
1347 });
1348 let result = Originate::inline(ep, vec![]);
1349 assert!(result.is_err());
1350 }
1351
1352 #[test]
1353 fn originate_from_string_round_trip() {
1354 let input = "originate {test='variable with quote'}sofia/internal/test@example.com 123";
1355 let orig: Originate = input
1356 .parse()
1357 .unwrap();
1358 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "123"));
1359 assert_eq!(orig.to_string(), input);
1360 }
1361
1362 #[test]
1363 fn originate_socket_app_quoted() {
1364 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1365 let orig = Originate::application(
1366 ep,
1367 Application::new("socket", Some("127.0.0.1:8040 async full")),
1368 );
1369 assert_eq!(
1370 orig.to_string(),
1371 "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'"
1372 );
1373 }
1374
1375 #[test]
1376 fn originate_socket_round_trip() {
1377 let input = "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'";
1378 let parsed: Originate = input
1379 .parse()
1380 .unwrap();
1381 assert_eq!(parsed.to_string(), input);
1382 if let OriginateTarget::Application(ref app) = parsed.target() {
1383 assert_eq!(app.args(), Some("127.0.0.1:8040 async full"));
1384 } else {
1385 panic!("expected Application target");
1386 }
1387 }
1388
1389 #[test]
1390 fn originate_display_round_trip() {
1391 let ep = Endpoint::Sofia(SofiaEndpoint {
1392 profile: "internal".into(),
1393 destination: "123@example.com".into(),
1394 variables: None,
1395 });
1396 let orig = Originate::application(ep, Application::new("conference", Some("1")))
1397 .dialplan(DialplanType::Xml)
1398 .unwrap();
1399 let s = orig.to_string();
1400 let parsed: Originate = s
1401 .parse()
1402 .unwrap();
1403 assert_eq!(parsed.to_string(), s);
1404 }
1405
1406 #[test]
1407 fn originate_inline_no_args_round_trip() {
1408 let input = "originate sofia/internal/123@example.com park inline";
1409 let parsed: Originate = input
1410 .parse()
1411 .unwrap();
1412 assert_eq!(parsed.to_string(), input);
1413 if let OriginateTarget::InlineApplications(ref apps) = parsed.target() {
1414 assert!(apps[0]
1415 .args()
1416 .is_none());
1417 } else {
1418 panic!("expected InlineApplications target");
1419 }
1420 }
1421
1422 #[test]
1423 fn originate_inline_multi_app_round_trip() {
1424 let input =
1425 "originate sofia/internal/123@example.com playback:/tmp/test.wav,hangup:NORMAL_CLEARING inline";
1426 let parsed: Originate = input
1427 .parse()
1428 .unwrap();
1429 assert_eq!(parsed.to_string(), input);
1430 }
1431
1432 #[test]
1433 fn originate_inline_auto_dialplan() {
1434 let ep = Endpoint::Sofia(SofiaEndpoint {
1435 profile: "internal".into(),
1436 destination: "123@example.com".into(),
1437 variables: None,
1438 });
1439 let orig = Originate::inline(ep, vec![Application::simple("park")]).unwrap();
1440 assert!(orig
1441 .to_string()
1442 .contains("inline"));
1443 }
1444
1445 #[test]
1448 fn dialplan_type_display() {
1449 assert_eq!(DialplanType::Inline.to_string(), "inline");
1450 assert_eq!(DialplanType::Xml.to_string(), "XML");
1451 }
1452
1453 #[test]
1454 fn dialplan_type_from_str() {
1455 assert_eq!(
1456 "inline"
1457 .parse::<DialplanType>()
1458 .unwrap(),
1459 DialplanType::Inline
1460 );
1461 assert_eq!(
1462 "XML"
1463 .parse::<DialplanType>()
1464 .unwrap(),
1465 DialplanType::Xml
1466 );
1467 }
1468
1469 #[test]
1470 fn dialplan_type_from_str_case_insensitive() {
1471 assert_eq!(
1472 "xml"
1473 .parse::<DialplanType>()
1474 .unwrap(),
1475 DialplanType::Xml
1476 );
1477 assert_eq!(
1478 "Xml"
1479 .parse::<DialplanType>()
1480 .unwrap(),
1481 DialplanType::Xml
1482 );
1483 assert_eq!(
1484 "INLINE"
1485 .parse::<DialplanType>()
1486 .unwrap(),
1487 DialplanType::Inline
1488 );
1489 assert_eq!(
1490 "Inline"
1491 .parse::<DialplanType>()
1492 .unwrap(),
1493 DialplanType::Inline
1494 );
1495 }
1496
1497 #[test]
1500 fn serde_dialplan_type_xml() {
1501 let json = serde_json::to_string(&DialplanType::Xml).unwrap();
1502 assert_eq!(json, "\"xml\"");
1503 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1504 assert_eq!(parsed, DialplanType::Xml);
1505 }
1506
1507 #[test]
1508 fn serde_dialplan_type_inline() {
1509 let json = serde_json::to_string(&DialplanType::Inline).unwrap();
1510 assert_eq!(json, "\"inline\"");
1511 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1512 assert_eq!(parsed, DialplanType::Inline);
1513 }
1514
1515 #[test]
1516 fn serde_variables_type() {
1517 let json = serde_json::to_string(&VariablesType::Enterprise).unwrap();
1518 assert_eq!(json, "\"enterprise\"");
1519 let parsed: VariablesType = serde_json::from_str(&json).unwrap();
1520 assert_eq!(parsed, VariablesType::Enterprise);
1521 }
1522
1523 #[test]
1524 fn serde_variables_flat_default() {
1525 let mut vars = Variables::new(VariablesType::Default);
1526 vars.insert("key1", "val1");
1527 vars.insert("key2", "val2");
1528 let json = serde_json::to_string(&vars).unwrap();
1529 let parsed: Variables = serde_json::from_str(&json).unwrap();
1531 assert_eq!(parsed.scope(), VariablesType::Default);
1532 assert_eq!(parsed.get("key1"), Some("val1"));
1533 assert_eq!(parsed.get("key2"), Some("val2"));
1534 }
1535
1536 #[test]
1537 fn serde_variables_scoped_enterprise() {
1538 let mut vars = Variables::new(VariablesType::Enterprise);
1539 vars.insert("key1", "val1");
1540 let json = serde_json::to_string(&vars).unwrap();
1541 assert!(json.contains("\"enterprise\""));
1543 let parsed: Variables = serde_json::from_str(&json).unwrap();
1544 assert_eq!(parsed.scope(), VariablesType::Enterprise);
1545 assert_eq!(parsed.get("key1"), Some("val1"));
1546 }
1547
1548 #[test]
1549 fn serde_variables_flat_map_deserializes_as_default() {
1550 let json = r#"{"key1":"val1","key2":"val2"}"#;
1551 let vars: Variables = serde_json::from_str(json).unwrap();
1552 assert_eq!(vars.scope(), VariablesType::Default);
1553 assert_eq!(vars.get("key1"), Some("val1"));
1554 assert_eq!(vars.get("key2"), Some("val2"));
1555 }
1556
1557 #[test]
1558 fn serde_variables_scoped_deserializes() {
1559 let json = r#"{"scope":"channel","vars":{"k":"v"}}"#;
1560 let vars: Variables = serde_json::from_str(json).unwrap();
1561 assert_eq!(vars.scope(), VariablesType::Channel);
1562 assert_eq!(vars.get("k"), Some("v"));
1563 }
1564
1565 #[test]
1566 fn serde_application() {
1567 let app = Application::new("park", None::<&str>);
1568 let json = serde_json::to_string(&app).unwrap();
1569 let parsed: Application = serde_json::from_str(&json).unwrap();
1570 assert_eq!(parsed, app);
1571 }
1572
1573 #[test]
1574 fn serde_application_with_args() {
1575 let app = Application::new("conference", Some("1"));
1576 let json = serde_json::to_string(&app).unwrap();
1577 let parsed: Application = serde_json::from_str(&json).unwrap();
1578 assert_eq!(parsed, app);
1579 }
1580
1581 #[test]
1582 fn serde_application_skips_none_args() {
1583 let app = Application::new("park", None::<&str>);
1584 let json = serde_json::to_string(&app).unwrap();
1585 assert!(!json.contains("args"));
1586 }
1587
1588 #[test]
1589 fn serde_originate_application_round_trip() {
1590 let ep = Endpoint::Sofia(SofiaEndpoint {
1591 profile: "internal".into(),
1592 destination: "123@example.com".into(),
1593 variables: None,
1594 });
1595 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1596 .dialplan(DialplanType::Xml)
1597 .unwrap()
1598 .context("default")
1599 .cid_name("Test")
1600 .cid_num("5551234")
1601 .timeout(Duration::from_secs(30));
1602 let json = serde_json::to_string(&orig).unwrap();
1603 assert!(json.contains("\"application\""));
1604 let parsed: Originate = serde_json::from_str(&json).unwrap();
1605 assert_eq!(parsed, orig);
1606 }
1607
1608 #[test]
1609 fn serde_originate_extension() {
1610 let json = r#"{
1611 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1612 "extension": "1000",
1613 "dialplan": "xml",
1614 "context": "default"
1615 }"#;
1616 let orig: Originate = serde_json::from_str(json).unwrap();
1617 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1618 assert_eq!(
1619 orig.to_string(),
1620 "originate sofia/internal/123@example.com 1000 XML default"
1621 );
1622 }
1623
1624 #[test]
1625 fn serde_originate_extension_with_inline_rejected() {
1626 let json = r#"{
1627 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1628 "extension": "1000",
1629 "dialplan": "inline"
1630 }"#;
1631 let result = serde_json::from_str::<Originate>(json);
1632 assert!(result.is_err());
1633 }
1634
1635 #[test]
1636 fn serde_originate_empty_inline_rejected() {
1637 let json = r#"{
1638 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1639 "inline_applications": []
1640 }"#;
1641 let result = serde_json::from_str::<Originate>(json);
1642 assert!(result.is_err());
1643 }
1644
1645 #[test]
1646 fn serde_originate_inline_applications() {
1647 let json = r#"{
1648 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1649 "inline_applications": [
1650 {"name": "playback", "args": "/tmp/test.wav"},
1651 {"name": "hangup", "args": "NORMAL_CLEARING"}
1652 ]
1653 }"#;
1654 let orig: Originate = serde_json::from_str(json).unwrap();
1655 if let OriginateTarget::InlineApplications(ref apps) = orig.target() {
1656 assert_eq!(apps.len(), 2);
1657 } else {
1658 panic!("expected InlineApplications");
1659 }
1660 assert!(orig
1661 .to_string()
1662 .contains("inline"));
1663 }
1664
1665 #[test]
1666 fn serde_originate_skips_none_fields() {
1667 let ep = Endpoint::Sofia(SofiaEndpoint {
1668 profile: "internal".into(),
1669 destination: "123@example.com".into(),
1670 variables: None,
1671 });
1672 let orig = Originate::application(ep, Application::new("park", None::<&str>));
1673 let json = serde_json::to_string(&orig).unwrap();
1674 assert!(!json.contains("dialplan"));
1675 assert!(!json.contains("context"));
1676 assert!(!json.contains("cid_name"));
1677 assert!(!json.contains("cid_num"));
1678 assert!(!json.contains("timeout"));
1679 }
1680
1681 #[test]
1682 fn serde_originate_to_wire_format() {
1683 let json = r#"{
1684 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1685 "application": {"name": "park"},
1686 "dialplan": "xml",
1687 "context": "default"
1688 }"#;
1689 let orig: Originate = serde_json::from_str(json).unwrap();
1690 let wire = orig.to_string();
1691 assert!(wire.starts_with("originate"));
1692 assert!(wire.contains("sofia/internal/123@example.com"));
1693 assert!(wire.contains("&park()"));
1694 assert!(wire.contains("XML"));
1695 }
1696
1697 #[test]
1700 fn application_simple_no_args() {
1701 let app = Application::simple("park");
1702 assert_eq!(app.name(), "park");
1703 assert!(app
1704 .args()
1705 .is_none());
1706 }
1707
1708 #[test]
1709 fn application_simple_xml_format() {
1710 let app = Application::simple("park");
1711 assert_eq!(app.to_string_with_dialplan(&DialplanType::Xml), "&park()");
1712 }
1713
1714 #[test]
1717 fn originate_target_from_application() {
1718 let target: OriginateTarget = Application::simple("park").into();
1719 assert!(matches!(target, OriginateTarget::Application(_)));
1720 }
1721
1722 #[test]
1723 fn originate_target_from_vec() {
1724 let target: OriginateTarget = vec![
1725 Application::new("conference", Some("1")),
1726 Application::new("hangup", Some("NORMAL_CLEARING")),
1727 ]
1728 .into();
1729 if let OriginateTarget::InlineApplications(apps) = target {
1730 assert_eq!(apps.len(), 2);
1731 } else {
1732 panic!("expected InlineApplications");
1733 }
1734 }
1735
1736 #[test]
1737 fn originate_target_application_wire_format() {
1738 let ep = Endpoint::Sofia(SofiaEndpoint {
1739 profile: "internal".into(),
1740 destination: "123@example.com".into(),
1741 variables: None,
1742 });
1743 let orig = Originate::application(ep, Application::simple("park"));
1744 assert_eq!(
1745 orig.to_string(),
1746 "originate sofia/internal/123@example.com &park()"
1747 );
1748 }
1749
1750 #[test]
1751 fn originate_timeout_only_fills_positional_gaps() {
1752 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1753 let cmd = Originate::application(ep, Application::simple("park"))
1754 .timeout(Duration::from_secs(30));
1755 assert_eq!(
1758 cmd.to_string(),
1759 "originate loopback/9199/test &park() XML default undef undef 30"
1760 );
1761 }
1762
1763 #[test]
1764 fn originate_cid_num_only_fills_preceding_gaps() {
1765 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1766 let cmd = Originate::application(ep, Application::simple("park")).cid_num("5551234");
1767 assert_eq!(
1768 cmd.to_string(),
1769 "originate loopback/9199/test &park() XML default undef 5551234"
1770 );
1771 }
1772
1773 #[test]
1774 fn originate_context_only_fills_dialplan() {
1775 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1776 let cmd = Originate::extension(ep, "1000").context("myctx");
1777 assert_eq!(
1778 cmd.to_string(),
1779 "originate loopback/9199/test 1000 XML myctx"
1780 );
1781 }
1782
1783 #[test]
1789 fn originate_context_gap_filler_round_trip_asymmetry() {
1790 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1791 let cmd = Originate::application(ep, Application::simple("park")).cid_name("Alice");
1792 let wire = cmd.to_string();
1793 assert!(wire.contains("default"), "gap-filler should emit 'default'");
1794
1795 let parsed: Originate = wire
1796 .parse()
1797 .unwrap();
1798 assert_eq!(parsed.context_str(), Some("default"));
1800
1801 assert_eq!(parsed.to_string(), wire);
1803 }
1804
1805 #[test]
1808 fn serde_originate_full_round_trip_with_variables() {
1809 let mut ep_vars = Variables::new(VariablesType::Default);
1810 ep_vars.insert("originate_timeout", "30");
1811 ep_vars.insert("sip_h_X-Custom", "value with spaces");
1812 let ep = Endpoint::SofiaGateway(SofiaGateway {
1813 gateway: "my_provider".into(),
1814 destination: "18005551234".into(),
1815 profile: Some("external".into()),
1816 variables: Some(ep_vars),
1817 });
1818 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1819 .dialplan(DialplanType::Xml)
1820 .unwrap()
1821 .context("public")
1822 .cid_name("Test Caller")
1823 .cid_num("5551234")
1824 .timeout(Duration::from_secs(60));
1825 let json = serde_json::to_string(&orig).unwrap();
1826 let parsed: Originate = serde_json::from_str(&json).unwrap();
1827 assert_eq!(parsed, orig);
1828 assert_eq!(parsed.to_string(), orig.to_string());
1829 }
1830
1831 #[test]
1832 fn serde_originate_inline_round_trip_with_all_fields() {
1833 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1834 let orig = Originate::inline(
1835 ep,
1836 vec![
1837 Application::new("playback", Some("/tmp/test.wav")),
1838 Application::new("hangup", Some("NORMAL_CLEARING")),
1839 ],
1840 )
1841 .unwrap()
1842 .dialplan(DialplanType::Inline)
1843 .unwrap()
1844 .context("default")
1845 .cid_name("IVR")
1846 .cid_num("0000")
1847 .timeout(Duration::from_secs(45));
1848 let json = serde_json::to_string(&orig).unwrap();
1849 let parsed: Originate = serde_json::from_str(&json).unwrap();
1850 assert_eq!(parsed, orig);
1851 assert_eq!(parsed.to_string(), orig.to_string());
1852 }
1853
1854 #[test]
1857 fn variables_from_str_empty_block() {
1858 let result = "{}".parse::<Variables>();
1859 assert!(
1860 result.is_ok(),
1861 "empty variable block should parse successfully"
1862 );
1863 let vars = result.unwrap();
1864 assert!(
1865 vars.is_empty(),
1866 "parsed empty block should have no variables"
1867 );
1868 }
1869
1870 #[test]
1871 fn variables_from_str_empty_channel_block() {
1872 let result = "[]".parse::<Variables>();
1873 assert!(result.is_ok());
1874 let vars = result.unwrap();
1875 assert!(vars.is_empty());
1876 assert_eq!(vars.scope(), VariablesType::Channel);
1877 }
1878
1879 #[test]
1880 fn variables_from_str_empty_enterprise_block() {
1881 let result = "<>".parse::<Variables>();
1882 assert!(result.is_ok());
1883 let vars = result.unwrap();
1884 assert!(vars.is_empty());
1885 assert_eq!(vars.scope(), VariablesType::Enterprise);
1886 }
1887
1888 #[test]
1891 fn originate_context_named_inline() {
1892 let ep = Endpoint::Sofia(SofiaEndpoint {
1893 profile: "internal".into(),
1894 destination: "123@example.com".into(),
1895 variables: None,
1896 });
1897 let orig = Originate::extension(ep, "1000")
1898 .dialplan(DialplanType::Xml)
1899 .unwrap()
1900 .context("inline");
1901 let wire = orig.to_string();
1902 assert!(wire.contains("XML inline"), "wire: {}", wire);
1903 let parsed: Originate = wire
1904 .parse()
1905 .unwrap();
1906 assert_eq!(parsed.to_string(), wire);
1909 }
1910
1911 #[test]
1912 fn originate_context_named_xml() {
1913 let ep = Endpoint::Sofia(SofiaEndpoint {
1914 profile: "internal".into(),
1915 destination: "123@example.com".into(),
1916 variables: None,
1917 });
1918 let orig = Originate::extension(ep, "1000")
1919 .dialplan(DialplanType::Xml)
1920 .unwrap()
1921 .context("XML");
1922 let wire = orig.to_string();
1923 assert!(wire.contains("XML XML"), "wire: {}", wire);
1925 let parsed: Originate = wire
1926 .parse()
1927 .unwrap();
1928 assert_eq!(parsed.to_string(), wire);
1929 }
1930
1931 #[test]
1932 fn originate_accessors() {
1933 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1934 let cmd = Originate::extension(ep, "1000")
1935 .dialplan(DialplanType::Xml)
1936 .unwrap()
1937 .context("default")
1938 .cid_name("Alice")
1939 .cid_num("5551234")
1940 .timeout(Duration::from_secs(30));
1941
1942 assert!(matches!(cmd.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1943 assert_eq!(cmd.dialplan_type(), Some(&DialplanType::Xml));
1944 assert_eq!(cmd.context_str(), Some("default"));
1945 assert_eq!(cmd.caller_id_name(), Some("Alice"));
1946 assert_eq!(cmd.caller_id_number(), Some("5551234"));
1947 assert_eq!(cmd.timeout_seconds(), Some(30));
1948 }
1949}