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