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