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