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 {
809 let dp = dialplan
810 .as_ref()
811 .cloned()
812 .unwrap_or(DialplanType::Xml);
813 write!(f, " {}", dp)?;
814 }
815 if has_ctx || has_name || has_num || has_timeout {
816 write!(
817 f,
818 " {}",
819 self.context
820 .as_deref()
821 .unwrap_or("default")
822 )?;
823 }
824 if has_name || has_num || has_timeout {
825 let name = self
826 .cid_name
827 .as_deref()
828 .unwrap_or(UNDEF);
829 write!(f, " {}", originate_quote(name))?;
830 }
831 if has_num || has_timeout {
832 let num = self
833 .cid_num
834 .as_deref()
835 .unwrap_or(UNDEF);
836 write!(f, " {}", originate_quote(num))?;
837 }
838 if let Some(ref timeout) = self.timeout {
839 write!(f, " {}", timeout.as_secs())?;
840 }
841 Ok(())
842 }
843}
844
845impl FromStr for Originate {
846 type Err = OriginateError;
847
848 fn from_str(s: &str) -> Result<Self, Self::Err> {
849 let s = s
850 .strip_prefix("originate")
851 .unwrap_or(s)
852 .trim();
853 let mut args = originate_split(s, ' ')?;
854
855 if args.is_empty() {
856 return Err(OriginateError::ParseError("empty originate".into()));
857 }
858
859 let endpoint_str = args.remove(0);
860 let endpoint: Endpoint = endpoint_str.parse()?;
861
862 if args.is_empty() {
863 return Err(OriginateError::ParseError(
864 "missing target in originate".into(),
865 ));
866 }
867
868 let target_str = originate_unquote(&args.remove(0));
869
870 let dialplan = args
871 .first()
872 .and_then(|s| {
873 s.parse::<DialplanType>()
874 .ok()
875 });
876 if dialplan.is_some() {
877 args.remove(0);
878 }
879
880 let target = super::parse_originate_target(&target_str, dialplan.as_ref())?;
881
882 let context = if !args.is_empty() {
883 Some(args.remove(0))
884 } else {
885 None
886 };
887 let cid_name = if !args.is_empty() {
888 let v = args.remove(0);
889 if v.eq_ignore_ascii_case(UNDEF) {
890 None
891 } else {
892 Some(v)
893 }
894 } else {
895 None
896 };
897 let cid_num = if !args.is_empty() {
898 let v = args.remove(0);
899 if v.eq_ignore_ascii_case(UNDEF) {
900 None
901 } else {
902 Some(v)
903 }
904 } else {
905 None
906 };
907 let timeout = if !args.is_empty() {
908 Some(Duration::from_secs(
909 args.remove(0)
910 .parse::<u64>()
911 .map_err(|e| OriginateError::ParseError(format!("invalid timeout: {}", e)))?,
912 ))
913 } else {
914 None
915 };
916
917 let mut orig = match target {
919 OriginateTarget::Extension(ref ext) => Self::extension(endpoint, ext.clone()),
920 OriginateTarget::Application(ref app) => Self::application(endpoint, app.clone()),
921 OriginateTarget::InlineApplications(ref apps) => Self::inline(endpoint, apps.clone())?,
922 };
923 orig.dialplan = dialplan;
924 orig.context = context;
925 orig.cid_name = cid_name;
926 orig.cid_num = cid_num;
927 orig.timeout = timeout;
928 Ok(orig)
929 }
930}
931
932#[derive(Debug, Clone, PartialEq, Eq)]
934#[non_exhaustive]
935pub enum OriginateError {
936 UnclosedQuote(String),
938 ParseError(String),
940 EmptyInlineApplications,
942 ExtensionWithInlineDialplan,
944}
945
946impl std::fmt::Display for OriginateError {
947 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
948 match self {
949 Self::UnclosedQuote(s) => write!(f, "unclosed quote at: {s}"),
950 Self::ParseError(s) => write!(f, "parse error: {s}"),
951 Self::EmptyInlineApplications => {
952 f.write_str("inline originate requires at least one application")
953 }
954 Self::ExtensionWithInlineDialplan => {
955 f.write_str("extension target is incompatible with inline dialplan")
956 }
957 }
958 }
959}
960
961impl std::error::Error for OriginateError {}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966 use crate::commands::endpoint::{LoopbackEndpoint, SofiaEndpoint, SofiaGateway};
967
968 #[test]
971 fn variables_standard_chars() {
972 let mut vars = Variables::new(VariablesType::Default);
973 vars.insert("test_key", "this_value");
974 let result = vars.to_string();
975 assert!(result.contains("test_key"));
976 assert!(result.contains("this_value"));
977 }
978
979 #[test]
980 fn variables_comma_escaped() {
981 let mut vars = Variables::new(VariablesType::Default);
982 vars.insert("test_key", "this,is,a,value");
983 let result = vars.to_string();
984 assert!(result.contains("\\,"));
985 }
986
987 #[test]
988 fn variables_spaces_quoted() {
989 let mut vars = Variables::new(VariablesType::Default);
990 vars.insert("test_key", "this is a value");
991 let result = vars.to_string();
992 assert_eq!(
993 result
994 .matches('\'')
995 .count(),
996 2
997 );
998 }
999
1000 #[test]
1001 fn variables_single_quote_escaped() {
1002 let mut vars = Variables::new(VariablesType::Default);
1003 vars.insert("test_key", "let's_this_be_a_value");
1004 let result = vars.to_string();
1005 assert!(result.contains("\\'"));
1006 }
1007
1008 #[test]
1009 fn variables_enterprise_delimiters() {
1010 let mut vars = Variables::new(VariablesType::Enterprise);
1011 vars.insert("k", "v");
1012 let result = vars.to_string();
1013 assert!(result.starts_with('<'));
1014 assert!(result.ends_with('>'));
1015 }
1016
1017 #[test]
1018 fn variables_channel_delimiters() {
1019 let mut vars = Variables::new(VariablesType::Channel);
1020 vars.insert("k", "v");
1021 let result = vars.to_string();
1022 assert!(result.starts_with('['));
1023 assert!(result.ends_with(']'));
1024 }
1025
1026 #[test]
1027 fn variables_default_delimiters() {
1028 let mut vars = Variables::new(VariablesType::Default);
1029 vars.insert("k", "v");
1030 let result = vars.to_string();
1031 assert!(result.starts_with('{'));
1032 assert!(result.ends_with('}'));
1033 }
1034
1035 #[test]
1036 fn variables_parse_round_trip() {
1037 let mut vars = Variables::new(VariablesType::Default);
1038 vars.insert("origination_caller_id_number", "9005551212");
1039 vars.insert("sip_h_Call-Info", "<url>;meta=123,<uri>");
1040 let s = vars.to_string();
1041 let parsed: Variables = s
1042 .parse()
1043 .unwrap();
1044 assert_eq!(
1045 parsed.get("origination_caller_id_number"),
1046 Some("9005551212")
1047 );
1048 assert_eq!(parsed.get("sip_h_Call-Info"), Some("<url>;meta=123,<uri>"));
1049 }
1050
1051 #[test]
1052 fn split_unescaped_commas_basic() {
1053 assert_eq!(split_unescaped_commas("a,b,c"), vec!["a", "b", "c"]);
1054 }
1055
1056 #[test]
1057 fn split_unescaped_commas_escaped() {
1058 assert_eq!(split_unescaped_commas(r"a\,b,c"), vec![r"a\,b", "c"]);
1059 }
1060
1061 #[test]
1062 fn split_unescaped_commas_double_backslash() {
1063 assert_eq!(split_unescaped_commas(r"a\\,b"), vec![r"a\\", "b"]);
1065 }
1066
1067 #[test]
1068 fn split_unescaped_commas_triple_backslash() {
1069 assert_eq!(split_unescaped_commas(r"a\\\,b"), vec![r"a\\\,b"]);
1071 }
1072
1073 #[test]
1074 fn variables_caret_caret_separator() {
1075 let vars: Variables =
1076 "[^^:sip_invite_domain=pbx.example.com:presence_id=1211@pbx.example.com]"
1077 .parse()
1078 .unwrap();
1079 assert_eq!(vars.scope(), VariablesType::Channel);
1080 assert_eq!(vars.get("sip_invite_domain"), Some("pbx.example.com"));
1081 assert_eq!(vars.get("presence_id"), Some("1211@pbx.example.com"));
1082 }
1083
1084 #[test]
1085 fn variables_caret_caret_display_uses_canonical_comma() {
1086 let vars: Variables = "[^^:a=1:b=2]"
1087 .parse()
1088 .unwrap();
1089 assert_eq!(vars.to_string(), "[a=1,b=2]");
1090 }
1091
1092 #[test]
1093 fn variables_caret_caret_default_scope() {
1094 let vars: Variables = "{^^|x=1|y=2}"
1095 .parse()
1096 .unwrap();
1097 assert_eq!(vars.scope(), VariablesType::Default);
1098 assert_eq!(vars.get("x"), Some("1"));
1099 assert_eq!(vars.get("y"), Some("2"));
1100 }
1101
1102 #[test]
1103 fn variables_caret_caret_enterprise_scope() {
1104 let vars: Variables = "<^^;a=1;b=2>"
1105 .parse()
1106 .unwrap();
1107 assert_eq!(vars.scope(), VariablesType::Enterprise);
1108 assert_eq!(vars.get("a"), Some("1"));
1109 }
1110
1111 #[test]
1112 fn variables_caret_caret_no_unescape() {
1113 let vars: Variables = r"[^^:key=val\,ue:other=x]"
1114 .parse()
1115 .unwrap();
1116 assert_eq!(vars.get("key"), Some(r"val\,ue"));
1117 }
1118
1119 #[test]
1120 fn variables_caret_caret_values_with_commas() {
1121 let vars: Variables = "[^^|sip_h_X-Call-Info=<urn:foo>;purpose=bar,<urn:baz>|other=val]"
1122 .parse()
1123 .unwrap();
1124 assert_eq!(
1125 vars.get("sip_h_X-Call-Info"),
1126 Some("<urn:foo>;purpose=bar,<urn:baz>")
1127 );
1128 assert_eq!(vars.get("other"), Some("val"));
1129 }
1130
1131 #[test]
1132 fn variables_caret_caret_empty_vars() {
1133 let vars: Variables = "[^^:]"
1134 .parse()
1135 .unwrap();
1136 assert!(vars.is_empty());
1137 assert_eq!(vars.scope(), VariablesType::Channel);
1138 }
1139
1140 #[test]
1141 fn variables_caret_caret_missing_separator() {
1142 assert!("[^^]"
1143 .parse::<Variables>()
1144 .is_err());
1145 }
1146
1147 #[test]
1148 fn variables_caret_caret_closing_bracket_as_sep() {
1149 assert!("[^^]]"
1150 .parse::<Variables>()
1151 .is_err());
1152 }
1153
1154 #[test]
1155 fn variables_caret_caret_equals_as_sep() {
1156 assert!("[^^=a=1]"
1157 .parse::<Variables>()
1158 .is_err());
1159 }
1160
1161 #[test]
1164 fn endpoint_uri_only() {
1165 let ep = Endpoint::Sofia(SofiaEndpoint {
1166 profile: "internal".into(),
1167 destination: "123@example.com".into(),
1168 variables: None,
1169 });
1170 assert_eq!(ep.to_string(), "sofia/internal/123@example.com");
1171 }
1172
1173 #[test]
1174 fn endpoint_uri_with_variable() {
1175 let mut vars = Variables::new(VariablesType::Default);
1176 vars.insert("one_variable", "1");
1177 let ep = Endpoint::Sofia(SofiaEndpoint {
1178 profile: "internal".into(),
1179 destination: "123@example.com".into(),
1180 variables: Some(vars),
1181 });
1182 assert_eq!(
1183 ep.to_string(),
1184 "{one_variable=1}sofia/internal/123@example.com"
1185 );
1186 }
1187
1188 #[test]
1189 fn endpoint_variable_with_quote() {
1190 let mut vars = Variables::new(VariablesType::Default);
1191 vars.insert("one_variable", "one'quote");
1192 let ep = Endpoint::Sofia(SofiaEndpoint {
1193 profile: "internal".into(),
1194 destination: "123@example.com".into(),
1195 variables: Some(vars),
1196 });
1197 assert_eq!(
1198 ep.to_string(),
1199 "{one_variable=one\\'quote}sofia/internal/123@example.com"
1200 );
1201 }
1202
1203 #[test]
1204 fn loopback_endpoint_display() {
1205 let mut vars = Variables::new(VariablesType::Default);
1206 vars.insert("one_variable", "1");
1207 let ep = Endpoint::Loopback(
1208 LoopbackEndpoint::new("aUri")
1209 .with_context("aContext")
1210 .with_variables(vars),
1211 );
1212 assert_eq!(ep.to_string(), "{one_variable=1}loopback/aUri/aContext");
1213 }
1214
1215 #[test]
1216 fn sofia_gateway_endpoint_display() {
1217 let mut vars = Variables::new(VariablesType::Default);
1218 vars.insert("one_variable", "1");
1219 let ep = Endpoint::SofiaGateway(SofiaGateway {
1220 destination: "aUri".into(),
1221 profile: None,
1222 gateway: "internal".into(),
1223 variables: Some(vars),
1224 });
1225 assert_eq!(
1226 ep.to_string(),
1227 "{one_variable=1}sofia/gateway/internal/aUri"
1228 );
1229 }
1230
1231 #[test]
1234 fn application_xml_format() {
1235 let app = Application::new("testApp", Some("testArg"));
1236 assert_eq!(
1237 app.to_string_with_dialplan(&DialplanType::Xml),
1238 "&testApp(testArg)"
1239 );
1240 }
1241
1242 #[test]
1243 fn application_inline_format() {
1244 let app = Application::new("testApp", Some("testArg"));
1245 assert_eq!(
1246 app.to_string_with_dialplan(&DialplanType::Inline),
1247 "testApp:testArg"
1248 );
1249 }
1250
1251 #[test]
1252 fn application_inline_no_args() {
1253 let app = Application::simple("park");
1254 assert_eq!(app.to_string_with_dialplan(&DialplanType::Inline), "park");
1255 }
1256
1257 #[test]
1260 fn originate_xml_display() {
1261 let ep = Endpoint::Sofia(SofiaEndpoint {
1262 profile: "internal".into(),
1263 destination: "123@example.com".into(),
1264 variables: None,
1265 });
1266 let orig = Originate::application(ep, Application::new("conference", Some("1")))
1267 .dialplan(DialplanType::Xml)
1268 .unwrap();
1269 assert_eq!(
1270 orig.to_string(),
1271 "originate sofia/internal/123@example.com &conference(1) XML"
1272 );
1273 }
1274
1275 #[test]
1276 fn originate_inline_display() {
1277 let ep = Endpoint::Sofia(SofiaEndpoint {
1278 profile: "internal".into(),
1279 destination: "123@example.com".into(),
1280 variables: None,
1281 });
1282 let orig = Originate::inline(ep, vec![Application::new("conference", Some("1"))])
1283 .unwrap()
1284 .dialplan(DialplanType::Inline)
1285 .unwrap();
1286 assert_eq!(
1287 orig.to_string(),
1288 "originate sofia/internal/123@example.com conference:1 inline"
1289 );
1290 }
1291
1292 #[test]
1293 fn originate_extension_display() {
1294 let ep = Endpoint::Sofia(SofiaEndpoint {
1295 profile: "internal".into(),
1296 destination: "123@example.com".into(),
1297 variables: None,
1298 });
1299 let orig = Originate::extension(ep, "1000")
1300 .dialplan(DialplanType::Xml)
1301 .unwrap()
1302 .context("default");
1303 assert_eq!(
1304 orig.to_string(),
1305 "originate sofia/internal/123@example.com 1000 XML default"
1306 );
1307 }
1308
1309 #[test]
1310 fn originate_extension_round_trip() {
1311 let input = "originate sofia/internal/test@example.com 1000 XML default";
1312 let parsed: Originate = input
1313 .parse()
1314 .unwrap();
1315 assert_eq!(parsed.to_string(), input);
1316 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1317 }
1318
1319 #[test]
1320 fn originate_extension_no_dialplan() {
1321 let input = "originate sofia/internal/test@example.com 1000";
1322 let parsed: Originate = input
1323 .parse()
1324 .unwrap();
1325 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1326 assert_eq!(parsed.to_string(), input);
1327 }
1328
1329 #[test]
1330 fn originate_extension_with_inline_errors() {
1331 let ep = Endpoint::Sofia(SofiaEndpoint {
1332 profile: "internal".into(),
1333 destination: "123@example.com".into(),
1334 variables: None,
1335 });
1336 let result = Originate::extension(ep, "1000").dialplan(DialplanType::Inline);
1337 assert!(result.is_err());
1338 }
1339
1340 #[test]
1341 fn originate_empty_inline_errors() {
1342 let ep = Endpoint::Sofia(SofiaEndpoint {
1343 profile: "internal".into(),
1344 destination: "123@example.com".into(),
1345 variables: None,
1346 });
1347 let result = Originate::inline(ep, vec![]);
1348 assert!(result.is_err());
1349 }
1350
1351 #[test]
1352 fn originate_from_string_round_trip() {
1353 let input = "originate {test='variable with quote'}sofia/internal/test@example.com 123";
1354 let orig: Originate = input
1355 .parse()
1356 .unwrap();
1357 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "123"));
1358 assert_eq!(orig.to_string(), input);
1359 }
1360
1361 #[test]
1362 fn originate_socket_app_quoted() {
1363 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1364 let orig = Originate::application(
1365 ep,
1366 Application::new("socket", Some("127.0.0.1:8040 async full")),
1367 );
1368 assert_eq!(
1369 orig.to_string(),
1370 "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'"
1371 );
1372 }
1373
1374 #[test]
1375 fn originate_socket_round_trip() {
1376 let input = "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'";
1377 let parsed: Originate = input
1378 .parse()
1379 .unwrap();
1380 assert_eq!(parsed.to_string(), input);
1381 if let OriginateTarget::Application(ref app) = parsed.target() {
1382 assert_eq!(app.args(), Some("127.0.0.1:8040 async full"));
1383 } else {
1384 panic!("expected Application target");
1385 }
1386 }
1387
1388 #[test]
1389 fn originate_display_round_trip() {
1390 let ep = Endpoint::Sofia(SofiaEndpoint {
1391 profile: "internal".into(),
1392 destination: "123@example.com".into(),
1393 variables: None,
1394 });
1395 let orig = Originate::application(ep, Application::new("conference", Some("1")))
1396 .dialplan(DialplanType::Xml)
1397 .unwrap();
1398 let s = orig.to_string();
1399 let parsed: Originate = s
1400 .parse()
1401 .unwrap();
1402 assert_eq!(parsed.to_string(), s);
1403 }
1404
1405 #[test]
1406 fn originate_inline_no_args_round_trip() {
1407 let input = "originate sofia/internal/123@example.com park inline";
1408 let parsed: Originate = input
1409 .parse()
1410 .unwrap();
1411 assert_eq!(parsed.to_string(), input);
1412 if let OriginateTarget::InlineApplications(ref apps) = parsed.target() {
1413 assert!(apps[0]
1414 .args()
1415 .is_none());
1416 } else {
1417 panic!("expected InlineApplications target");
1418 }
1419 }
1420
1421 #[test]
1422 fn originate_inline_multi_app_round_trip() {
1423 let input =
1424 "originate sofia/internal/123@example.com playback:/tmp/test.wav,hangup:NORMAL_CLEARING inline";
1425 let parsed: Originate = input
1426 .parse()
1427 .unwrap();
1428 assert_eq!(parsed.to_string(), input);
1429 }
1430
1431 #[test]
1432 fn originate_inline_auto_dialplan() {
1433 let ep = Endpoint::Sofia(SofiaEndpoint {
1434 profile: "internal".into(),
1435 destination: "123@example.com".into(),
1436 variables: None,
1437 });
1438 let orig = Originate::inline(ep, vec![Application::simple("park")]).unwrap();
1439 assert!(orig
1440 .to_string()
1441 .contains("inline"));
1442 }
1443
1444 #[test]
1447 fn dialplan_type_display() {
1448 assert_eq!(DialplanType::Inline.to_string(), "inline");
1449 assert_eq!(DialplanType::Xml.to_string(), "XML");
1450 }
1451
1452 #[test]
1453 fn dialplan_type_from_str() {
1454 assert_eq!(
1455 "inline"
1456 .parse::<DialplanType>()
1457 .unwrap(),
1458 DialplanType::Inline
1459 );
1460 assert_eq!(
1461 "XML"
1462 .parse::<DialplanType>()
1463 .unwrap(),
1464 DialplanType::Xml
1465 );
1466 }
1467
1468 #[test]
1469 fn dialplan_type_from_str_case_insensitive() {
1470 assert_eq!(
1471 "xml"
1472 .parse::<DialplanType>()
1473 .unwrap(),
1474 DialplanType::Xml
1475 );
1476 assert_eq!(
1477 "Xml"
1478 .parse::<DialplanType>()
1479 .unwrap(),
1480 DialplanType::Xml
1481 );
1482 assert_eq!(
1483 "INLINE"
1484 .parse::<DialplanType>()
1485 .unwrap(),
1486 DialplanType::Inline
1487 );
1488 assert_eq!(
1489 "Inline"
1490 .parse::<DialplanType>()
1491 .unwrap(),
1492 DialplanType::Inline
1493 );
1494 }
1495
1496 #[test]
1499 fn serde_dialplan_type_xml() {
1500 let json = serde_json::to_string(&DialplanType::Xml).unwrap();
1501 assert_eq!(json, "\"xml\"");
1502 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1503 assert_eq!(parsed, DialplanType::Xml);
1504 }
1505
1506 #[test]
1507 fn serde_dialplan_type_inline() {
1508 let json = serde_json::to_string(&DialplanType::Inline).unwrap();
1509 assert_eq!(json, "\"inline\"");
1510 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1511 assert_eq!(parsed, DialplanType::Inline);
1512 }
1513
1514 #[test]
1515 fn serde_variables_type() {
1516 let json = serde_json::to_string(&VariablesType::Enterprise).unwrap();
1517 assert_eq!(json, "\"enterprise\"");
1518 let parsed: VariablesType = serde_json::from_str(&json).unwrap();
1519 assert_eq!(parsed, VariablesType::Enterprise);
1520 }
1521
1522 #[test]
1523 fn serde_variables_flat_default() {
1524 let mut vars = Variables::new(VariablesType::Default);
1525 vars.insert("key1", "val1");
1526 vars.insert("key2", "val2");
1527 let json = serde_json::to_string(&vars).unwrap();
1528 let parsed: Variables = serde_json::from_str(&json).unwrap();
1530 assert_eq!(parsed.scope(), VariablesType::Default);
1531 assert_eq!(parsed.get("key1"), Some("val1"));
1532 assert_eq!(parsed.get("key2"), Some("val2"));
1533 }
1534
1535 #[test]
1536 fn serde_variables_scoped_enterprise() {
1537 let mut vars = Variables::new(VariablesType::Enterprise);
1538 vars.insert("key1", "val1");
1539 let json = serde_json::to_string(&vars).unwrap();
1540 assert!(json.contains("\"enterprise\""));
1542 let parsed: Variables = serde_json::from_str(&json).unwrap();
1543 assert_eq!(parsed.scope(), VariablesType::Enterprise);
1544 assert_eq!(parsed.get("key1"), Some("val1"));
1545 }
1546
1547 #[test]
1548 fn serde_variables_flat_map_deserializes_as_default() {
1549 let json = r#"{"key1":"val1","key2":"val2"}"#;
1550 let vars: Variables = serde_json::from_str(json).unwrap();
1551 assert_eq!(vars.scope(), VariablesType::Default);
1552 assert_eq!(vars.get("key1"), Some("val1"));
1553 assert_eq!(vars.get("key2"), Some("val2"));
1554 }
1555
1556 #[test]
1557 fn serde_variables_scoped_deserializes() {
1558 let json = r#"{"scope":"channel","vars":{"k":"v"}}"#;
1559 let vars: Variables = serde_json::from_str(json).unwrap();
1560 assert_eq!(vars.scope(), VariablesType::Channel);
1561 assert_eq!(vars.get("k"), Some("v"));
1562 }
1563
1564 #[test]
1565 fn serde_application() {
1566 let app = Application::new("park", None::<&str>);
1567 let json = serde_json::to_string(&app).unwrap();
1568 let parsed: Application = serde_json::from_str(&json).unwrap();
1569 assert_eq!(parsed, app);
1570 }
1571
1572 #[test]
1573 fn serde_application_with_args() {
1574 let app = Application::new("conference", Some("1"));
1575 let json = serde_json::to_string(&app).unwrap();
1576 let parsed: Application = serde_json::from_str(&json).unwrap();
1577 assert_eq!(parsed, app);
1578 }
1579
1580 #[test]
1581 fn serde_application_skips_none_args() {
1582 let app = Application::new("park", None::<&str>);
1583 let json = serde_json::to_string(&app).unwrap();
1584 assert!(!json.contains("args"));
1585 }
1586
1587 #[test]
1588 fn serde_originate_application_round_trip() {
1589 let ep = Endpoint::Sofia(SofiaEndpoint {
1590 profile: "internal".into(),
1591 destination: "123@example.com".into(),
1592 variables: None,
1593 });
1594 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1595 .dialplan(DialplanType::Xml)
1596 .unwrap()
1597 .context("default")
1598 .cid_name("Test")
1599 .cid_num("5551234")
1600 .timeout(Duration::from_secs(30));
1601 let json = serde_json::to_string(&orig).unwrap();
1602 assert!(json.contains("\"application\""));
1603 let parsed: Originate = serde_json::from_str(&json).unwrap();
1604 assert_eq!(parsed, orig);
1605 }
1606
1607 #[test]
1608 fn serde_originate_extension() {
1609 let json = r#"{
1610 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1611 "extension": "1000",
1612 "dialplan": "xml",
1613 "context": "default"
1614 }"#;
1615 let orig: Originate = serde_json::from_str(json).unwrap();
1616 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1617 assert_eq!(
1618 orig.to_string(),
1619 "originate sofia/internal/123@example.com 1000 XML default"
1620 );
1621 }
1622
1623 #[test]
1624 fn serde_originate_extension_with_inline_rejected() {
1625 let json = r#"{
1626 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1627 "extension": "1000",
1628 "dialplan": "inline"
1629 }"#;
1630 let result = serde_json::from_str::<Originate>(json);
1631 assert!(result.is_err());
1632 }
1633
1634 #[test]
1635 fn serde_originate_empty_inline_rejected() {
1636 let json = r#"{
1637 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1638 "inline_applications": []
1639 }"#;
1640 let result = serde_json::from_str::<Originate>(json);
1641 assert!(result.is_err());
1642 }
1643
1644 #[test]
1645 fn serde_originate_inline_applications() {
1646 let json = r#"{
1647 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1648 "inline_applications": [
1649 {"name": "playback", "args": "/tmp/test.wav"},
1650 {"name": "hangup", "args": "NORMAL_CLEARING"}
1651 ]
1652 }"#;
1653 let orig: Originate = serde_json::from_str(json).unwrap();
1654 if let OriginateTarget::InlineApplications(ref apps) = orig.target() {
1655 assert_eq!(apps.len(), 2);
1656 } else {
1657 panic!("expected InlineApplications");
1658 }
1659 assert!(orig
1660 .to_string()
1661 .contains("inline"));
1662 }
1663
1664 #[test]
1665 fn serde_originate_skips_none_fields() {
1666 let ep = Endpoint::Sofia(SofiaEndpoint {
1667 profile: "internal".into(),
1668 destination: "123@example.com".into(),
1669 variables: None,
1670 });
1671 let orig = Originate::application(ep, Application::new("park", None::<&str>));
1672 let json = serde_json::to_string(&orig).unwrap();
1673 assert!(!json.contains("dialplan"));
1674 assert!(!json.contains("context"));
1675 assert!(!json.contains("cid_name"));
1676 assert!(!json.contains("cid_num"));
1677 assert!(!json.contains("timeout"));
1678 }
1679
1680 #[test]
1681 fn serde_originate_to_wire_format() {
1682 let json = r#"{
1683 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1684 "application": {"name": "park"},
1685 "dialplan": "xml",
1686 "context": "default"
1687 }"#;
1688 let orig: Originate = serde_json::from_str(json).unwrap();
1689 let wire = orig.to_string();
1690 assert!(wire.starts_with("originate"));
1691 assert!(wire.contains("sofia/internal/123@example.com"));
1692 assert!(wire.contains("&park()"));
1693 assert!(wire.contains("XML"));
1694 }
1695
1696 #[test]
1699 fn application_simple_no_args() {
1700 let app = Application::simple("park");
1701 assert_eq!(app.name(), "park");
1702 assert!(app
1703 .args()
1704 .is_none());
1705 }
1706
1707 #[test]
1708 fn application_simple_xml_format() {
1709 let app = Application::simple("park");
1710 assert_eq!(app.to_string_with_dialplan(&DialplanType::Xml), "&park()");
1711 }
1712
1713 #[test]
1716 fn originate_target_from_application() {
1717 let target: OriginateTarget = Application::simple("park").into();
1718 assert!(matches!(target, OriginateTarget::Application(_)));
1719 }
1720
1721 #[test]
1722 fn originate_target_from_vec() {
1723 let target: OriginateTarget = vec![
1724 Application::new("conference", Some("1")),
1725 Application::new("hangup", Some("NORMAL_CLEARING")),
1726 ]
1727 .into();
1728 if let OriginateTarget::InlineApplications(apps) = target {
1729 assert_eq!(apps.len(), 2);
1730 } else {
1731 panic!("expected InlineApplications");
1732 }
1733 }
1734
1735 #[test]
1736 fn originate_target_application_wire_format() {
1737 let ep = Endpoint::Sofia(SofiaEndpoint {
1738 profile: "internal".into(),
1739 destination: "123@example.com".into(),
1740 variables: None,
1741 });
1742 let orig = Originate::application(ep, Application::simple("park"));
1743 assert_eq!(
1744 orig.to_string(),
1745 "originate sofia/internal/123@example.com &park()"
1746 );
1747 }
1748
1749 #[test]
1750 fn originate_timeout_only_fills_positional_gaps() {
1751 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1752 let cmd = Originate::application(ep, Application::simple("park"))
1753 .timeout(Duration::from_secs(30));
1754 assert_eq!(
1757 cmd.to_string(),
1758 "originate loopback/9199/test &park() XML default undef undef 30"
1759 );
1760 }
1761
1762 #[test]
1763 fn originate_cid_num_only_fills_preceding_gaps() {
1764 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1765 let cmd = Originate::application(ep, Application::simple("park")).cid_num("5551234");
1766 assert_eq!(
1767 cmd.to_string(),
1768 "originate loopback/9199/test &park() XML default undef 5551234"
1769 );
1770 }
1771
1772 #[test]
1773 fn originate_context_only_fills_dialplan() {
1774 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1775 let cmd = Originate::extension(ep, "1000").context("myctx");
1776 assert_eq!(
1777 cmd.to_string(),
1778 "originate loopback/9199/test 1000 XML myctx"
1779 );
1780 }
1781
1782 #[test]
1788 fn originate_context_gap_filler_round_trip_asymmetry() {
1789 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1790 let cmd = Originate::application(ep, Application::simple("park")).cid_name("Alice");
1791 let wire = cmd.to_string();
1792 assert!(wire.contains("default"), "gap-filler should emit 'default'");
1793
1794 let parsed: Originate = wire
1795 .parse()
1796 .unwrap();
1797 assert_eq!(parsed.context_str(), Some("default"));
1799
1800 assert_eq!(parsed.to_string(), wire);
1802 }
1803
1804 #[test]
1807 fn serde_originate_full_round_trip_with_variables() {
1808 let mut ep_vars = Variables::new(VariablesType::Default);
1809 ep_vars.insert("originate_timeout", "30");
1810 ep_vars.insert("sip_h_X-Custom", "value with spaces");
1811 let ep = Endpoint::SofiaGateway(SofiaGateway {
1812 gateway: "my_provider".into(),
1813 destination: "18005551234".into(),
1814 profile: Some("external".into()),
1815 variables: Some(ep_vars),
1816 });
1817 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1818 .dialplan(DialplanType::Xml)
1819 .unwrap()
1820 .context("public")
1821 .cid_name("Test Caller")
1822 .cid_num("5551234")
1823 .timeout(Duration::from_secs(60));
1824 let json = serde_json::to_string(&orig).unwrap();
1825 let parsed: Originate = serde_json::from_str(&json).unwrap();
1826 assert_eq!(parsed, orig);
1827 assert_eq!(parsed.to_string(), orig.to_string());
1828 }
1829
1830 #[test]
1831 fn serde_originate_inline_round_trip_with_all_fields() {
1832 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1833 let orig = Originate::inline(
1834 ep,
1835 vec![
1836 Application::new("playback", Some("/tmp/test.wav")),
1837 Application::new("hangup", Some("NORMAL_CLEARING")),
1838 ],
1839 )
1840 .unwrap()
1841 .dialplan(DialplanType::Inline)
1842 .unwrap()
1843 .context("default")
1844 .cid_name("IVR")
1845 .cid_num("0000")
1846 .timeout(Duration::from_secs(45));
1847 let json = serde_json::to_string(&orig).unwrap();
1848 let parsed: Originate = serde_json::from_str(&json).unwrap();
1849 assert_eq!(parsed, orig);
1850 assert_eq!(parsed.to_string(), orig.to_string());
1851 }
1852
1853 #[test]
1856 fn variables_from_str_empty_block() {
1857 let result = "{}".parse::<Variables>();
1858 assert!(
1859 result.is_ok(),
1860 "empty variable block should parse successfully"
1861 );
1862 let vars = result.unwrap();
1863 assert!(
1864 vars.is_empty(),
1865 "parsed empty block should have no variables"
1866 );
1867 }
1868
1869 #[test]
1870 fn variables_from_str_empty_channel_block() {
1871 let result = "[]".parse::<Variables>();
1872 assert!(result.is_ok());
1873 let vars = result.unwrap();
1874 assert!(vars.is_empty());
1875 assert_eq!(vars.scope(), VariablesType::Channel);
1876 }
1877
1878 #[test]
1879 fn variables_from_str_empty_enterprise_block() {
1880 let result = "<>".parse::<Variables>();
1881 assert!(result.is_ok());
1882 let vars = result.unwrap();
1883 assert!(vars.is_empty());
1884 assert_eq!(vars.scope(), VariablesType::Enterprise);
1885 }
1886
1887 #[test]
1890 fn originate_context_named_inline() {
1891 let ep = Endpoint::Sofia(SofiaEndpoint {
1892 profile: "internal".into(),
1893 destination: "123@example.com".into(),
1894 variables: None,
1895 });
1896 let orig = Originate::extension(ep, "1000")
1897 .dialplan(DialplanType::Xml)
1898 .unwrap()
1899 .context("inline");
1900 let wire = orig.to_string();
1901 assert!(wire.contains("XML inline"), "wire: {}", wire);
1902 let parsed: Originate = wire
1903 .parse()
1904 .unwrap();
1905 assert_eq!(parsed.to_string(), wire);
1908 }
1909
1910 #[test]
1911 fn originate_context_named_xml() {
1912 let ep = Endpoint::Sofia(SofiaEndpoint {
1913 profile: "internal".into(),
1914 destination: "123@example.com".into(),
1915 variables: None,
1916 });
1917 let orig = Originate::extension(ep, "1000")
1918 .dialplan(DialplanType::Xml)
1919 .unwrap()
1920 .context("XML");
1921 let wire = orig.to_string();
1922 assert!(wire.contains("XML XML"), "wire: {}", wire);
1924 let parsed: Originate = wire
1925 .parse()
1926 .unwrap();
1927 assert_eq!(parsed.to_string(), wire);
1928 }
1929
1930 #[test]
1931 fn originate_accessors() {
1932 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1933 let cmd = Originate::extension(ep, "1000")
1934 .dialplan(DialplanType::Xml)
1935 .unwrap()
1936 .context("default")
1937 .cid_name("Alice")
1938 .cid_num("5551234")
1939 .timeout(Duration::from_secs(30));
1940
1941 assert!(matches!(cmd.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1942 assert_eq!(cmd.dialplan_type(), Some(&DialplanType::Xml));
1943 assert_eq!(cmd.context_str(), Some("default"));
1944 assert_eq!(cmd.caller_id_name(), Some("Alice"));
1945 assert_eq!(cmd.caller_id_number(), Some("5551234"));
1946 assert_eq!(cmd.timeout_seconds(), Some(30));
1947 }
1948}