Skip to main content

freeswitch_types/commands/endpoint/
mod.rs

1//! FreeSWITCH endpoint types for originate and bridge dial strings.
2//!
3//! Each endpoint type corresponds to a real FreeSWITCH endpoint module
4//! or runtime expression. Concrete structs implement the [`DialString`]
5//! trait independently; the [`Endpoint`] enum wraps them for
6//! serialization and polymorphic storage.
7
8/// Emit the [`DialString`] impl and the `with_variables` builder for an endpoint
9/// struct holding `variables: Option<Variables>`. Given the module text and the
10/// function writing it, also emit the target-aware `write_for` — variable-block
11/// prologue included — and the [`Display`](std::fmt::Display) that renders it for
12/// the default target.
13macro_rules! impl_dial_string_with_variables {
14    ($ty:ty) => {
15        impl $crate::commands::endpoint::DialString for $ty {
16            fn variables(&self) -> Option<&$crate::commands::variables::Variables> {
17                self.variables
18                    .as_ref()
19            }
20            fn variables_mut(&mut self) -> Option<&mut $crate::commands::variables::Variables> {
21                self.variables
22                    .as_mut()
23            }
24            fn set_variables(&mut self, vars: Option<$crate::commands::variables::Variables>) {
25                self.variables = vars;
26            }
27        }
28
29        impl $ty {
30            /// Set per-channel variables.
31            pub fn with_variables(
32                mut self,
33                variables: $crate::commands::variables::Variables,
34            ) -> Self {
35                self.variables = Some(variables);
36                self
37            }
38        }
39    };
40    ($ty:ty, $write:ident, |$this:ident| $text:expr) => {
41        impl_dial_string_with_variables!($ty);
42
43        impl $ty {
44            /// The text after the variable block, as the endpoint module receives it.
45            pub(crate) fn module_text(&self) -> String {
46                let $this = self;
47                $text
48            }
49
50            pub(super) fn write_for(
51                &self,
52                f: &mut ::std::fmt::Formatter<'_>,
53                target: $crate::commands::variables::DialStringTarget,
54            ) -> ::std::fmt::Result {
55                $crate::commands::endpoint::write_variables(f, &self.variables, target)?;
56                $crate::commands::endpoint::$write(f, &self.module_text(), target)
57            }
58        }
59
60        impl ::std::fmt::Display for $ty {
61            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
62                self.write_for(
63                    f,
64                    $crate::commands::variables::DialStringCarrier::EslApi.into(),
65                )
66            }
67        }
68    };
69}
70
71/// Dispatch a [`DialString`] method to whichever concrete endpoint the
72/// [`Endpoint`] variant wraps.
73macro_rules! forward_to_variant {
74    ($self:ident, $method:ident $(, $arg:expr)?) => {
75        match $self {
76            Self::Sofia(ep) => ep.$method($($arg)?),
77            Self::SofiaGateway(ep) => ep.$method($($arg)?),
78            Self::Loopback(ep) => ep.$method($($arg)?),
79            Self::User(ep) => ep.$method($($arg)?),
80            Self::SofiaContact(ep) => ep.$method($($arg)?),
81            Self::GroupCall(ep) => ep.$method($($arg)?),
82            Self::Error(ep) => ep.$method($($arg)?),
83            Self::PortAudio(ep) | Self::PulseAudio(ep) | Self::Alsa(ep) => ep.$method($($arg)?),
84        }
85    };
86}
87
88/// Emit `FromStr` at the ESL API carrier for each endpoint parsed from a dial string, or the serde
89/// `config` module a file's endpoints load through, each load refusing what `check_deliverable` refuses.
90macro_rules! impl_endpoint_parse {
91    (from_str: $($ty:ident),* $(,)?) => {
92        $(
93            impl ::std::str::FromStr for $ty {
94                type Err = $crate::commands::originate::OriginateError;
95
96                fn from_str(s: &str) -> Result<Self, Self::Err> {
97                    let (variables, ep) = $crate::commands::endpoint::parse_leg(
98                        s,
99                        $crate::commands::variables::DialStringCarrier::EslApi.into(),
100                        Self::parse_bare,
101                    )?;
102                    Ok(Self { variables, ..ep })
103                }
104            }
105        )*
106    };
107    (expression_from_str: $($ty:ident => $kind:literal),* $(,)?) => {
108        $(
109            impl ::std::str::FromStr for $ty {
110                type Err = $crate::commands::originate::OriginateError;
111
112                /// Refused once parsed: `FromStr` reads the API carrier, which never expands it.
113                fn from_str(s: &str) -> Result<Self, Self::Err> {
114                    $crate::commands::endpoint::parse_leg(
115                        s,
116                        $crate::commands::variables::DialStringCarrier::EslApi.into(),
117                        Self::parse_bare,
118                    )?;
119                    Err($crate::commands::originate::OriginateError::UnexpandedExpression {
120                        endpoint: $kind,
121                    })
122                }
123            }
124        )*
125    };
126    (config: $($ty:ident { $($required:ident: $rty:ty,)* ; $($optional:ident: $oty:ty,)* })*) => {
127        #[cfg(feature = "serde")]
128        mod config {
129            use super::*;
130
131            $(
132                #[derive(serde::Deserialize)]
133                pub(super) struct $ty {
134                    $(pub(super) $required: $rty,)*
135                    $(
136                        #[serde(default)]
137                        pub(super) $optional: $oty,
138                    )*
139                }
140            )*
141        }
142
143        $(
144            #[cfg(feature = "serde")]
145            impl TryFrom<config::$ty> for $ty {
146                type Error = $crate::commands::originate::OriginateError;
147
148                fn try_from(config: config::$ty) -> Result<Self, Self::Error> {
149                    let ep = Self {
150                        $($required: config.$required,)*
151                        $($optional: config.$optional,)*
152                    };
153                    ep.check_deliverable()?;
154                    Ok(ep)
155                }
156            }
157        )*
158    };
159}
160
161mod audio;
162mod error;
163mod group_call;
164mod loopback;
165mod sofia;
166mod user;
167
168pub use audio::AudioEndpoint;
169pub use error::ErrorEndpoint;
170pub use group_call::{GroupCall, GroupCallOrder, ParseGroupCallOrderError};
171pub use loopback::LoopbackEndpoint;
172pub use sofia::{DestinationEncoding, SofiaContact, SofiaEndpoint, SofiaGateway};
173pub use user::UserEndpoint;
174
175use std::fmt;
176use std::str::FromStr;
177
178use super::originate::OriginateError;
179use super::variables::{
180    installed_variables, read_leg, DialStringCarrier, DialStringTarget, Variables,
181};
182use crate::switch_passes::brackets::unbalanced;
183use crate::switch_passes::escape::{escape_text, EscapedField};
184use crate::switch_passes::originate_legs::splits_into_threads;
185
186type PrefixParser = fn(&str) -> Result<Endpoint, OriginateError>;
187
188/// Why an endpoint field cannot reach the switch as written, whatever the escaping.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190#[non_exhaustive]
191pub enum EndpointFieldFault {
192    /// Carries `:_:`, on which `switch_ivr_originate` splits the dial string into threads.
193    EnterpriseSeparator,
194    /// Carries a separator the endpoint module splits its text on.
195    ModuleSeparator(&'static str),
196    /// A sofia profile reading `gateway` in any case, which mod_sofia takes for the gateway path.
197    ReadsAsGateway,
198    /// Set after an `app=` loopback extension, which mod_loopback reads as that application's.
199    FollowsAnApplication,
200    /// Empty, which the module or function reads as its default.
201    EmptyReadsAsDefault,
202    /// Carries a separator a `sofia_contact` or `group_call` function splits its argument on.
203    FunctionSeparator(&'static str),
204    /// Carries what a pass reads ahead of a `sofia_contact` or `group_call` expansion: a space,
205    /// `,`, `|`, a quote, a backslash, or an unbalanced brace or parenthesis.
206    ReadBeforeTheFunction,
207    /// Empty where the module needs text to dial, so it stops before placing a call.
208    EmptyDialsNothing,
209}
210
211impl fmt::Display for EndpointFieldFault {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            Self::EnterpriseSeparator => {
215                f.write_str("carries :_:, on which the switch splits the dial string into threads")
216            }
217            Self::ModuleSeparator(sep) => {
218                write!(
219                    f,
220                    "carries {sep}, on which the endpoint module splits its text"
221                )
222            }
223            Self::ReadsAsGateway => f.write_str("reads as gateway, which mod_sofia dials as one"),
224            Self::FollowsAnApplication => f.write_str(
225                "follows an app= extension, which mod_loopback reads as the application's argument",
226            ),
227            Self::EmptyReadsAsDefault => {
228                f.write_str("is empty, which the switch reads as its default")
229            }
230            Self::FunctionSeparator(sep) => write!(
231                f,
232                "carries {sep}, on which the expression's function splits its argument"
233            ),
234            Self::ReadBeforeTheFunction => f.write_str(
235                "carries a space, comma, pipe, quote, backslash or unbalanced bracket, \
236                 which a pass reads before the expression's function",
237            ),
238            Self::EmptyDialsNothing => {
239                f.write_str("is empty, so the endpoint module dials nothing")
240            }
241        }
242    }
243}
244
245/// Refuse `text` as `field` of the `endpoint` expression when it carries `:_:`, one of the
246/// function's `separators`, or what [`EndpointFieldFault::ReadBeforeTheFunction`] names.
247pub(super) fn check_expression_field(
248    endpoint: &'static str,
249    field: &'static str,
250    text: &str,
251    separators: &[&'static str],
252) -> Result<(), OriginateError> {
253    let fault = if splits_into_threads(text) {
254        Some(EndpointFieldFault::EnterpriseSeparator)
255    } else if let Some(sep) = separators
256        .iter()
257        .find(|sep| text.contains(**sep))
258    {
259        Some(EndpointFieldFault::FunctionSeparator(sep))
260    } else if text.contains([' ', ',', '|', '\'', '\\'])
261        || unbalanced(text, ('{', '}')).is_some()
262        || unbalanced(text, ('(', ')')).is_some()
263    {
264        Some(EndpointFieldFault::ReadBeforeTheFunction)
265    } else {
266        None
267    };
268    fault.map_or(Ok(()), |fault| Err(undeliverable(endpoint, field, fault)))
269}
270
271pub(super) fn undeliverable(
272    endpoint: &'static str,
273    field: &'static str,
274    fault: EndpointFieldFault,
275) -> OriginateError {
276    OriginateError::UndeliverableEndpointField {
277        endpoint,
278        field,
279        fault,
280    }
281}
282
283/// Refuse `text` as `field` of `endpoint` when it carries `:_:` or one of `separators`.
284pub(super) fn check_field(
285    endpoint: &'static str,
286    field: &'static str,
287    text: &str,
288    separators: &[&'static str],
289) -> Result<(), OriginateError> {
290    let fault = if splits_into_threads(text) {
291        Some(EndpointFieldFault::EnterpriseSeparator)
292    } else {
293        separators
294            .iter()
295            .find(|sep| text.contains(**sep))
296            .map(|sep| EndpointFieldFault::ModuleSeparator(sep))
297    };
298    fault.map_or(Ok(()), |fault| Err(undeliverable(endpoint, field, fault)))
299}
300
301/// Common interface for anything that formats as a FreeSWITCH dial string.
302///
303/// Implemented on each concrete endpoint struct and on the [`Endpoint`] enum.
304/// Downstream crates can implement this on custom endpoint types.
305pub trait DialString: fmt::Display {
306    /// Per-endpoint variables, if any.
307    fn variables(&self) -> Option<&Variables>;
308    /// Mutable access to per-endpoint variables.
309    fn variables_mut(&mut self) -> Option<&mut Variables>;
310    /// Replace per-endpoint variables.
311    fn set_variables(&mut self, vars: Option<Variables>);
312}
313
314// ---------------------------------------------------------------------------
315// Helpers
316// ---------------------------------------------------------------------------
317
318fn write_variables(
319    f: &mut fmt::Formatter<'_>,
320    vars: &Option<Variables>,
321    target: DialStringTarget,
322) -> fmt::Result {
323    if let Some(vars) = vars {
324        if !vars.is_empty() {
325            write!(f, "{}", vars.display_for(target))?;
326        }
327    }
328    Ok(())
329}
330
331/// Module text, escaped for the carrier's pass and the leg splits ahead of the module.
332fn write_module_text(
333    f: &mut fmt::Formatter<'_>,
334    text: &str,
335    target: DialStringTarget,
336) -> fmt::Result {
337    f.write_str(&escape_text(text, target, EscapedField::Endpoint))
338}
339
340/// A runtime expression, written for the switch to expand.
341fn write_expression(f: &mut fmt::Formatter<'_>, text: &str, _: DialStringTarget) -> fmt::Result {
342    f.write_str(text)
343}
344
345/// The module text after a fixed endpoint prefix. A prefix that does not itself end a path
346/// segment (`alsa`, not `sofia/`) must be followed by `/` or by nothing, or `alsafoo/bar` strips
347/// to a bare `alsa`.
348pub(super) fn after_prefix<'a>(
349    text: &'a str,
350    prefix: &str,
351    kind: &str,
352) -> Result<&'a str, OriginateError> {
353    text.strip_prefix(prefix)
354        .filter(|rest| prefix.ends_with('/') || rest.is_empty() || rest.starts_with('/'))
355        .ok_or_else(|| OriginateError::ParseError(format!("not a {kind} endpoint")))
356}
357
358/// Parse a dial string written for `target`: the one leg the switch reads of it, its block's
359/// variables, and its module text through `bare`.
360pub(super) fn parse_leg<T>(
361    s: &str,
362    target: DialStringTarget,
363    bare: impl FnOnce(&str) -> Result<T, OriginateError>,
364) -> Result<(Option<Variables>, T), OriginateError> {
365    let (argument, target) = target.read_argument(s)?;
366    let (block, text) = read_leg(&argument, target)?;
367    Ok((installed_variables(block.as_ref())?, bare(&text)?))
368}
369
370// ---------------------------------------------------------------------------
371// Endpoint enum
372// ---------------------------------------------------------------------------
373
374/// Polymorphic endpoint wrapping all concrete types.
375///
376/// Use this in [`Originate`](super::originate::Originate) and
377/// [`BridgeDialString`](super::bridge::BridgeDialString) where any endpoint type must be accepted.
378#[derive(Debug, Clone, PartialEq, Eq)]
379#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
380#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
381#[non_exhaustive]
382pub enum Endpoint {
383    /// `sofia/{profile}/{destination}`
384    Sofia(SofiaEndpoint),
385    /// `sofia/gateway/[{profile}::]{gateway}/{destination}`
386    SofiaGateway(SofiaGateway),
387    /// `loopback/{extension}[/{context}]`
388    Loopback(LoopbackEndpoint),
389    /// `user/{name}[@{domain}]`
390    User(UserEndpoint),
391    /// `${sofia_contact([profile/]user@domain)}`
392    SofiaContact(SofiaContact),
393    /// `${group_call(group@domain[+order])}`
394    GroupCall(GroupCall),
395    /// `error/{cause}`
396    Error(ErrorEndpoint),
397    /// `portaudio[/{destination}]`
398    #[cfg_attr(feature = "serde", serde(rename = "portaudio"))]
399    PortAudio(AudioEndpoint),
400    /// `pulseaudio[/{destination}]`
401    #[cfg_attr(feature = "serde", serde(rename = "pulseaudio"))]
402    PulseAudio(AudioEndpoint),
403    /// `alsa[/{destination}]`
404    Alsa(AudioEndpoint),
405}
406
407// ---------------------------------------------------------------------------
408// From impls
409// ---------------------------------------------------------------------------
410
411impl From<SofiaEndpoint> for Endpoint {
412    fn from(ep: SofiaEndpoint) -> Self {
413        Self::Sofia(ep)
414    }
415}
416
417impl From<SofiaGateway> for Endpoint {
418    fn from(ep: SofiaGateway) -> Self {
419        Self::SofiaGateway(ep)
420    }
421}
422
423impl From<LoopbackEndpoint> for Endpoint {
424    fn from(ep: LoopbackEndpoint) -> Self {
425        Self::Loopback(ep)
426    }
427}
428
429impl From<UserEndpoint> for Endpoint {
430    fn from(ep: UserEndpoint) -> Self {
431        Self::User(ep)
432    }
433}
434
435impl From<SofiaContact> for Endpoint {
436    fn from(ep: SofiaContact) -> Self {
437        Self::SofiaContact(ep)
438    }
439}
440
441impl From<GroupCall> for Endpoint {
442    fn from(ep: GroupCall) -> Self {
443        Self::GroupCall(ep)
444    }
445}
446
447impl From<ErrorEndpoint> for Endpoint {
448    fn from(ep: ErrorEndpoint) -> Self {
449        Self::Error(ep)
450    }
451}
452
453// ---------------------------------------------------------------------------
454// Display
455// ---------------------------------------------------------------------------
456
457impl Endpoint {
458    pub(crate) fn write_for(
459        &self,
460        f: &mut fmt::Formatter<'_>,
461        target: DialStringTarget,
462    ) -> fmt::Result {
463        match self {
464            Self::Sofia(ep) => ep.write_for(f, target),
465            Self::SofiaGateway(ep) => ep.write_for(f, target),
466            Self::Loopback(ep) => ep.write_for(f, target),
467            Self::User(ep) => ep.write_for(f, target),
468            Self::SofiaContact(ep) => ep.write_for(f, target),
469            Self::GroupCall(ep) => ep.write_for(f, target),
470            Self::Error(ep) => fmt::Display::fmt(ep, f),
471            Self::PortAudio(ep) => ep.write_with_prefix(f, "portaudio", target),
472            Self::PulseAudio(ep) => ep.write_with_prefix(f, "pulseaudio", target),
473            Self::Alsa(ep) => ep.write_with_prefix(f, "alsa", target),
474        }
475    }
476
477    /// Render for a named carrier or [`DialStringTarget`] rather than the
478    /// [`DialStringCarrier::EslApi`] default of [`Display`](fmt::Display).
479    pub fn display_for(&self, target: impl Into<DialStringTarget>) -> EndpointDisplay<'_> {
480        EndpointDisplay {
481            endpoint: self,
482            target: target.into(),
483        }
484    }
485
486    /// Parse a dial string written for a named carrier or [`DialStringTarget`],
487    /// mirroring [`display_for`](Self::display_for). [`FromStr`] uses the
488    /// [`DialStringCarrier::EslApi`] default.
489    ///
490    /// A `sofia_contact` or `group_call` expression parses at the dialplan carrier only.
491    pub fn parse_for(s: &str, target: impl Into<DialStringTarget>) -> Result<Self, OriginateError> {
492        let target = target.into();
493        let (variables, mut endpoint) = parse_leg(s, target, Self::parse_bare)?;
494        endpoint.check_expanded(target.carrier())?;
495        if variables.is_some() {
496            endpoint.set_variables(variables);
497            if endpoint
498                .variables()
499                .is_none()
500            {
501                return Err(OriginateError::VariablesNotSupported(endpoint.kind()));
502            }
503        }
504        Ok(endpoint)
505    }
506
507    /// Refuse an expression at a carrier that never expands it.
508    pub(crate) fn check_expanded(&self, carrier: DialStringCarrier) -> Result<(), OriginateError> {
509        match self {
510            Self::SofiaContact(_) | Self::GroupCall(_) if !carrier.expands() => {
511                Err(OriginateError::UnexpandedExpression {
512                    endpoint: self.kind(),
513                })
514            }
515            _ => Ok(()),
516        }
517    }
518
519    /// The module name this variant renders, for diagnostics.
520    pub(crate) fn kind(&self) -> &'static str {
521        match self {
522            Self::Sofia(_) => "sofia",
523            Self::SofiaGateway(_) => "sofia gateway",
524            Self::Loopback(_) => "loopback",
525            Self::User(_) => "user",
526            Self::SofiaContact(_) => "sofia_contact",
527            Self::GroupCall(_) => "group_call",
528            Self::Error(_) => "error",
529            Self::PortAudio(_) => "portaudio",
530            Self::PulseAudio(_) => "pulseaudio",
531            Self::Alsa(_) => "alsa",
532        }
533    }
534
535    /// Module prefix to parser, first match wins.
536    const PREFIX_PARSERS: &[(&str, PrefixParser)] = &[
537        ("${sofia_contact(", |u| {
538            Ok(Self::SofiaContact(SofiaContact::parse_bare(u)?))
539        }),
540        ("${group_call(", |u| {
541            Ok(Self::GroupCall(GroupCall::parse_bare(u)?))
542        }),
543        ("error/", |u| Ok(Self::Error(u.parse()?))),
544        ("loopback/", |u| {
545            Ok(Self::Loopback(LoopbackEndpoint::parse_bare(u)?))
546        }),
547        // Must precede "sofia/", which also matches a gateway string.
548        ("sofia/gateway/", |u| {
549            Ok(Self::SofiaGateway(SofiaGateway::parse_bare(u)?))
550        }),
551        ("sofia/", |u| Ok(Self::Sofia(SofiaEndpoint::parse_bare(u)?))),
552        ("user/", |u| Ok(Self::User(UserEndpoint::parse_bare(u)?))),
553        ("portaudio", |u| {
554            Ok(Self::PortAudio(AudioEndpoint::parse_bare(u, "portaudio")?))
555        }),
556        ("pulseaudio", |u| {
557            Ok(Self::PulseAudio(AudioEndpoint::parse_bare(
558                u,
559                "pulseaudio",
560            )?))
561        }),
562        ("alsa", |u| {
563            Ok(Self::Alsa(AudioEndpoint::parse_bare(u, "alsa")?))
564        }),
565    ];
566
567    /// The text after the variable block, as the endpoint module receives it.
568    #[cfg(test)]
569    pub(crate) fn module_text(&self) -> String {
570        match self {
571            Self::Sofia(ep) => ep.module_text(),
572            Self::SofiaGateway(ep) => ep.module_text(),
573            Self::Loopback(ep) => ep.module_text(),
574            Self::User(ep) => ep.module_text(),
575            Self::SofiaContact(ep) => ep.module_text(),
576            Self::GroupCall(ep) => ep.module_text(),
577            Self::Error(ep) => ep.to_string(),
578            Self::PortAudio(ep) => ep.module_text("portaudio"),
579            Self::PulseAudio(ep) => ep.module_text("pulseaudio"),
580            Self::Alsa(ep) => ep.module_text("alsa"),
581        }
582    }
583
584    /// Dispatch on the module prefix of module text as the switch's leg splits leave it,
585    /// refusing a field the module reads as something else.
586    pub(crate) fn parse_bare(uri: &str) -> Result<Self, OriginateError> {
587        let (_, parse) = Self::PREFIX_PARSERS
588            .iter()
589            .find(|(prefix, _)| uri.starts_with(prefix))
590            .ok_or_else(|| OriginateError::UnknownEndpointType(uri.to_string()))?;
591        parse(uri)
592    }
593}
594
595/// Renders an [`Endpoint`] for one target. Returned by
596/// [`Endpoint::display_for`].
597#[derive(Debug, Clone, Copy)]
598pub struct EndpointDisplay<'a> {
599    endpoint: &'a Endpoint,
600    target: DialStringTarget,
601}
602
603impl fmt::Display for EndpointDisplay<'_> {
604    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
605        self.target
606            .write_argument(f, |f, target| {
607                self.endpoint
608                    .write_for(f, target)
609            })
610    }
611}
612
613impl fmt::Display for Endpoint {
614    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
615        self.write_for(f, DialStringCarrier::EslApi.into())
616    }
617}
618
619// ---------------------------------------------------------------------------
620// FromStr
621// ---------------------------------------------------------------------------
622
623impl FromStr for Endpoint {
624    type Err = OriginateError;
625
626    fn from_str(s: &str) -> Result<Self, Self::Err> {
627        Self::parse_for(s, DialStringCarrier::EslApi)
628    }
629}
630
631// ---------------------------------------------------------------------------
632// DialString impls
633// ---------------------------------------------------------------------------
634
635impl DialString for ErrorEndpoint {
636    fn variables(&self) -> Option<&Variables> {
637        None
638    }
639    fn variables_mut(&mut self) -> Option<&mut Variables> {
640        None
641    }
642    fn set_variables(&mut self, _vars: Option<Variables>) {}
643}
644
645impl DialString for Endpoint {
646    fn variables(&self) -> Option<&Variables> {
647        forward_to_variant!(self, variables)
648    }
649    fn variables_mut(&mut self) -> Option<&mut Variables> {
650        forward_to_variant!(self, variables_mut)
651    }
652    fn set_variables(&mut self, vars: Option<Variables>) {
653        forward_to_variant!(self, set_variables, vars)
654    }
655}
656
657// ---------------------------------------------------------------------------
658// Tests
659// ---------------------------------------------------------------------------
660
661#[cfg(test)]
662mod tests;
663
664#[cfg(all(test, feature = "serde"))]
665mod c_oracle;