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 = (&String, &String)> {
197        self.inner
198            .iter()
199    }
200
201    /// Mutable iterator over key-value pairs in insertion order.
202    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&String, &mut String)> {
203        self.inner
204            .iter_mut()
205    }
206
207    /// Mutable iterator over values in insertion order.
208    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut String> {
209        self.inner
210            .values_mut()
211    }
212}
213
214#[cfg(feature = "serde")]
215impl serde::Serialize for Variables {
216    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
217        if self.vars_type == VariablesType::Default {
218            self.inner
219                .serialize(serializer)
220        } else {
221            use serde::ser::SerializeStruct;
222            let mut s = serializer.serialize_struct("Variables", 2)?;
223            s.serialize_field("scope", &self.vars_type)?;
224            s.serialize_field("vars", &self.inner)?;
225            s.end()
226        }
227    }
228}
229
230#[cfg(feature = "serde")]
231impl<'de> serde::Deserialize<'de> for Variables {
232    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
233        #[derive(serde::Deserialize)]
234        #[serde(untagged)]
235        enum VariablesRepr {
236            Scoped {
237                scope: VariablesType,
238                vars: IndexMap<String, String>,
239            },
240            Flat(IndexMap<String, String>),
241        }
242
243        match VariablesRepr::deserialize(deserializer)? {
244            VariablesRepr::Scoped { scope, vars } => Ok(Self {
245                vars_type: scope,
246                inner: vars,
247            }),
248            VariablesRepr::Flat(map) => Ok(Self {
249                vars_type: VariablesType::Default,
250                inner: map,
251            }),
252        }
253    }
254}
255
256impl fmt::Display for Variables {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        let (open, close) = self
259            .vars_type
260            .delimiters();
261        f.write_fmt(format_args!("{}", open))?;
262        for (i, (key, value)) in self
263            .inner
264            .iter()
265            .enumerate()
266        {
267            if i > 0 {
268                f.write_str(",")?;
269            }
270            write!(f, "{}={}", key, escape_value(value))?;
271        }
272        f.write_fmt(format_args!("{}", close))
273    }
274}
275
276impl FromStr for Variables {
277    type Err = OriginateError;
278
279    fn from_str(s: &str) -> Result<Self, Self::Err> {
280        let s = s.trim();
281        if s.len() < 2 {
282            return Err(OriginateError::ParseError(
283                "variable block too short".into(),
284            ));
285        }
286
287        let (vars_type, inner_str) = match (s.as_bytes()[0], s.as_bytes()[s.len() - 1]) {
288            (b'{', b'}') => (VariablesType::Default, &s[1..s.len() - 1]),
289            (b'<', b'>') => (VariablesType::Enterprise, &s[1..s.len() - 1]),
290            (b'[', b']') => (VariablesType::Channel, &s[1..s.len() - 1]),
291            _ => {
292                return Err(OriginateError::ParseError(format!(
293                    "unknown variable delimiters: {}",
294                    s
295                )));
296            }
297        };
298
299        let mut inner = IndexMap::new();
300        if !inner_str.is_empty() {
301            // Split on commas not preceded by backslash
302            for part in split_unescaped_commas(inner_str) {
303                let (key, value) = part
304                    .split_once('=')
305                    .ok_or_else(|| {
306                        OriginateError::ParseError(format!("missing = in variable: {}", part))
307                    })?;
308                inner.insert(key.to_string(), unescape_value(value));
309            }
310        }
311
312        Ok(Self { vars_type, inner })
313    }
314}
315
316/// Split on commas that are not escaped by a backslash.
317///
318/// A comma preceded by an odd number of backslashes is escaped (e.g. `\,`).
319/// A comma preceded by an even number of backslashes is a real split point
320/// (e.g. `\\,` means escaped backslash followed by comma delimiter).
321fn split_unescaped_commas(s: &str) -> Vec<&str> {
322    let mut parts = Vec::new();
323    let mut start = 0;
324    let bytes = s.as_bytes();
325
326    for i in 0..bytes.len() {
327        if bytes[i] == b',' {
328            let mut backslashes = 0;
329            let mut j = i;
330            while j > 0 && bytes[j - 1] == b'\\' {
331                backslashes += 1;
332                j -= 1;
333            }
334            if backslashes % 2 == 0 {
335                parts.push(&s[start..i]);
336                start = i + 1;
337            }
338        }
339    }
340    parts.push(&s[start..]);
341    parts
342}
343
344// Endpoint is now defined in endpoint.rs — re-exported via pub use below.
345pub use super::endpoint::Endpoint;
346
347/// A single dialplan application with optional arguments.
348///
349/// Formats differently depending on [`DialplanType`]:
350/// - Inline: `name` or `name:args`
351/// - XML: `&name(args)`
352#[derive(Debug, Clone, PartialEq, Eq)]
353#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
354#[non_exhaustive]
355pub struct Application {
356    /// Application name (e.g. `park`, `conference`, `socket`).
357    pub name: String,
358    /// Application arguments, if any.
359    #[cfg_attr(
360        feature = "serde",
361        serde(default, skip_serializing_if = "Option::is_none")
362    )]
363    pub args: Option<String>,
364}
365
366impl Application {
367    /// Create an application with optional arguments.
368    pub fn new(name: impl Into<String>, args: Option<impl Into<String>>) -> Self {
369        Self {
370            name: name.into(),
371            args: args.map(|a| a.into()),
372        }
373    }
374
375    /// Create an application with no arguments.
376    pub fn simple(name: impl Into<String>) -> Self {
377        Self {
378            name: name.into(),
379            args: None,
380        }
381    }
382
383    /// Park the channel (hold in place without bridging).
384    pub fn park() -> Self {
385        Self::simple("park")
386    }
387
388    /// Format as inline (`name:args`) or XML (`&name(args)`) syntax.
389    pub fn to_string_with_dialplan(&self, dialplan: &DialplanType) -> String {
390        match dialplan {
391            DialplanType::Inline => match &self.args {
392                Some(args) => format!("{}:{}", self.name, args),
393                None => self
394                    .name
395                    .clone(),
396            },
397            // XML and custom dialplans use the &app(args) syntax.
398            _ => {
399                let args = self
400                    .args
401                    .as_deref()
402                    .unwrap_or("");
403                format!("&{}({})", self.name, args)
404            }
405        }
406    }
407}
408
409/// The target of an originate command: either a dialplan extension or
410/// application(s) to execute directly.
411///
412/// FreeSWITCH syntax: `originate <endpoint> <target> [dialplan] ...`
413/// where `<target>` is either a bare extension string (routes through
414/// the dialplan engine) or `&app(args)` / `app:args` (executes inline).
415#[derive(Debug, Clone, PartialEq, Eq)]
416#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
417#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
418#[non_exhaustive]
419pub enum OriginateTarget {
420    /// Route through the dialplan engine to this extension.
421    Extension(String),
422    /// Single application for XML dialplan: `&app(args)`.
423    Application(Application),
424    /// One or more applications for inline dialplan: `app:args,app:args`.
425    InlineApplications(Vec<Application>),
426}
427
428impl From<Application> for OriginateTarget {
429    fn from(app: Application) -> Self {
430        Self::Application(app)
431    }
432}
433
434impl From<Vec<Application>> for OriginateTarget {
435    fn from(apps: Vec<Application>) -> Self {
436        Self::InlineApplications(apps)
437    }
438}
439
440/// Originate command builder: `originate <endpoint> <target> [dialplan] [context] [cid_name] [cid_num] [timeout]`.
441///
442/// Constructed via [`Originate::extension`], [`Originate::application`], or
443/// [`Originate::inline`]. Invalid states (Extension + Inline dialplan, empty
444/// inline apps) are rejected at construction time rather than at `Display`.
445///
446/// Optional fields are set via consuming-self chaining methods:
447///
448/// ```
449/// # use std::time::Duration;
450/// # use freeswitch_types::commands::*;
451/// let cmd = Originate::application(
452///     Endpoint::Loopback(LoopbackEndpoint::new("9196").with_context("default")),
453///     Application::simple("park"),
454/// )
455/// .cid_name("Alice")
456/// .cid_num("5551234")
457/// .timeout(Duration::from_secs(30));
458/// ```
459#[derive(Debug, Clone, PartialEq, Eq)]
460pub struct Originate {
461    endpoint: Endpoint,
462    target: OriginateTarget,
463    dialplan: Option<DialplanType>,
464    context: Option<String>,
465    cid_name: Option<String>,
466    cid_num: Option<String>,
467    timeout: Option<Duration>,
468}
469
470#[cfg(feature = "serde")]
471mod serde_support {
472    use super::*;
473
474    /// Intermediate type for serde, mirroring the old public-field layout.
475    #[derive(serde::Serialize, serde::Deserialize)]
476    pub(super) struct OriginateRaw {
477        pub endpoint: Endpoint,
478        #[serde(flatten)]
479        pub target: OriginateTarget,
480        #[serde(default, skip_serializing_if = "Option::is_none")]
481        pub dialplan: Option<DialplanType>,
482        #[serde(default, skip_serializing_if = "Option::is_none")]
483        pub context: Option<String>,
484        #[serde(default, skip_serializing_if = "Option::is_none")]
485        pub cid_name: Option<String>,
486        #[serde(default, skip_serializing_if = "Option::is_none")]
487        pub cid_num: Option<String>,
488        #[serde(default, skip_serializing_if = "Option::is_none")]
489        pub timeout_secs: Option<u64>,
490    }
491
492    impl TryFrom<OriginateRaw> for Originate {
493        type Error = OriginateError;
494
495        fn try_from(raw: OriginateRaw) -> Result<Self, Self::Error> {
496            if matches!(raw.target, OriginateTarget::Extension(_))
497                && matches!(raw.dialplan, Some(DialplanType::Inline))
498            {
499                return Err(OriginateError::ExtensionWithInlineDialplan);
500            }
501            if let OriginateTarget::InlineApplications(ref apps) = raw.target {
502                if apps.is_empty() {
503                    return Err(OriginateError::EmptyInlineApplications);
504                }
505            }
506            Ok(Self {
507                endpoint: raw.endpoint,
508                target: raw.target,
509                dialplan: raw.dialplan,
510                context: raw.context,
511                cid_name: raw.cid_name,
512                cid_num: raw.cid_num,
513                timeout: raw
514                    .timeout_secs
515                    .map(Duration::from_secs),
516            })
517        }
518    }
519
520    impl From<Originate> for OriginateRaw {
521        fn from(o: Originate) -> Self {
522            Self {
523                endpoint: o.endpoint,
524                target: o.target,
525                dialplan: o.dialplan,
526                context: o.context,
527                cid_name: o.cid_name,
528                cid_num: o.cid_num,
529                timeout_secs: o
530                    .timeout
531                    .map(|d| d.as_secs()),
532            }
533        }
534    }
535
536    impl serde::Serialize for Originate {
537        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
538            OriginateRaw::from(self.clone()).serialize(serializer)
539        }
540    }
541
542    impl<'de> serde::Deserialize<'de> for Originate {
543        fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
544            let raw = OriginateRaw::deserialize(deserializer)?;
545            Originate::try_from(raw).map_err(serde::de::Error::custom)
546        }
547    }
548}
549
550impl Originate {
551    /// Route through the dialplan engine to an extension.
552    pub fn extension(endpoint: Endpoint, extension: impl Into<String>) -> Self {
553        Self {
554            endpoint,
555            target: OriginateTarget::Extension(extension.into()),
556            dialplan: None,
557            context: None,
558            cid_name: None,
559            cid_num: None,
560            timeout: None,
561        }
562    }
563
564    /// Execute a single XML-format application on the answered channel.
565    pub fn application(endpoint: Endpoint, app: Application) -> Self {
566        Self {
567            endpoint,
568            target: OriginateTarget::Application(app),
569            dialplan: None,
570            context: None,
571            cid_name: None,
572            cid_num: None,
573            timeout: None,
574        }
575    }
576
577    /// Execute inline applications on the answered channel.
578    ///
579    /// Returns `Err` if the iterator yields no applications.
580    pub fn inline(
581        endpoint: Endpoint,
582        apps: impl IntoIterator<Item = Application>,
583    ) -> Result<Self, OriginateError> {
584        let apps: Vec<Application> = apps
585            .into_iter()
586            .collect();
587        if apps.is_empty() {
588            return Err(OriginateError::EmptyInlineApplications);
589        }
590        Ok(Self {
591            endpoint,
592            target: OriginateTarget::InlineApplications(apps),
593            dialplan: None,
594            context: None,
595            cid_name: None,
596            cid_num: None,
597            timeout: None,
598        })
599    }
600
601    /// Set the dialplan type.
602    ///
603    /// Returns `Err` if setting `Inline` on an `Extension` target.
604    pub fn dialplan(mut self, dp: DialplanType) -> Result<Self, OriginateError> {
605        if matches!(self.target, OriginateTarget::Extension(_)) && dp == DialplanType::Inline {
606            return Err(OriginateError::ExtensionWithInlineDialplan);
607        }
608        self.dialplan = Some(dp);
609        Ok(self)
610    }
611
612    /// Set the dialplan context.
613    pub fn context(mut self, ctx: impl Into<String>) -> Self {
614        self.context = Some(ctx.into());
615        self
616    }
617
618    /// Set the caller ID name.
619    pub fn cid_name(mut self, name: impl Into<String>) -> Self {
620        self.cid_name = Some(name.into());
621        self
622    }
623
624    /// Set the caller ID number.
625    pub fn cid_num(mut self, num: impl Into<String>) -> Self {
626        self.cid_num = Some(num.into());
627        self
628    }
629
630    /// Set the originate timeout. Sub-second precision is truncated to whole
631    /// seconds on the wire and in serde round-trips.
632    pub fn timeout(mut self, duration: Duration) -> Self {
633        self.timeout = Some(duration);
634        self
635    }
636
637    /// The dial endpoint.
638    pub fn endpoint(&self) -> &Endpoint {
639        &self.endpoint
640    }
641
642    /// Mutable reference to the dial endpoint.
643    pub fn endpoint_mut(&mut self) -> &mut Endpoint {
644        &mut self.endpoint
645    }
646
647    /// The originate target (extension, application, or inline apps).
648    pub fn target(&self) -> &OriginateTarget {
649        &self.target
650    }
651
652    /// Mutable reference to the originate target.
653    pub fn target_mut(&mut self) -> &mut OriginateTarget {
654        &mut self.target
655    }
656
657    /// The dialplan type, if explicitly set.
658    pub fn dialplan_type(&self) -> Option<&DialplanType> {
659        self.dialplan
660            .as_ref()
661    }
662
663    /// The dialplan context, if set.
664    pub fn context_str(&self) -> Option<&str> {
665        self.context
666            .as_deref()
667    }
668
669    /// The caller ID name, if set.
670    pub fn caller_id_name(&self) -> Option<&str> {
671        self.cid_name
672            .as_deref()
673    }
674
675    /// The caller ID number, if set.
676    pub fn caller_id_number(&self) -> Option<&str> {
677        self.cid_num
678            .as_deref()
679    }
680
681    /// The timeout as a `Duration`, if set.
682    pub fn timeout_duration(&self) -> Option<Duration> {
683        self.timeout
684    }
685
686    /// The timeout in whole seconds, if set.
687    pub fn timeout_seconds(&self) -> Option<u64> {
688        self.timeout
689            .map(|d| d.as_secs())
690    }
691}
692
693impl fmt::Display for Originate {
694    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
695        let target_str = match &self.target {
696            OriginateTarget::Extension(ext) => ext.clone(),
697            OriginateTarget::Application(app) => app.to_string_with_dialplan(&DialplanType::Xml),
698            OriginateTarget::InlineApplications(apps) => {
699                // Constructor guarantees non-empty
700                let parts: Vec<String> = apps
701                    .iter()
702                    .map(|a| a.to_string_with_dialplan(&DialplanType::Inline))
703                    .collect();
704                parts.join(",")
705            }
706        };
707
708        write!(
709            f,
710            "originate {} {}",
711            self.endpoint,
712            originate_quote(&target_str)
713        )?;
714
715        // Positional args: dialplan, context, cid_name, cid_num, timeout.
716        // FreeSWITCH parses by position, so if a later arg is present,
717        // all preceding ones must be emitted with defaults.
718        let dialplan = match &self.target {
719            OriginateTarget::InlineApplications(_) => Some(
720                self.dialplan
721                    .unwrap_or(DialplanType::Inline),
722            ),
723            _ => self.dialplan,
724        };
725        let has_ctx = self
726            .context
727            .is_some();
728        let has_name = self
729            .cid_name
730            .is_some();
731        let has_num = self
732            .cid_num
733            .is_some();
734        let has_timeout = self
735            .timeout
736            .is_some();
737
738        if dialplan.is_some() || has_ctx || has_name || has_num || has_timeout {
739            let dp = dialplan
740                .as_ref()
741                .cloned()
742                .unwrap_or(DialplanType::Xml);
743            write!(f, " {}", dp)?;
744        }
745        if has_ctx || has_name || has_num || has_timeout {
746            write!(
747                f,
748                " {}",
749                self.context
750                    .as_deref()
751                    .unwrap_or("default")
752            )?;
753        }
754        if has_name || has_num || has_timeout {
755            let name = self
756                .cid_name
757                .as_deref()
758                .unwrap_or(UNDEF);
759            write!(f, " {}", originate_quote(name))?;
760        }
761        if has_num || has_timeout {
762            let num = self
763                .cid_num
764                .as_deref()
765                .unwrap_or(UNDEF);
766            write!(f, " {}", originate_quote(num))?;
767        }
768        if let Some(ref timeout) = self.timeout {
769            write!(f, " {}", timeout.as_secs())?;
770        }
771        Ok(())
772    }
773}
774
775impl FromStr for Originate {
776    type Err = OriginateError;
777
778    fn from_str(s: &str) -> Result<Self, Self::Err> {
779        let s = s
780            .strip_prefix("originate")
781            .unwrap_or(s)
782            .trim();
783        let mut args = originate_split(s, ' ')?;
784
785        if args.is_empty() {
786            return Err(OriginateError::ParseError("empty originate".into()));
787        }
788
789        let endpoint_str = args.remove(0);
790        let endpoint: Endpoint = endpoint_str.parse()?;
791
792        if args.is_empty() {
793            return Err(OriginateError::ParseError(
794                "missing target in originate".into(),
795            ));
796        }
797
798        let target_str = originate_unquote(&args.remove(0));
799
800        let dialplan = args
801            .first()
802            .and_then(|s| {
803                s.parse::<DialplanType>()
804                    .ok()
805            });
806        if dialplan.is_some() {
807            args.remove(0);
808        }
809
810        let target = super::parse_originate_target(&target_str, dialplan.as_ref())?;
811
812        let context = if !args.is_empty() {
813            Some(args.remove(0))
814        } else {
815            None
816        };
817        let cid_name = if !args.is_empty() {
818            let v = args.remove(0);
819            if v.eq_ignore_ascii_case(UNDEF) {
820                None
821            } else {
822                Some(v)
823            }
824        } else {
825            None
826        };
827        let cid_num = if !args.is_empty() {
828            let v = args.remove(0);
829            if v.eq_ignore_ascii_case(UNDEF) {
830                None
831            } else {
832                Some(v)
833            }
834        } else {
835            None
836        };
837        let timeout = if !args.is_empty() {
838            Some(Duration::from_secs(
839                args.remove(0)
840                    .parse::<u64>()
841                    .map_err(|e| OriginateError::ParseError(format!("invalid timeout: {}", e)))?,
842            ))
843        } else {
844            None
845        };
846
847        // Validate via constructors then set parsed fields directly (same module)
848        let mut orig = match target {
849            OriginateTarget::Extension(ref ext) => Self::extension(endpoint, ext.clone()),
850            OriginateTarget::Application(ref app) => Self::application(endpoint, app.clone()),
851            OriginateTarget::InlineApplications(ref apps) => Self::inline(endpoint, apps.clone())?,
852        };
853        orig.dialplan = dialplan;
854        orig.context = context;
855        orig.cid_name = cid_name;
856        orig.cid_num = cid_num;
857        orig.timeout = timeout;
858        Ok(orig)
859    }
860}
861
862/// Errors from originate command parsing or construction.
863#[derive(Debug, thiserror::Error)]
864#[non_exhaustive]
865pub enum OriginateError {
866    /// A single-quoted token was never closed.
867    #[error("unclosed quote at: {0}")]
868    UnclosedQuote(String),
869    /// General parse failure with a description.
870    #[error("parse error: {0}")]
871    ParseError(String),
872    /// Inline originate requires at least one application.
873    #[error("inline originate requires at least one application")]
874    EmptyInlineApplications,
875    /// Extension target cannot use inline dialplan.
876    #[error("extension target is incompatible with inline dialplan")]
877    ExtensionWithInlineDialplan,
878}
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883    use crate::commands::endpoint::{LoopbackEndpoint, SofiaEndpoint, SofiaGateway};
884
885    // --- Variables ---
886
887    #[test]
888    fn variables_standard_chars() {
889        let mut vars = Variables::new(VariablesType::Default);
890        vars.insert("test_key", "this_value");
891        let result = vars.to_string();
892        assert!(result.contains("test_key"));
893        assert!(result.contains("this_value"));
894    }
895
896    #[test]
897    fn variables_comma_escaped() {
898        let mut vars = Variables::new(VariablesType::Default);
899        vars.insert("test_key", "this,is,a,value");
900        let result = vars.to_string();
901        assert!(result.contains("\\,"));
902    }
903
904    #[test]
905    fn variables_spaces_quoted() {
906        let mut vars = Variables::new(VariablesType::Default);
907        vars.insert("test_key", "this is a value");
908        let result = vars.to_string();
909        assert_eq!(
910            result
911                .matches('\'')
912                .count(),
913            2
914        );
915    }
916
917    #[test]
918    fn variables_single_quote_escaped() {
919        let mut vars = Variables::new(VariablesType::Default);
920        vars.insert("test_key", "let's_this_be_a_value");
921        let result = vars.to_string();
922        assert!(result.contains("\\'"));
923    }
924
925    #[test]
926    fn variables_enterprise_delimiters() {
927        let mut vars = Variables::new(VariablesType::Enterprise);
928        vars.insert("k", "v");
929        let result = vars.to_string();
930        assert!(result.starts_with('<'));
931        assert!(result.ends_with('>'));
932    }
933
934    #[test]
935    fn variables_channel_delimiters() {
936        let mut vars = Variables::new(VariablesType::Channel);
937        vars.insert("k", "v");
938        let result = vars.to_string();
939        assert!(result.starts_with('['));
940        assert!(result.ends_with(']'));
941    }
942
943    #[test]
944    fn variables_default_delimiters() {
945        let mut vars = Variables::new(VariablesType::Default);
946        vars.insert("k", "v");
947        let result = vars.to_string();
948        assert!(result.starts_with('{'));
949        assert!(result.ends_with('}'));
950    }
951
952    #[test]
953    fn variables_parse_round_trip() {
954        let mut vars = Variables::new(VariablesType::Default);
955        vars.insert("origination_caller_id_number", "9005551212");
956        vars.insert("sip_h_Call-Info", "<url>;meta=123,<uri>");
957        let s = vars.to_string();
958        let parsed: Variables = s
959            .parse()
960            .unwrap();
961        assert_eq!(
962            parsed.get("origination_caller_id_number"),
963            Some("9005551212")
964        );
965        assert_eq!(parsed.get("sip_h_Call-Info"), Some("<url>;meta=123,<uri>"));
966    }
967
968    #[test]
969    fn split_unescaped_commas_basic() {
970        assert_eq!(split_unescaped_commas("a,b,c"), vec!["a", "b", "c"]);
971    }
972
973    #[test]
974    fn split_unescaped_commas_escaped() {
975        assert_eq!(split_unescaped_commas(r"a\,b,c"), vec![r"a\,b", "c"]);
976    }
977
978    #[test]
979    fn split_unescaped_commas_double_backslash() {
980        // \\, = escaped backslash + comma delimiter
981        assert_eq!(split_unescaped_commas(r"a\\,b"), vec![r"a\\", "b"]);
982    }
983
984    #[test]
985    fn split_unescaped_commas_triple_backslash() {
986        // \\\, = escaped backslash + escaped comma (no split)
987        assert_eq!(split_unescaped_commas(r"a\\\,b"), vec![r"a\\\,b"]);
988    }
989
990    // --- Endpoint ---
991
992    #[test]
993    fn endpoint_uri_only() {
994        let ep = Endpoint::Sofia(SofiaEndpoint {
995            profile: "internal".into(),
996            destination: "123@example.com".into(),
997            variables: None,
998        });
999        assert_eq!(ep.to_string(), "sofia/internal/123@example.com");
1000    }
1001
1002    #[test]
1003    fn endpoint_uri_with_variable() {
1004        let mut vars = Variables::new(VariablesType::Default);
1005        vars.insert("one_variable", "1");
1006        let ep = Endpoint::Sofia(SofiaEndpoint {
1007            profile: "internal".into(),
1008            destination: "123@example.com".into(),
1009            variables: Some(vars),
1010        });
1011        assert_eq!(
1012            ep.to_string(),
1013            "{one_variable=1}sofia/internal/123@example.com"
1014        );
1015    }
1016
1017    #[test]
1018    fn endpoint_variable_with_quote() {
1019        let mut vars = Variables::new(VariablesType::Default);
1020        vars.insert("one_variable", "one'quote");
1021        let ep = Endpoint::Sofia(SofiaEndpoint {
1022            profile: "internal".into(),
1023            destination: "123@example.com".into(),
1024            variables: Some(vars),
1025        });
1026        assert_eq!(
1027            ep.to_string(),
1028            "{one_variable=one\\'quote}sofia/internal/123@example.com"
1029        );
1030    }
1031
1032    #[test]
1033    fn loopback_endpoint_display() {
1034        let mut vars = Variables::new(VariablesType::Default);
1035        vars.insert("one_variable", "1");
1036        let ep = Endpoint::Loopback(
1037            LoopbackEndpoint::new("aUri")
1038                .with_context("aContext")
1039                .with_variables(vars),
1040        );
1041        assert_eq!(ep.to_string(), "{one_variable=1}loopback/aUri/aContext");
1042    }
1043
1044    #[test]
1045    fn sofia_gateway_endpoint_display() {
1046        let mut vars = Variables::new(VariablesType::Default);
1047        vars.insert("one_variable", "1");
1048        let ep = Endpoint::SofiaGateway(SofiaGateway {
1049            destination: "aUri".into(),
1050            profile: None,
1051            gateway: "internal".into(),
1052            variables: Some(vars),
1053        });
1054        assert_eq!(
1055            ep.to_string(),
1056            "{one_variable=1}sofia/gateway/internal/aUri"
1057        );
1058    }
1059
1060    // --- Application ---
1061
1062    #[test]
1063    fn application_xml_format() {
1064        let app = Application::new("testApp", Some("testArg"));
1065        assert_eq!(
1066            app.to_string_with_dialplan(&DialplanType::Xml),
1067            "&testApp(testArg)"
1068        );
1069    }
1070
1071    #[test]
1072    fn application_inline_format() {
1073        let app = Application::new("testApp", Some("testArg"));
1074        assert_eq!(
1075            app.to_string_with_dialplan(&DialplanType::Inline),
1076            "testApp:testArg"
1077        );
1078    }
1079
1080    #[test]
1081    fn application_inline_no_args() {
1082        let app = Application::simple("park");
1083        assert_eq!(app.to_string_with_dialplan(&DialplanType::Inline), "park");
1084    }
1085
1086    // --- Originate ---
1087
1088    #[test]
1089    fn originate_xml_display() {
1090        let ep = Endpoint::Sofia(SofiaEndpoint {
1091            profile: "internal".into(),
1092            destination: "123@example.com".into(),
1093            variables: None,
1094        });
1095        let orig = Originate::application(ep, Application::new("conference", Some("1")))
1096            .dialplan(DialplanType::Xml)
1097            .unwrap();
1098        assert_eq!(
1099            orig.to_string(),
1100            "originate sofia/internal/123@example.com &conference(1) XML"
1101        );
1102    }
1103
1104    #[test]
1105    fn originate_inline_display() {
1106        let ep = Endpoint::Sofia(SofiaEndpoint {
1107            profile: "internal".into(),
1108            destination: "123@example.com".into(),
1109            variables: None,
1110        });
1111        let orig = Originate::inline(ep, vec![Application::new("conference", Some("1"))])
1112            .unwrap()
1113            .dialplan(DialplanType::Inline)
1114            .unwrap();
1115        assert_eq!(
1116            orig.to_string(),
1117            "originate sofia/internal/123@example.com conference:1 inline"
1118        );
1119    }
1120
1121    #[test]
1122    fn originate_extension_display() {
1123        let ep = Endpoint::Sofia(SofiaEndpoint {
1124            profile: "internal".into(),
1125            destination: "123@example.com".into(),
1126            variables: None,
1127        });
1128        let orig = Originate::extension(ep, "1000")
1129            .dialplan(DialplanType::Xml)
1130            .unwrap()
1131            .context("default");
1132        assert_eq!(
1133            orig.to_string(),
1134            "originate sofia/internal/123@example.com 1000 XML default"
1135        );
1136    }
1137
1138    #[test]
1139    fn originate_extension_round_trip() {
1140        let input = "originate sofia/internal/test@example.com 1000 XML default";
1141        let parsed: Originate = input
1142            .parse()
1143            .unwrap();
1144        assert_eq!(parsed.to_string(), input);
1145        assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1146    }
1147
1148    #[test]
1149    fn originate_extension_no_dialplan() {
1150        let input = "originate sofia/internal/test@example.com 1000";
1151        let parsed: Originate = input
1152            .parse()
1153            .unwrap();
1154        assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1155        assert_eq!(parsed.to_string(), input);
1156    }
1157
1158    #[test]
1159    fn originate_extension_with_inline_errors() {
1160        let ep = Endpoint::Sofia(SofiaEndpoint {
1161            profile: "internal".into(),
1162            destination: "123@example.com".into(),
1163            variables: None,
1164        });
1165        let result = Originate::extension(ep, "1000").dialplan(DialplanType::Inline);
1166        assert!(result.is_err());
1167    }
1168
1169    #[test]
1170    fn originate_empty_inline_errors() {
1171        let ep = Endpoint::Sofia(SofiaEndpoint {
1172            profile: "internal".into(),
1173            destination: "123@example.com".into(),
1174            variables: None,
1175        });
1176        let result = Originate::inline(ep, vec![]);
1177        assert!(result.is_err());
1178    }
1179
1180    #[test]
1181    fn originate_from_string_round_trip() {
1182        let input = "originate {test='variable with quote'}sofia/internal/test@example.com 123";
1183        let orig: Originate = input
1184            .parse()
1185            .unwrap();
1186        assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "123"));
1187        assert_eq!(orig.to_string(), input);
1188    }
1189
1190    #[test]
1191    fn originate_socket_app_quoted() {
1192        let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1193        let orig = Originate::application(
1194            ep,
1195            Application::new("socket", Some("127.0.0.1:8040 async full")),
1196        );
1197        assert_eq!(
1198            orig.to_string(),
1199            "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'"
1200        );
1201    }
1202
1203    #[test]
1204    fn originate_socket_round_trip() {
1205        let input = "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'";
1206        let parsed: Originate = input
1207            .parse()
1208            .unwrap();
1209        assert_eq!(parsed.to_string(), input);
1210        if let OriginateTarget::Application(ref app) = parsed.target() {
1211            assert_eq!(
1212                app.args
1213                    .as_deref(),
1214                Some("127.0.0.1:8040 async full")
1215            );
1216        } else {
1217            panic!("expected Application target");
1218        }
1219    }
1220
1221    #[test]
1222    fn originate_display_round_trip() {
1223        let ep = Endpoint::Sofia(SofiaEndpoint {
1224            profile: "internal".into(),
1225            destination: "123@example.com".into(),
1226            variables: None,
1227        });
1228        let orig = Originate::application(ep, Application::new("conference", Some("1")))
1229            .dialplan(DialplanType::Xml)
1230            .unwrap();
1231        let s = orig.to_string();
1232        let parsed: Originate = s
1233            .parse()
1234            .unwrap();
1235        assert_eq!(parsed.to_string(), s);
1236    }
1237
1238    #[test]
1239    fn originate_inline_no_args_round_trip() {
1240        let input = "originate sofia/internal/123@example.com park inline";
1241        let parsed: Originate = input
1242            .parse()
1243            .unwrap();
1244        assert_eq!(parsed.to_string(), input);
1245        if let OriginateTarget::InlineApplications(ref apps) = parsed.target() {
1246            assert!(apps[0]
1247                .args
1248                .is_none());
1249        } else {
1250            panic!("expected InlineApplications target");
1251        }
1252    }
1253
1254    #[test]
1255    fn originate_inline_multi_app_round_trip() {
1256        let input =
1257            "originate sofia/internal/123@example.com playback:/tmp/test.wav,hangup:NORMAL_CLEARING inline";
1258        let parsed: Originate = input
1259            .parse()
1260            .unwrap();
1261        assert_eq!(parsed.to_string(), input);
1262    }
1263
1264    #[test]
1265    fn originate_inline_auto_dialplan() {
1266        let ep = Endpoint::Sofia(SofiaEndpoint {
1267            profile: "internal".into(),
1268            destination: "123@example.com".into(),
1269            variables: None,
1270        });
1271        let orig = Originate::inline(ep, vec![Application::simple("park")]).unwrap();
1272        assert!(orig
1273            .to_string()
1274            .contains("inline"));
1275    }
1276
1277    // --- DialplanType ---
1278
1279    #[test]
1280    fn dialplan_type_display() {
1281        assert_eq!(DialplanType::Inline.to_string(), "inline");
1282        assert_eq!(DialplanType::Xml.to_string(), "XML");
1283    }
1284
1285    #[test]
1286    fn dialplan_type_from_str() {
1287        assert_eq!(
1288            "inline"
1289                .parse::<DialplanType>()
1290                .unwrap(),
1291            DialplanType::Inline
1292        );
1293        assert_eq!(
1294            "XML"
1295                .parse::<DialplanType>()
1296                .unwrap(),
1297            DialplanType::Xml
1298        );
1299    }
1300
1301    #[test]
1302    fn dialplan_type_from_str_case_insensitive() {
1303        assert_eq!(
1304            "xml"
1305                .parse::<DialplanType>()
1306                .unwrap(),
1307            DialplanType::Xml
1308        );
1309        assert_eq!(
1310            "Xml"
1311                .parse::<DialplanType>()
1312                .unwrap(),
1313            DialplanType::Xml
1314        );
1315        assert_eq!(
1316            "INLINE"
1317                .parse::<DialplanType>()
1318                .unwrap(),
1319            DialplanType::Inline
1320        );
1321        assert_eq!(
1322            "Inline"
1323                .parse::<DialplanType>()
1324                .unwrap(),
1325            DialplanType::Inline
1326        );
1327    }
1328
1329    // --- Serde ---
1330
1331    #[test]
1332    fn serde_dialplan_type_xml() {
1333        let json = serde_json::to_string(&DialplanType::Xml).unwrap();
1334        assert_eq!(json, "\"xml\"");
1335        let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1336        assert_eq!(parsed, DialplanType::Xml);
1337    }
1338
1339    #[test]
1340    fn serde_dialplan_type_inline() {
1341        let json = serde_json::to_string(&DialplanType::Inline).unwrap();
1342        assert_eq!(json, "\"inline\"");
1343        let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1344        assert_eq!(parsed, DialplanType::Inline);
1345    }
1346
1347    #[test]
1348    fn serde_variables_type() {
1349        let json = serde_json::to_string(&VariablesType::Enterprise).unwrap();
1350        assert_eq!(json, "\"enterprise\"");
1351        let parsed: VariablesType = serde_json::from_str(&json).unwrap();
1352        assert_eq!(parsed, VariablesType::Enterprise);
1353    }
1354
1355    #[test]
1356    fn serde_variables_flat_default() {
1357        let mut vars = Variables::new(VariablesType::Default);
1358        vars.insert("key1", "val1");
1359        vars.insert("key2", "val2");
1360        let json = serde_json::to_string(&vars).unwrap();
1361        // Default scope serializes as a flat map
1362        let parsed: Variables = serde_json::from_str(&json).unwrap();
1363        assert_eq!(parsed.scope(), VariablesType::Default);
1364        assert_eq!(parsed.get("key1"), Some("val1"));
1365        assert_eq!(parsed.get("key2"), Some("val2"));
1366    }
1367
1368    #[test]
1369    fn serde_variables_scoped_enterprise() {
1370        let mut vars = Variables::new(VariablesType::Enterprise);
1371        vars.insert("key1", "val1");
1372        let json = serde_json::to_string(&vars).unwrap();
1373        // Non-default scope serializes as {scope, vars}
1374        assert!(json.contains("\"enterprise\""));
1375        let parsed: Variables = serde_json::from_str(&json).unwrap();
1376        assert_eq!(parsed.scope(), VariablesType::Enterprise);
1377        assert_eq!(parsed.get("key1"), Some("val1"));
1378    }
1379
1380    #[test]
1381    fn serde_variables_flat_map_deserializes_as_default() {
1382        let json = r#"{"key1":"val1","key2":"val2"}"#;
1383        let vars: Variables = serde_json::from_str(json).unwrap();
1384        assert_eq!(vars.scope(), VariablesType::Default);
1385        assert_eq!(vars.get("key1"), Some("val1"));
1386        assert_eq!(vars.get("key2"), Some("val2"));
1387    }
1388
1389    #[test]
1390    fn serde_variables_scoped_deserializes() {
1391        let json = r#"{"scope":"channel","vars":{"k":"v"}}"#;
1392        let vars: Variables = serde_json::from_str(json).unwrap();
1393        assert_eq!(vars.scope(), VariablesType::Channel);
1394        assert_eq!(vars.get("k"), Some("v"));
1395    }
1396
1397    #[test]
1398    fn serde_application() {
1399        let app = Application::new("park", None::<&str>);
1400        let json = serde_json::to_string(&app).unwrap();
1401        let parsed: Application = serde_json::from_str(&json).unwrap();
1402        assert_eq!(parsed, app);
1403    }
1404
1405    #[test]
1406    fn serde_application_with_args() {
1407        let app = Application::new("conference", Some("1"));
1408        let json = serde_json::to_string(&app).unwrap();
1409        let parsed: Application = serde_json::from_str(&json).unwrap();
1410        assert_eq!(parsed, app);
1411    }
1412
1413    #[test]
1414    fn serde_application_skips_none_args() {
1415        let app = Application::new("park", None::<&str>);
1416        let json = serde_json::to_string(&app).unwrap();
1417        assert!(!json.contains("args"));
1418    }
1419
1420    #[test]
1421    fn serde_originate_application_round_trip() {
1422        let ep = Endpoint::Sofia(SofiaEndpoint {
1423            profile: "internal".into(),
1424            destination: "123@example.com".into(),
1425            variables: None,
1426        });
1427        let orig = Originate::application(ep, Application::new("park", None::<&str>))
1428            .dialplan(DialplanType::Xml)
1429            .unwrap()
1430            .context("default")
1431            .cid_name("Test")
1432            .cid_num("5551234")
1433            .timeout(Duration::from_secs(30));
1434        let json = serde_json::to_string(&orig).unwrap();
1435        assert!(json.contains("\"application\""));
1436        let parsed: Originate = serde_json::from_str(&json).unwrap();
1437        assert_eq!(parsed, orig);
1438    }
1439
1440    #[test]
1441    fn serde_originate_extension() {
1442        let json = r#"{
1443            "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1444            "extension": "1000",
1445            "dialplan": "xml",
1446            "context": "default"
1447        }"#;
1448        let orig: Originate = serde_json::from_str(json).unwrap();
1449        assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1450        assert_eq!(
1451            orig.to_string(),
1452            "originate sofia/internal/123@example.com 1000 XML default"
1453        );
1454    }
1455
1456    #[test]
1457    fn serde_originate_extension_with_inline_rejected() {
1458        let json = r#"{
1459            "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1460            "extension": "1000",
1461            "dialplan": "inline"
1462        }"#;
1463        let result = serde_json::from_str::<Originate>(json);
1464        assert!(result.is_err());
1465    }
1466
1467    #[test]
1468    fn serde_originate_empty_inline_rejected() {
1469        let json = r#"{
1470            "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1471            "inline_applications": []
1472        }"#;
1473        let result = serde_json::from_str::<Originate>(json);
1474        assert!(result.is_err());
1475    }
1476
1477    #[test]
1478    fn serde_originate_inline_applications() {
1479        let json = r#"{
1480            "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1481            "inline_applications": [
1482                {"name": "playback", "args": "/tmp/test.wav"},
1483                {"name": "hangup", "args": "NORMAL_CLEARING"}
1484            ]
1485        }"#;
1486        let orig: Originate = serde_json::from_str(json).unwrap();
1487        if let OriginateTarget::InlineApplications(ref apps) = orig.target() {
1488            assert_eq!(apps.len(), 2);
1489        } else {
1490            panic!("expected InlineApplications");
1491        }
1492        assert!(orig
1493            .to_string()
1494            .contains("inline"));
1495    }
1496
1497    #[test]
1498    fn serde_originate_skips_none_fields() {
1499        let ep = Endpoint::Sofia(SofiaEndpoint {
1500            profile: "internal".into(),
1501            destination: "123@example.com".into(),
1502            variables: None,
1503        });
1504        let orig = Originate::application(ep, Application::new("park", None::<&str>));
1505        let json = serde_json::to_string(&orig).unwrap();
1506        assert!(!json.contains("dialplan"));
1507        assert!(!json.contains("context"));
1508        assert!(!json.contains("cid_name"));
1509        assert!(!json.contains("cid_num"));
1510        assert!(!json.contains("timeout"));
1511    }
1512
1513    #[test]
1514    fn serde_originate_to_wire_format() {
1515        let json = r#"{
1516            "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1517            "application": {"name": "park"},
1518            "dialplan": "xml",
1519            "context": "default"
1520        }"#;
1521        let orig: Originate = serde_json::from_str(json).unwrap();
1522        let wire = orig.to_string();
1523        assert!(wire.starts_with("originate"));
1524        assert!(wire.contains("sofia/internal/123@example.com"));
1525        assert!(wire.contains("&park()"));
1526        assert!(wire.contains("XML"));
1527    }
1528
1529    // --- Application::simple ---
1530
1531    #[test]
1532    fn application_simple_no_args() {
1533        let app = Application::simple("park");
1534        assert_eq!(app.name, "park");
1535        assert!(app
1536            .args
1537            .is_none());
1538    }
1539
1540    #[test]
1541    fn application_simple_xml_format() {
1542        let app = Application::simple("park");
1543        assert_eq!(app.to_string_with_dialplan(&DialplanType::Xml), "&park()");
1544    }
1545
1546    // --- OriginateTarget From impls ---
1547
1548    #[test]
1549    fn originate_target_from_application() {
1550        let target: OriginateTarget = Application::simple("park").into();
1551        assert!(matches!(target, OriginateTarget::Application(_)));
1552    }
1553
1554    #[test]
1555    fn originate_target_from_vec() {
1556        let target: OriginateTarget = vec![
1557            Application::new("conference", Some("1")),
1558            Application::new("hangup", Some("NORMAL_CLEARING")),
1559        ]
1560        .into();
1561        if let OriginateTarget::InlineApplications(apps) = target {
1562            assert_eq!(apps.len(), 2);
1563        } else {
1564            panic!("expected InlineApplications");
1565        }
1566    }
1567
1568    #[test]
1569    fn originate_target_application_wire_format() {
1570        let ep = Endpoint::Sofia(SofiaEndpoint {
1571            profile: "internal".into(),
1572            destination: "123@example.com".into(),
1573            variables: None,
1574        });
1575        let orig = Originate::application(ep, Application::simple("park"));
1576        assert_eq!(
1577            orig.to_string(),
1578            "originate sofia/internal/123@example.com &park()"
1579        );
1580    }
1581
1582    #[test]
1583    fn originate_timeout_only_fills_positional_gaps() {
1584        let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1585        let cmd = Originate::application(ep, Application::simple("park"))
1586            .timeout(Duration::from_secs(30));
1587        // timeout is arg 7; dialplan/context/cid must be filled so FS
1588        // doesn't interpret "30" as the dialplan name
1589        assert_eq!(
1590            cmd.to_string(),
1591            "originate loopback/9199/test &park() XML default undef undef 30"
1592        );
1593    }
1594
1595    #[test]
1596    fn originate_cid_num_only_fills_preceding_gaps() {
1597        let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1598        let cmd = Originate::application(ep, Application::simple("park")).cid_num("5551234");
1599        assert_eq!(
1600            cmd.to_string(),
1601            "originate loopback/9199/test &park() XML default undef 5551234"
1602        );
1603    }
1604
1605    #[test]
1606    fn originate_context_only_fills_dialplan() {
1607        let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1608        let cmd = Originate::extension(ep, "1000").context("myctx");
1609        assert_eq!(
1610            cmd.to_string(),
1611            "originate loopback/9199/test 1000 XML myctx"
1612        );
1613    }
1614
1615    /// `context: None` with a later positional arg emits `"default"` as a
1616    /// gap-filler. `FromStr` reads it back as `Some("default")` because
1617    /// `"default"` is also a valid user-specified context. The wire format
1618    /// round-trips correctly (identical string), but the struct-level
1619    /// representation differs. This is an accepted asymmetry.
1620    #[test]
1621    fn originate_context_gap_filler_round_trip_asymmetry() {
1622        let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1623        let cmd = Originate::application(ep, Application::simple("park")).cid_name("Alice");
1624        let wire = cmd.to_string();
1625        assert!(wire.contains("default"), "gap-filler should emit 'default'");
1626
1627        let parsed: Originate = wire
1628            .parse()
1629            .unwrap();
1630        // Struct-level asymmetry: None became Some("default")
1631        assert_eq!(parsed.context_str(), Some("default"));
1632
1633        // Wire format is identical (the important invariant)
1634        assert_eq!(parsed.to_string(), wire);
1635    }
1636
1637    // --- T1: Full Originate serde round-trip ---
1638
1639    #[test]
1640    fn serde_originate_full_round_trip_with_variables() {
1641        let mut ep_vars = Variables::new(VariablesType::Default);
1642        ep_vars.insert("originate_timeout", "30");
1643        ep_vars.insert("sip_h_X-Custom", "value with spaces");
1644        let ep = Endpoint::SofiaGateway(SofiaGateway {
1645            gateway: "my_provider".into(),
1646            destination: "18005551234".into(),
1647            profile: Some("external".into()),
1648            variables: Some(ep_vars),
1649        });
1650        let orig = Originate::application(ep, Application::new("park", None::<&str>))
1651            .dialplan(DialplanType::Xml)
1652            .unwrap()
1653            .context("public")
1654            .cid_name("Test Caller")
1655            .cid_num("5551234")
1656            .timeout(Duration::from_secs(60));
1657        let json = serde_json::to_string(&orig).unwrap();
1658        let parsed: Originate = serde_json::from_str(&json).unwrap();
1659        assert_eq!(parsed, orig);
1660        assert_eq!(parsed.to_string(), orig.to_string());
1661    }
1662
1663    #[test]
1664    fn serde_originate_inline_round_trip_with_all_fields() {
1665        let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1666        let orig = Originate::inline(
1667            ep,
1668            vec![
1669                Application::new("playback", Some("/tmp/test.wav")),
1670                Application::new("hangup", Some("NORMAL_CLEARING")),
1671            ],
1672        )
1673        .unwrap()
1674        .dialplan(DialplanType::Inline)
1675        .unwrap()
1676        .context("default")
1677        .cid_name("IVR")
1678        .cid_num("0000")
1679        .timeout(Duration::from_secs(45));
1680        let json = serde_json::to_string(&orig).unwrap();
1681        let parsed: Originate = serde_json::from_str(&json).unwrap();
1682        assert_eq!(parsed, orig);
1683        assert_eq!(parsed.to_string(), orig.to_string());
1684    }
1685
1686    // --- T5: Variables::from_str with empty block ---
1687
1688    #[test]
1689    fn variables_from_str_empty_block() {
1690        let result = "{}".parse::<Variables>();
1691        assert!(
1692            result.is_ok(),
1693            "empty variable block should parse successfully"
1694        );
1695        let vars = result.unwrap();
1696        assert!(
1697            vars.is_empty(),
1698            "parsed empty block should have no variables"
1699        );
1700    }
1701
1702    #[test]
1703    fn variables_from_str_empty_channel_block() {
1704        let result = "[]".parse::<Variables>();
1705        assert!(result.is_ok());
1706        let vars = result.unwrap();
1707        assert!(vars.is_empty());
1708        assert_eq!(vars.scope(), VariablesType::Channel);
1709    }
1710
1711    #[test]
1712    fn variables_from_str_empty_enterprise_block() {
1713        let result = "<>".parse::<Variables>();
1714        assert!(result.is_ok());
1715        let vars = result.unwrap();
1716        assert!(vars.is_empty());
1717        assert_eq!(vars.scope(), VariablesType::Enterprise);
1718    }
1719
1720    // --- T5: Originate::from_str with context named "inline" or "XML" ---
1721
1722    #[test]
1723    fn originate_context_named_inline() {
1724        let ep = Endpoint::Sofia(SofiaEndpoint {
1725            profile: "internal".into(),
1726            destination: "123@example.com".into(),
1727            variables: None,
1728        });
1729        let orig = Originate::extension(ep, "1000")
1730            .dialplan(DialplanType::Xml)
1731            .unwrap()
1732            .context("inline");
1733        let wire = orig.to_string();
1734        assert!(wire.contains("XML inline"), "wire: {}", wire);
1735        let parsed: Originate = wire
1736            .parse()
1737            .unwrap();
1738        // "inline" is consumed as the dialplan type, not the context
1739        // This is an accepted limitation of positional parsing
1740        assert_eq!(parsed.to_string(), wire);
1741    }
1742
1743    #[test]
1744    fn originate_context_named_xml() {
1745        let ep = Endpoint::Sofia(SofiaEndpoint {
1746            profile: "internal".into(),
1747            destination: "123@example.com".into(),
1748            variables: None,
1749        });
1750        let orig = Originate::extension(ep, "1000")
1751            .dialplan(DialplanType::Xml)
1752            .unwrap()
1753            .context("XML");
1754        let wire = orig.to_string();
1755        // "XML XML" - first is dialplan, second is context
1756        assert!(wire.contains("XML XML"), "wire: {}", wire);
1757        let parsed: Originate = wire
1758            .parse()
1759            .unwrap();
1760        assert_eq!(parsed.to_string(), wire);
1761    }
1762
1763    #[test]
1764    fn originate_accessors() {
1765        let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1766        let cmd = Originate::extension(ep, "1000")
1767            .dialplan(DialplanType::Xml)
1768            .unwrap()
1769            .context("default")
1770            .cid_name("Alice")
1771            .cid_num("5551234")
1772            .timeout(Duration::from_secs(30));
1773
1774        assert!(matches!(cmd.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1775        assert_eq!(cmd.dialplan_type(), Some(&DialplanType::Xml));
1776        assert_eq!(cmd.context_str(), Some("default"));
1777        assert_eq!(cmd.caller_id_name(), Some("Alice"));
1778        assert_eq!(cmd.caller_id_number(), Some("5551234"));
1779        assert_eq!(cmd.timeout_seconds(), Some(30));
1780    }
1781}