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