Skip to main content

freeswitch_types/event/
subscription.rs

1use super::event_type::EslEventType;
2use super::format::EventFormat;
3use crate::headers::EventHeader;
4use crate::sofia::SofiaEventSubclass;
5use std::fmt;
6
7/// Error returned when an [`EventSubscription`] builder method receives invalid input.
8///
9/// Custom subclasses and filter values are validated against ESL wire-safety
10/// constraints: no newlines, carriage returns, or (for subclasses) spaces.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct EventSubscriptionError(pub String);
13
14impl fmt::Display for EventSubscriptionError {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        write!(f, "invalid event subscription: {}", self.0)
17    }
18}
19
20impl std::error::Error for EventSubscriptionError {}
21
22/// Declarative description of an ESL event subscription.
23///
24/// Captures the event format, event types, custom subclasses, and filters
25/// as a single unit. Useful for config-driven subscriptions and reconnection
26/// patterns where the caller needs to rebuild subscriptions from a saved
27/// description.
28///
29/// # Wire safety
30///
31/// Builder methods validate inputs against ESL wire injection risks.
32/// Custom subclasses reject `\n`, `\r`, spaces, and empty strings.
33/// Filter headers and values reject `\n` and `\r`.
34///
35/// # Example
36///
37/// ```rust
38/// use freeswitch_types::{EventSubscription, EventFormat, EslEventType, EventHeader};
39///
40/// let sub = EventSubscription::new(EventFormat::Plain)
41///     .events(EslEventType::CHANNEL_EVENTS)
42///     .event(EslEventType::Heartbeat)
43///     .custom_subclass("sofia::register").unwrap()
44///     .filter(EventHeader::CallDirection, "inbound").unwrap();
45///
46/// assert!(!sub.is_empty());
47/// assert!(!sub.is_all());
48/// ```
49#[derive(Debug, Clone, PartialEq, Eq)]
50#[non_exhaustive]
51pub struct EventSubscription {
52    format: EventFormat,
53    events: Vec<EslEventType>,
54    raw_events: Vec<String>,
55    custom_subclasses: Vec<String>,
56    filters: Vec<(String, String)>,
57}
58
59/// Wire-safety validator shared by raw events, custom subclasses, and filter fields.
60///
61/// Newlines/CRs are always rejected (would inject extra ESL commands). When
62/// `reject_empty` is set the value must be non-empty. When `reject_space` is
63/// set spaces are rejected (token splitting on the wire).
64fn validate_wire_token(
65    s: &str,
66    label: &str,
67    reject_empty: bool,
68    reject_space: bool,
69) -> Result<(), EventSubscriptionError> {
70    if reject_empty && s.is_empty() {
71        return Err(EventSubscriptionError(format!("{} cannot be empty", label)));
72    }
73    if crate::wire_safety::contains_wire_terminator(s) {
74        return Err(EventSubscriptionError(format!(
75            "{} contains newline: {:?}",
76            label, s
77        )));
78    }
79    if reject_space && s.contains(' ') {
80        return Err(EventSubscriptionError(format!(
81            "{} contains space: {:?}",
82            label, s
83        )));
84    }
85    Ok(())
86}
87
88fn validate_raw_event(s: &str) -> Result<(), EventSubscriptionError> {
89    validate_wire_token(s, "raw event", true, true)
90}
91
92fn validate_custom_subclass(s: &str) -> Result<(), EventSubscriptionError> {
93    validate_wire_token(s, "custom subclass", true, true)
94}
95
96fn validate_filter_field(field: &str, label: &str) -> Result<(), EventSubscriptionError> {
97    validate_wire_token(field, &format!("filter {}", label), false, false)
98}
99
100impl EventSubscription {
101    /// Create an empty subscription with the given format.
102    pub fn new(format: EventFormat) -> Self {
103        Self {
104            format,
105            events: Vec::new(),
106            raw_events: Vec::new(),
107            custom_subclasses: Vec::new(),
108            filters: Vec::new(),
109        }
110    }
111
112    /// Create a subscription for all events.
113    pub fn all(format: EventFormat) -> Self {
114        Self {
115            format,
116            events: vec![EslEventType::All],
117            raw_events: Vec::new(),
118            custom_subclasses: Vec::new(),
119            filters: Vec::new(),
120        }
121    }
122
123    /// Add a single event type.
124    pub fn event(mut self, event: EslEventType) -> Self {
125        self.events
126            .push(event);
127        self
128    }
129
130    /// Add multiple event types (e.g. from group constants like `EslEventType::CHANNEL_EVENTS`).
131    pub fn events<T: IntoIterator<Item = impl std::borrow::Borrow<EslEventType>>>(
132        mut self,
133        events: T,
134    ) -> Self {
135        self.events
136            .extend(
137                events
138                    .into_iter()
139                    .map(|e| *e.borrow()),
140            );
141        self
142    }
143
144    /// Add a single event by wire name.
145    ///
146    /// Escape hatch for events the [`EslEventType`] enum hasn't yet been
147    /// updated to cover. The argument is validated for newline injection,
148    /// spaces, and emptiness.
149    ///
150    /// Raw events appear on the wire alongside typed events when
151    /// [`to_event_string()`](Self::to_event_string) is called.
152    pub fn event_raw(mut self, event: impl Into<String>) -> Result<Self, EventSubscriptionError> {
153        let s = event.into();
154        validate_raw_event(&s)?;
155        self.raw_events
156            .push(s);
157        Ok(self)
158    }
159
160    /// Add multiple events by wire name.
161    ///
162    /// Returns `Err` on the first invalid entry.
163    pub fn events_raw<I, S>(mut self, events: I) -> Result<Self, EventSubscriptionError>
164    where
165        I: IntoIterator<Item = S>,
166        S: Into<String>,
167    {
168        for e in events {
169            let s = e.into();
170            validate_raw_event(&s)?;
171            self.raw_events
172                .push(s);
173        }
174        Ok(self)
175    }
176
177    /// Add a custom subclass (e.g. `"sofia::register"`).
178    ///
179    /// Returns `Err` if the subclass contains spaces, newlines, or is empty.
180    pub fn custom_subclass(
181        mut self,
182        subclass: impl Into<String>,
183    ) -> Result<Self, EventSubscriptionError> {
184        let s = subclass.into();
185        validate_custom_subclass(&s)?;
186        self.custom_subclasses
187            .push(s);
188        Ok(self)
189    }
190
191    /// Add multiple custom subclasses.
192    ///
193    /// Returns `Err` on the first invalid subclass.
194    pub fn custom_subclasses(
195        mut self,
196        subclasses: impl IntoIterator<Item = impl Into<String>>,
197    ) -> Result<Self, EventSubscriptionError> {
198        for s in subclasses {
199            let s = s.into();
200            validate_custom_subclass(&s)?;
201            self.custom_subclasses
202                .push(s);
203        }
204        Ok(self)
205    }
206
207    /// Subscribe to a single Sofia event subclass.
208    ///
209    /// Convenience wrapper around [`custom_subclass()`](Self::custom_subclass) that
210    /// accepts a typed [`SofiaEventSubclass`] instead of a raw string.
211    pub fn sofia_event(mut self, subclass: SofiaEventSubclass) -> Self {
212        self.custom_subclasses
213            .push(
214                subclass
215                    .as_str()
216                    .to_string(),
217            );
218        self
219    }
220
221    /// Subscribe to multiple Sofia event subclasses.
222    pub fn sofia_events(
223        mut self,
224        subclasses: impl IntoIterator<Item = impl std::borrow::Borrow<SofiaEventSubclass>>,
225    ) -> Self {
226        self.custom_subclasses
227            .extend(
228                subclasses
229                    .into_iter()
230                    .map(|s| {
231                        s.borrow()
232                            .as_str()
233                            .to_string()
234                    }),
235            );
236        self
237    }
238
239    /// Add a filter with a typed header.
240    ///
241    /// The header enum is always valid; only the value is validated.
242    pub fn filter(
243        self,
244        header: EventHeader,
245        value: impl Into<String>,
246    ) -> Result<Self, EventSubscriptionError> {
247        let v = value.into();
248        validate_filter_field(&v, "value")?;
249        let mut s = self;
250        s.filters
251            .push((
252                header
253                    .as_str()
254                    .to_string(),
255                v,
256            ));
257        Ok(s)
258    }
259
260    /// Add a filter with raw header and value strings.
261    ///
262    /// Both header and value are validated against newline injection.
263    pub fn filter_raw(
264        self,
265        header: impl Into<String>,
266        value: impl Into<String>,
267    ) -> Result<Self, EventSubscriptionError> {
268        let h = header.into();
269        let v = value.into();
270        validate_filter_field(&h, "header")?;
271        validate_filter_field(&v, "value")?;
272        let mut s = self;
273        s.filters
274            .push((h, v));
275        Ok(s)
276    }
277
278    /// Change the event format.
279    pub fn with_format(mut self, format: EventFormat) -> Self {
280        self.format = format;
281        self
282    }
283
284    /// The event format.
285    pub fn format(&self) -> EventFormat {
286        self.format
287    }
288
289    /// Mutable reference to the event format.
290    pub fn format_mut(&mut self) -> &mut EventFormat {
291        &mut self.format
292    }
293
294    /// The subscribed event types.
295    pub fn event_types(&self) -> &[EslEventType] {
296        &self.events
297    }
298
299    /// Mutable access to the event types list.
300    pub fn event_types_mut(&mut self) -> &mut Vec<EslEventType> {
301        &mut self.events
302    }
303
304    /// Events subscribed by raw wire name (see [`event_raw`](Self::event_raw)).
305    pub fn event_types_raw(&self) -> &[String] {
306        &self.raw_events
307    }
308
309    /// Mutable access to the raw event list.
310    ///
311    /// Direct push to this list bypasses [`event_raw`](Self::event_raw)'s
312    /// validation. Callers are responsible for ensuring entries contain no
313    /// newlines, spaces, or empty strings.
314    pub fn event_types_raw_mut(&mut self) -> &mut Vec<String> {
315        &mut self.raw_events
316    }
317
318    /// The subscribed custom subclasses.
319    pub fn custom_subclass_list(&self) -> &[String] {
320        &self.custom_subclasses
321    }
322
323    /// Mutable access to the custom subclasses list.
324    pub fn custom_subclasses_mut(&mut self) -> &mut Vec<String> {
325        &mut self.custom_subclasses
326    }
327
328    /// The event filters as (header, value) pairs.
329    pub fn filters(&self) -> &[(String, String)] {
330        &self.filters
331    }
332
333    /// Mutable access to the filters list.
334    pub fn filters_mut(&mut self) -> &mut Vec<(String, String)> {
335        &mut self.filters
336    }
337
338    /// Whether the subscription includes all events.
339    pub fn is_all(&self) -> bool {
340        self.events
341            .contains(&EslEventType::All)
342    }
343
344    /// Whether the subscription has no events, no raw events, and no
345    /// custom subclasses.
346    pub fn is_empty(&self) -> bool {
347        self.events
348            .is_empty()
349            && self
350                .raw_events
351                .is_empty()
352            && self
353                .custom_subclasses
354                .is_empty()
355    }
356
357    /// Build the event string for the ESL `event` command.
358    ///
359    /// Returns `None` if no events, raw events, or custom subclasses are
360    /// configured. Returns `Some("ALL")` if `EslEventType::All` is present.
361    /// Otherwise returns space-separated typed event names, then raw event
362    /// names, with custom subclasses appended after a `CUSTOM` token.
363    pub fn to_event_string(&self) -> Option<String> {
364        if self
365            .events
366            .contains(&EslEventType::All)
367        {
368            return Some("ALL".to_string());
369        }
370
371        let mut parts: Vec<&str> = self
372            .events
373            .iter()
374            .map(|e| e.as_str())
375            .collect();
376
377        parts.extend(
378            self.raw_events
379                .iter()
380                .map(|s| s.as_str()),
381        );
382
383        if !self
384            .custom_subclasses
385            .is_empty()
386        {
387            if !self
388                .events
389                .contains(&EslEventType::Custom)
390            {
391                parts.push("CUSTOM");
392            }
393            for sc in &self.custom_subclasses {
394                parts.push(sc.as_str());
395            }
396        }
397
398        if parts.is_empty() {
399            None
400        } else {
401            Some(parts.join(" "))
402        }
403    }
404}
405
406#[cfg(feature = "serde")]
407mod event_subscription_serde {
408    use super::*;
409    use serde::{Deserialize, Serialize};
410
411    #[derive(Serialize, Deserialize)]
412    struct EventSubscriptionRaw {
413        format: EventFormat,
414        #[serde(default)]
415        events: Vec<EslEventType>,
416        #[serde(default, skip_serializing_if = "Vec::is_empty")]
417        raw_events: Vec<String>,
418        #[serde(default)]
419        custom_subclasses: Vec<String>,
420        #[serde(default)]
421        filters: Vec<(String, String)>,
422    }
423
424    impl TryFrom<EventSubscriptionRaw> for EventSubscription {
425        type Error = EventSubscriptionError;
426
427        fn try_from(raw: EventSubscriptionRaw) -> Result<Self, Self::Error> {
428            for re in &raw.raw_events {
429                validate_raw_event(re)?;
430            }
431            for sc in &raw.custom_subclasses {
432                validate_custom_subclass(sc)?;
433            }
434            for (h, v) in &raw.filters {
435                validate_filter_field(h, "header")?;
436                validate_filter_field(v, "value")?;
437            }
438            Ok(EventSubscription {
439                format: raw.format,
440                events: raw.events,
441                raw_events: raw.raw_events,
442                custom_subclasses: raw.custom_subclasses,
443                filters: raw.filters,
444            })
445        }
446    }
447
448    impl Serialize for EventSubscription {
449        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
450            let raw = EventSubscriptionRaw {
451                format: self.format,
452                events: self
453                    .events
454                    .clone(),
455                raw_events: self
456                    .raw_events
457                    .clone(),
458                custom_subclasses: self
459                    .custom_subclasses
460                    .clone(),
461                filters: self
462                    .filters
463                    .clone(),
464            };
465            raw.serialize(serializer)
466        }
467    }
468
469    impl<'de> Deserialize<'de> for EventSubscription {
470        fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
471            let raw = EventSubscriptionRaw::deserialize(deserializer)?;
472            EventSubscription::try_from(raw).map_err(serde::de::Error::custom)
473        }
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn new_creates_empty() {
483        let sub = EventSubscription::new(EventFormat::Plain);
484        assert!(sub.is_empty());
485        assert!(!sub.is_all());
486        assert_eq!(sub.format(), EventFormat::Plain);
487        assert!(sub
488            .event_types()
489            .is_empty());
490        assert!(sub
491            .custom_subclass_list()
492            .is_empty());
493        assert!(sub
494            .filters()
495            .is_empty());
496    }
497
498    #[test]
499    fn all_creates_all() {
500        let sub = EventSubscription::all(EventFormat::Json);
501        assert!(sub.is_all());
502        assert!(!sub.is_empty());
503        assert_eq!(sub.to_event_string(), Some("ALL".to_string()));
504    }
505
506    #[test]
507    fn event_string_typed_only() {
508        let sub = EventSubscription::new(EventFormat::Plain)
509            .event(EslEventType::ChannelCreate)
510            .event(EslEventType::ChannelAnswer);
511        assert_eq!(
512            sub.to_event_string(),
513            Some("CHANNEL_CREATE CHANNEL_ANSWER".to_string())
514        );
515    }
516
517    #[test]
518    fn event_string_custom_only() {
519        let sub = EventSubscription::new(EventFormat::Plain)
520            .custom_subclass("sofia::register")
521            .unwrap()
522            .custom_subclass("sofia::unregister")
523            .unwrap();
524        assert_eq!(
525            sub.to_event_string(),
526            Some("CUSTOM sofia::register sofia::unregister".to_string())
527        );
528    }
529
530    #[test]
531    fn event_string_mixed() {
532        let sub = EventSubscription::new(EventFormat::Plain)
533            .event(EslEventType::Heartbeat)
534            .custom_subclass("sofia::register")
535            .unwrap();
536        assert_eq!(
537            sub.to_event_string(),
538            Some("HEARTBEAT CUSTOM sofia::register".to_string())
539        );
540    }
541
542    #[test]
543    fn event_string_custom_not_duplicated() {
544        let sub = EventSubscription::new(EventFormat::Plain)
545            .event(EslEventType::Custom)
546            .custom_subclass("sofia::register")
547            .unwrap();
548        // Should not have "CUSTOM" twice
549        assert_eq!(
550            sub.to_event_string(),
551            Some("CUSTOM sofia::register".to_string())
552        );
553    }
554
555    #[test]
556    fn event_string_empty_is_none() {
557        let sub = EventSubscription::new(EventFormat::Plain);
558        assert_eq!(sub.to_event_string(), None);
559    }
560
561    #[test]
562    fn filters_preserve_order() {
563        let sub = EventSubscription::new(EventFormat::Plain)
564            .filter(EventHeader::CallDirection, "inbound")
565            .unwrap()
566            .filter_raw("X-Custom", "value1")
567            .unwrap()
568            .filter(EventHeader::ChannelState, "CS_EXECUTE")
569            .unwrap();
570        assert_eq!(
571            sub.filters(),
572            &[
573                ("Call-Direction".to_string(), "inbound".to_string()),
574                ("X-Custom".to_string(), "value1".to_string()),
575                ("Channel-State".to_string(), "CS_EXECUTE".to_string()),
576            ]
577        );
578    }
579
580    #[test]
581    fn builder_chain() {
582        let sub = EventSubscription::new(EventFormat::Plain)
583            .events(EslEventType::CHANNEL_EVENTS)
584            .event(EslEventType::Heartbeat)
585            .custom_subclass("sofia::register")
586            .unwrap()
587            .filter(EventHeader::CallDirection, "inbound")
588            .unwrap()
589            .with_format(EventFormat::Json);
590
591        assert_eq!(sub.format(), EventFormat::Json);
592        assert!(!sub.is_empty());
593        assert!(!sub.is_all());
594        assert!(sub
595            .event_types()
596            .contains(&EslEventType::ChannelCreate));
597        assert!(sub
598            .event_types()
599            .contains(&EslEventType::Heartbeat));
600        assert_eq!(sub.custom_subclass_list(), &["sofia::register"]);
601        assert_eq!(
602            sub.filters()
603                .len(),
604            1
605        );
606    }
607
608    #[test]
609    fn serde_round_trip_subscription() {
610        let sub = EventSubscription::new(EventFormat::Plain)
611            .event(EslEventType::ChannelCreate)
612            .event(EslEventType::Heartbeat)
613            .custom_subclass("sofia::register")
614            .unwrap()
615            .filter(EventHeader::CallDirection, "inbound")
616            .unwrap();
617
618        let json = serde_json::to_string(&sub).unwrap();
619        let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
620        assert_eq!(sub, deserialized);
621    }
622
623    #[test]
624    fn serde_rejects_invalid_subclass() {
625        let json =
626            r#"{"format":"Plain","events":[],"custom_subclasses":["bad subclass"],"filters":[]}"#;
627        let result: Result<EventSubscription, _> = serde_json::from_str(json);
628        assert!(result.is_err());
629        let err = result
630            .unwrap_err()
631            .to_string();
632        assert!(err.contains("space"), "error should mention space: {err}");
633    }
634
635    #[test]
636    fn serde_rejects_newline_in_filter() {
637        let json = r#"{"format":"Plain","events":[],"custom_subclasses":[],"filters":[["Header","val\n"]]}"#;
638        let result: Result<EventSubscription, _> = serde_json::from_str(json);
639        assert!(result.is_err());
640        let err = result
641            .unwrap_err()
642            .to_string();
643        assert!(
644            err.contains("newline"),
645            "error should mention newline: {err}"
646        );
647    }
648
649    #[test]
650    fn custom_subclass_rejects_space() {
651        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad subclass");
652        assert!(result.is_err());
653    }
654
655    #[test]
656    fn custom_subclass_rejects_newline() {
657        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad\nsubclass");
658        assert!(result.is_err());
659    }
660
661    #[test]
662    fn custom_subclass_rejects_empty() {
663        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("");
664        assert!(result.is_err());
665    }
666
667    #[test]
668    fn filter_raw_rejects_newline_in_header() {
669        let result = EventSubscription::new(EventFormat::Plain).filter_raw("Bad\nHeader", "value");
670        assert!(result.is_err());
671    }
672
673    #[test]
674    fn filter_raw_rejects_newline_in_value() {
675        let result = EventSubscription::new(EventFormat::Plain).filter_raw("Header", "bad\nvalue");
676        assert!(result.is_err());
677    }
678
679    #[test]
680    fn filter_typed_rejects_newline_in_value() {
681        let result = EventSubscription::new(EventFormat::Plain)
682            .filter(EventHeader::CallDirection, "bad\nvalue");
683        assert!(result.is_err());
684    }
685
686    #[test]
687    fn sofia_event_single() {
688        let sub =
689            EventSubscription::new(EventFormat::Plain).sofia_event(SofiaEventSubclass::Register);
690        assert_eq!(
691            sub.to_event_string(),
692            Some("CUSTOM sofia::register".to_string())
693        );
694    }
695
696    #[test]
697    fn sofia_events_group() {
698        let sub = EventSubscription::new(EventFormat::Plain)
699            .sofia_events(SofiaEventSubclass::GATEWAY_EVENTS);
700        let event_str = sub
701            .to_event_string()
702            .unwrap();
703        assert!(event_str.starts_with("CUSTOM"));
704        assert!(event_str.contains("sofia::gateway_state"));
705        assert!(event_str.contains("sofia::gateway_add"));
706        assert!(event_str.contains("sofia::gateway_delete"));
707        assert!(event_str.contains("sofia::gateway_invalid_digest_req"));
708    }
709
710    #[test]
711    fn event_raw_wire_string() {
712        let sub = EventSubscription::new(EventFormat::Plain)
713            .event(EslEventType::Heartbeat)
714            .event_raw("NEW_EVENT_NOT_IN_ENUM")
715            .unwrap();
716        assert_eq!(
717            sub.to_event_string(),
718            Some("HEARTBEAT NEW_EVENT_NOT_IN_ENUM".to_string())
719        );
720    }
721
722    #[test]
723    fn events_raw_wire_string() {
724        let sub = EventSubscription::new(EventFormat::Plain)
725            .events_raw(["FUTURE_A", "FUTURE_B"])
726            .unwrap();
727        assert_eq!(sub.to_event_string(), Some("FUTURE_A FUTURE_B".to_string()));
728    }
729
730    #[test]
731    fn event_raw_with_custom_subclass() {
732        let sub = EventSubscription::new(EventFormat::Plain)
733            .event_raw("NEW_EVENT")
734            .unwrap()
735            .custom_subclass("sofia::register")
736            .unwrap();
737        assert_eq!(
738            sub.to_event_string(),
739            Some("NEW_EVENT CUSTOM sofia::register".to_string())
740        );
741    }
742
743    #[test]
744    fn event_raw_rejects_newline() {
745        assert!(EventSubscription::new(EventFormat::Plain)
746            .event_raw("bad\nevent")
747            .is_err());
748    }
749
750    #[test]
751    fn event_raw_rejects_space() {
752        assert!(EventSubscription::new(EventFormat::Plain)
753            .event_raw("bad event")
754            .is_err());
755    }
756
757    #[test]
758    fn event_raw_rejects_empty() {
759        assert!(EventSubscription::new(EventFormat::Plain)
760            .event_raw("")
761            .is_err());
762    }
763
764    #[test]
765    fn events_raw_errors_on_first_invalid() {
766        let result =
767            EventSubscription::new(EventFormat::Plain).events_raw(["GOOD", "bad event", "OTHER"]);
768        assert!(result.is_err());
769    }
770
771    #[test]
772    fn event_types_raw_mut_mutable() {
773        let mut sub = EventSubscription::new(EventFormat::Plain);
774        sub.event_types_raw_mut()
775            .push("DIRECT_PUSH".to_string());
776        assert_eq!(sub.event_types_raw(), &["DIRECT_PUSH".to_string()]);
777    }
778
779    #[test]
780    fn is_empty_sees_raw_events() {
781        let sub = EventSubscription::new(EventFormat::Plain)
782            .event_raw("ONLY_RAW")
783            .unwrap();
784        assert!(!sub.is_empty());
785    }
786
787    #[test]
788    fn serde_round_trip_with_raw_events() {
789        let sub = EventSubscription::new(EventFormat::Plain)
790            .event(EslEventType::ChannelCreate)
791            .event_raw("FUTURE_EVENT")
792            .unwrap()
793            .custom_subclass("sofia::register")
794            .unwrap();
795
796        let json = serde_json::to_string(&sub).unwrap();
797        let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
798        assert_eq!(sub, deserialized);
799    }
800
801    #[test]
802    fn serde_rejects_invalid_raw_event() {
803        let json = r#"{"format":"Plain","events":[],"raw_events":["bad event"],"custom_subclasses":[],"filters":[]}"#;
804        let result: Result<EventSubscription, _> = serde_json::from_str(json);
805        assert!(result.is_err());
806    }
807
808    #[test]
809    fn serde_missing_raw_events_field_defaults_to_empty() {
810        // Back-compat: configs written before raw_events was added must still
811        // deserialize.
812        let json =
813            r#"{"format":"Plain","events":["Heartbeat"],"custom_subclasses":[],"filters":[]}"#;
814        let sub: EventSubscription = serde_json::from_str(json).unwrap();
815        assert!(sub
816            .event_types_raw()
817            .is_empty());
818    }
819
820    #[test]
821    fn sofia_event_mixed_with_typed_events() {
822        let sub = EventSubscription::new(EventFormat::Plain)
823            .event(EslEventType::Heartbeat)
824            .sofia_event(SofiaEventSubclass::GatewayState);
825        assert_eq!(
826            sub.to_event_string(),
827            Some("HEARTBEAT CUSTOM sofia::gateway_state".to_string())
828        );
829    }
830}