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