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