Skip to main content

freeswitch_types/commands/
originate.rs

1//! Originate command builder with endpoint configuration, variable scoping,
2//! and automatic quoting for socket application arguments.
3
4use std::fmt;
5use std::str::FromStr;
6use std::time::Duration;
7
8use indexmap::IndexMap;
9
10use super::{originate_quote, originate_split, originate_unquote};
11
12/// FreeSWITCH keyword for omitted positional arguments.
13///
14/// `switch_separate_string` converts `"undef"` to NULL, making it the
15/// canonical placeholder when a later positional arg forces earlier ones
16/// to be present on the wire.
17const UNDEF: &str = "undef";
18
19/// FreeSWITCH dialplan type for originate commands.
20#[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 dialplan: applications execute directly without XML lookup.
26    Inline,
27    /// XML dialplan: route through the XML dialplan engine.
28    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/// Error returned when parsing an invalid dialplan type string.
41#[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/// Scope for channel variables in an originate command.
67///
68/// - `Enterprise` (`<>`) -- applies across all threads (`:_:` separated)
69/// - `Default` (`{}`) -- applies to all channels in this originate
70/// - `Channel` (`[]`) -- applies only to one specific channel
71#[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    /// `<>` scope -- applies across all `:_:` separated threads.
77    Enterprise,
78    /// `{}` scope -- applies to all channels in this originate.
79    Default,
80    /// `[]` scope -- applies to one specific channel.
81    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/// Ordered set of channel variables with FreeSWITCH escaping.
95///
96/// Values containing commas are escaped with `\,`, single quotes with `\'`,
97/// and values with spaces are wrapped in single quotes.
98///
99/// # Serde format
100///
101/// [`Default`](VariablesType::Default) scope serializes as a flat JSON map:
102/// `{"key": "value", ...}`. Non-default scopes serialize as
103/// `{"scope": "Enterprise", "vars": {"key": "value"}}`.
104/// Deserialization accepts both formats; a flat map implies `Default` scope.
105#[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    /// Create an empty variable set with the given scope.
133    pub fn new(vars_type: VariablesType) -> Self {
134        Self {
135            vars_type,
136            inner: IndexMap::new(),
137        }
138    }
139
140    /// Create from an existing set of key-value pairs.
141    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    /// Insert or overwrite a variable.
155    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    /// Remove a variable by name, returning its value if it existed.
161    pub fn remove(&mut self, key: &str) -> Option<String> {
162        self.inner
163            .shift_remove(key)
164    }
165
166    /// Look up a variable by name.
167    pub fn get(&self, key: &str) -> Option<&str> {
168        self.inner
169            .get(key)
170            .map(|s| s.as_str())
171    }
172
173    /// Whether the set contains no variables.
174    pub fn is_empty(&self) -> bool {
175        self.inner
176            .is_empty()
177    }
178
179    /// Number of variables.
180    pub fn len(&self) -> usize {
181        self.inner
182            .len()
183    }
184
185    /// Variable scope (Enterprise, Default, or Channel).
186    pub fn scope(&self) -> VariablesType {
187        self.vars_type
188    }
189
190    /// Change the variable scope.
191    pub fn set_scope(&mut self, scope: VariablesType) {
192        self.vars_type = scope;
193    }
194
195    /// Iterate over key-value pairs in insertion order.
196    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    /// Mutable iterator over key-value pairs in insertion order.
203    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    /// Mutable iterator over values in insertion order.
210    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
343/// Split on commas that are not escaped by a backslash.
344///
345/// A comma preceded by an odd number of backslashes is escaped (e.g. `\,`).
346/// A comma preceded by an even number of backslashes is a real split point
347/// (e.g. `\\,` means escaped backslash followed by comma delimiter).
348fn 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
371// Endpoint is now defined in endpoint.rs -- re-exported via pub use below.
372pub use super::endpoint::Endpoint;
373
374/// A single dialplan application with optional arguments.
375///
376/// Formats differently depending on [`DialplanType`]:
377/// - Inline: `name` or `name:args`
378/// - XML: `&name(args)`
379#[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    /// Create an application with optional arguments.
392    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    /// Create an application with no arguments.
400    pub fn simple(name: impl Into<String>) -> Self {
401        Self {
402            name: name.into(),
403            args: None,
404        }
405    }
406
407    /// Park the channel (hold in place without bridging).
408    pub fn park() -> Self {
409        Self::simple("park")
410    }
411
412    /// Application name (e.g. `park`, `conference`, `socket`).
413    pub fn name(&self) -> &str {
414        &self.name
415    }
416
417    /// Application arguments, if any.
418    pub fn args(&self) -> Option<&str> {
419        self.args
420            .as_deref()
421    }
422
423    /// Mutable reference to the application name.
424    pub fn name_mut(&mut self) -> &mut String {
425        &mut self.name
426    }
427
428    /// Mutable reference to the application arguments.
429    pub fn args_mut(&mut self) -> &mut Option<String> {
430        &mut self.args
431    }
432
433    /// Format as inline (`name:args`) or XML (`&name(args)`) syntax.
434    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            // XML and custom dialplans use the &app(args) syntax.
443            _ => {
444                let args = self
445                    .args
446                    .as_deref()
447                    .unwrap_or("");
448                format!("&{}({})", self.name, args)
449            }
450        }
451    }
452}
453
454/// The target of an originate command: either a dialplan extension or
455/// application(s) to execute directly.
456///
457/// FreeSWITCH syntax: `originate <endpoint> <target> [dialplan] ...`
458/// where `<target>` is either a bare extension string (routes through
459/// the dialplan engine) or `&app(args)` / `app:args` (executes inline).
460#[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    /// Route through the dialplan engine to this extension.
466    Extension(String),
467    /// Single application for XML dialplan: `&app(args)`.
468    Application(Application),
469    /// One or more applications for inline dialplan: `app:args,app:args`.
470    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/// Originate command builder: `originate <endpoint> <target> [dialplan] [context] [cid_name] [cid_num] [timeout]`.
486///
487/// Constructed via [`Originate::extension`], [`Originate::application`], or
488/// [`Originate::inline`]. Invalid states (Extension + Inline dialplan, empty
489/// inline apps) are rejected at construction time rather than at `Display`.
490///
491/// Optional fields are set via consuming-self chaining methods:
492///
493/// ```
494/// # use std::time::Duration;
495/// # use freeswitch_types::commands::*;
496/// let cmd = Originate::application(
497///     Endpoint::Loopback(LoopbackEndpoint::new("9196").with_context("default")),
498///     Application::simple("park"),
499/// )
500/// .cid_name("Alice")
501/// .cid_num("5551234")
502/// .timeout(Duration::from_secs(30));
503/// ```
504#[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    /// Intermediate type for serde, mirroring the old public-field layout.
520    #[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    /// Route through the dialplan engine to an extension.
597    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    /// Execute a single XML-format application on the answered channel.
610    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    /// Execute inline applications on the answered channel.
623    ///
624    /// Returns `Err` if the iterator yields no applications.
625    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    /// Set the dialplan type.
647    ///
648    /// Returns `Err` if setting `Inline` on an `Extension` target.
649    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    /// Set the dialplan context.
658    pub fn context(mut self, ctx: impl Into<String>) -> Self {
659        self.context = Some(ctx.into());
660        self
661    }
662
663    /// Set the caller ID name.
664    pub fn cid_name(mut self, name: impl Into<String>) -> Self {
665        self.cid_name = Some(name.into());
666        self
667    }
668
669    /// Set the caller ID number.
670    pub fn cid_num(mut self, num: impl Into<String>) -> Self {
671        self.cid_num = Some(num.into());
672        self
673    }
674
675    /// Set the originate timeout. Sub-second precision is truncated to whole
676    /// seconds on the wire and in serde round-trips.
677    pub fn timeout(mut self, duration: Duration) -> Self {
678        self.timeout = Some(duration);
679        self
680    }
681
682    /// The dial endpoint.
683    pub fn endpoint(&self) -> &Endpoint {
684        &self.endpoint
685    }
686
687    /// Mutable reference to the dial endpoint.
688    pub fn endpoint_mut(&mut self) -> &mut Endpoint {
689        &mut self.endpoint
690    }
691
692    /// The originate target (extension, application, or inline apps).
693    pub fn target(&self) -> &OriginateTarget {
694        &self.target
695    }
696
697    /// Mutable reference to the originate target.
698    pub fn target_mut(&mut self) -> &mut OriginateTarget {
699        &mut self.target
700    }
701
702    /// The dialplan type, if explicitly set.
703    pub fn dialplan_type(&self) -> Option<&DialplanType> {
704        self.dialplan
705            .as_ref()
706    }
707
708    /// The dialplan context, if set.
709    pub fn context_str(&self) -> Option<&str> {
710        self.context
711            .as_deref()
712    }
713
714    /// The caller ID name, if set.
715    pub fn caller_id_name(&self) -> Option<&str> {
716        self.cid_name
717            .as_deref()
718    }
719
720    /// The caller ID number, if set.
721    pub fn caller_id_number(&self) -> Option<&str> {
722        self.cid_num
723            .as_deref()
724    }
725
726    /// The timeout as a `Duration`, if set.
727    pub fn timeout_duration(&self) -> Option<Duration> {
728        self.timeout
729    }
730
731    /// The timeout in whole seconds, if set.
732    pub fn timeout_seconds(&self) -> Option<u64> {
733        self.timeout
734            .map(|d| d.as_secs())
735    }
736
737    /// Override the dialplan type after construction.
738    pub fn set_dialplan(&mut self, dp: Option<DialplanType>) {
739        self.dialplan = dp;
740    }
741
742    /// Override the dialplan context after construction.
743    pub fn set_context(&mut self, ctx: Option<impl Into<String>>) {
744        self.context = ctx.map(|c| c.into());
745    }
746
747    /// Override the caller ID name after construction.
748    pub fn set_cid_name(&mut self, name: Option<impl Into<String>>) {
749        self.cid_name = name.map(|n| n.into());
750    }
751
752    /// Override the caller ID number after construction.
753    pub fn set_cid_num(&mut self, num: Option<impl Into<String>>) {
754        self.cid_num = num.map(|n| n.into());
755    }
756
757    /// Override the timeout after construction.
758    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                // Constructor guarantees non-empty
770                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        // Positional args: dialplan, context, cid_name, cid_num, timeout.
786        // FreeSWITCH parses by position, so if a later arg is present,
787        // all preceding ones must be emitted with defaults.
788        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        // Validate via constructors then set parsed fields directly (same module)
918        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/// Errors from originate command parsing or construction.
933#[derive(Debug, Clone, PartialEq, Eq)]
934#[non_exhaustive]
935pub enum OriginateError {
936    /// A single-quoted token was never closed.
937    UnclosedQuote(String),
938    /// General parse failure with a description.
939    ParseError(String),
940    /// Inline originate requires at least one application.
941    EmptyInlineApplications,
942    /// Extension target cannot use inline dialplan.
943    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    // --- Variables ---
969
970    #[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        // \\, = escaped backslash + comma delimiter
1064        assert_eq!(split_unescaped_commas(r"a\\,b"), vec![r"a\\", "b"]);
1065    }
1066
1067    #[test]
1068    fn split_unescaped_commas_triple_backslash() {
1069        // \\\, = escaped backslash + escaped comma (no split)
1070        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    // --- Endpoint ---
1162
1163    #[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    // --- Application ---
1232
1233    #[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    // --- Originate ---
1258
1259    #[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    // --- DialplanType ---
1445
1446    #[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    // --- Serde ---
1497
1498    #[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        // Default scope serializes as a flat map
1529        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        // Non-default scope serializes as {scope, vars}
1541        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    // --- Application::simple ---
1697
1698    #[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    // --- OriginateTarget From impls ---
1714
1715    #[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        // timeout is arg 7; dialplan/context/cid must be filled so FS
1755        // doesn't interpret "30" as the dialplan name
1756        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    /// `context: None` with a later positional arg emits `"default"` as a
1783    /// gap-filler. `FromStr` reads it back as `Some("default")` because
1784    /// `"default"` is also a valid user-specified context. The wire format
1785    /// round-trips correctly (identical string), but the struct-level
1786    /// representation differs. This is an accepted asymmetry.
1787    #[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        // Struct-level asymmetry: None became Some("default")
1798        assert_eq!(parsed.context_str(), Some("default"));
1799
1800        // Wire format is identical (the important invariant)
1801        assert_eq!(parsed.to_string(), wire);
1802    }
1803
1804    // --- T1: Full Originate serde round-trip ---
1805
1806    #[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    // --- T5: Variables::from_str with empty block ---
1854
1855    #[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    // --- T5: Originate::from_str with context named "inline" or "XML" ---
1888
1889    #[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        // "inline" is consumed as the dialplan type, not the context
1906        // This is an accepted limitation of positional parsing
1907        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        // "XML XML" - first is dialplan, second is context
1923        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}