tauri_plugin_notifications/
models.rs

1use std::{collections::HashMap, fmt::Display};
2
3use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
4use tauri::plugin::PermissionState;
5
6use url::Url;
7
8#[derive(Debug, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct PermissionResponse {
11    pub permission_state: PermissionState,
12}
13
14#[cfg(feature = "push-notifications")]
15#[derive(Debug, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct PushNotificationResponse {
18    pub device_token: String,
19}
20
21#[derive(Debug, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct Attachment {
24    id: String,
25    url: Url,
26}
27
28impl Attachment {
29    pub fn new(id: impl Into<String>, url: Url) -> Self {
30        Self { id: id.into(), url }
31    }
32}
33
34#[derive(Debug, Default, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct ScheduleInterval {
37    pub year: Option<u8>,
38    pub month: Option<u8>,
39    pub day: Option<u8>,
40    pub weekday: Option<u8>,
41    pub hour: Option<u8>,
42    pub minute: Option<u8>,
43    pub second: Option<u8>,
44}
45
46#[derive(Debug)]
47pub enum ScheduleEvery {
48    Year,
49    Month,
50    TwoWeeks,
51    Week,
52    Day,
53    Hour,
54    Minute,
55    Second,
56}
57
58impl Display for ScheduleEvery {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        write!(
61            f,
62            "{}",
63            match self {
64                Self::Year => "year",
65                Self::Month => "month",
66                Self::TwoWeeks => "twoWeeks",
67                Self::Week => "week",
68                Self::Day => "day",
69                Self::Hour => "hour",
70                Self::Minute => "minute",
71                Self::Second => "second",
72            }
73        )
74    }
75}
76
77impl Serialize for ScheduleEvery {
78    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
79    where
80        S: Serializer,
81    {
82        serializer.serialize_str(self.to_string().as_ref())
83    }
84}
85
86impl<'de> Deserialize<'de> for ScheduleEvery {
87    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
88    where
89        D: Deserializer<'de>,
90    {
91        let s = String::deserialize(deserializer)?;
92        match s.to_lowercase().as_str() {
93            "year" => Ok(Self::Year),
94            "month" => Ok(Self::Month),
95            "twoweeks" => Ok(Self::TwoWeeks),
96            "week" => Ok(Self::Week),
97            "day" => Ok(Self::Day),
98            "hour" => Ok(Self::Hour),
99            "minute" => Ok(Self::Minute),
100            "second" => Ok(Self::Second),
101            _ => Err(DeError::custom(format!("unknown every kind '{s}'"))),
102        }
103    }
104}
105
106#[derive(Debug, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub enum Schedule {
109    #[serde(rename_all = "camelCase")]
110    At {
111        #[serde(
112            serialize_with = "iso8601::serialize",
113            deserialize_with = "time::serde::iso8601::deserialize"
114        )]
115        date: time::OffsetDateTime,
116        #[serde(default)]
117        repeating: bool,
118        #[serde(default)]
119        allow_while_idle: bool,
120    },
121    #[serde(rename_all = "camelCase")]
122    Interval {
123        interval: ScheduleInterval,
124        #[serde(default)]
125        allow_while_idle: bool,
126    },
127    #[serde(rename_all = "camelCase")]
128    Every {
129        interval: ScheduleEvery,
130        count: u8,
131        #[serde(default)]
132        allow_while_idle: bool,
133    },
134}
135
136// custom ISO-8601 serialization that does not use 6 digits for years.
137mod iso8601 {
138    use serde::{ser::Error as _, Serialize, Serializer};
139    use time::{
140        format_description::well_known::iso8601::{Config, EncodedConfig},
141        format_description::well_known::Iso8601,
142        OffsetDateTime,
143    };
144
145    const SERDE_CONFIG: EncodedConfig = Config::DEFAULT.encode();
146
147    pub fn serialize<S: Serializer>(
148        datetime: &OffsetDateTime,
149        serializer: S,
150    ) -> Result<S::Ok, S::Error> {
151        datetime
152            .format(&Iso8601::<SERDE_CONFIG>)
153            .map_err(S::Error::custom)?
154            .serialize(serializer)
155    }
156}
157
158#[derive(Debug, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct NotificationData {
161    #[serde(default = "default_id")]
162    pub(crate) id: i32,
163    pub(crate) channel_id: Option<String>,
164    pub(crate) title: Option<String>,
165    pub(crate) body: Option<String>,
166    pub(crate) schedule: Option<Schedule>,
167    pub(crate) large_body: Option<String>,
168    pub(crate) summary: Option<String>,
169    pub(crate) action_type_id: Option<String>,
170    pub(crate) group: Option<String>,
171    #[serde(default)]
172    pub(crate) group_summary: bool,
173    pub(crate) sound: Option<String>,
174    #[serde(default)]
175    pub(crate) inbox_lines: Vec<String>,
176    pub(crate) icon: Option<String>,
177    pub(crate) large_icon: Option<String>,
178    pub(crate) icon_color: Option<String>,
179    #[serde(default)]
180    pub(crate) attachments: Vec<Attachment>,
181    #[serde(default)]
182    pub(crate) extra: HashMap<String, serde_json::Value>,
183    #[serde(default)]
184    pub(crate) ongoing: bool,
185    #[serde(default)]
186    pub(crate) auto_cancel: bool,
187    #[serde(default)]
188    pub(crate) silent: bool,
189}
190
191fn default_id() -> i32 {
192    rand::random()
193}
194
195impl Default for NotificationData {
196    fn default() -> Self {
197        Self {
198            id: default_id(),
199            channel_id: None,
200            title: None,
201            body: None,
202            schedule: None,
203            large_body: None,
204            summary: None,
205            action_type_id: None,
206            group: None,
207            group_summary: false,
208            sound: None,
209            inbox_lines: Vec::new(),
210            icon: None,
211            large_icon: None,
212            icon_color: None,
213            attachments: Vec::new(),
214            extra: Default::default(),
215            ongoing: false,
216            auto_cancel: false,
217            silent: false,
218        }
219    }
220}
221
222#[derive(Debug, Deserialize, Serialize)]
223#[serde(rename_all = "camelCase")]
224pub struct PendingNotification {
225    id: i32,
226    title: Option<String>,
227    body: Option<String>,
228    schedule: Schedule,
229}
230
231impl PendingNotification {
232    pub fn id(&self) -> i32 {
233        self.id
234    }
235
236    pub fn title(&self) -> Option<&str> {
237        self.title.as_deref()
238    }
239
240    pub fn body(&self) -> Option<&str> {
241        self.body.as_deref()
242    }
243
244    pub fn schedule(&self) -> &Schedule {
245        &self.schedule
246    }
247}
248
249#[derive(Debug, Deserialize, Serialize)]
250#[serde(rename_all = "camelCase")]
251pub struct ActiveNotification {
252    id: i32,
253    tag: Option<String>,
254    title: Option<String>,
255    body: Option<String>,
256    group: Option<String>,
257    #[serde(default)]
258    group_summary: bool,
259    #[serde(default)]
260    data: HashMap<String, String>,
261    #[serde(default)]
262    extra: HashMap<String, serde_json::Value>,
263    #[serde(default)]
264    attachments: Vec<Attachment>,
265    action_type_id: Option<String>,
266    schedule: Option<Schedule>,
267    sound: Option<String>,
268}
269
270impl ActiveNotification {
271    pub fn id(&self) -> i32 {
272        self.id
273    }
274
275    pub fn tag(&self) -> Option<&str> {
276        self.tag.as_deref()
277    }
278
279    pub fn title(&self) -> Option<&str> {
280        self.title.as_deref()
281    }
282
283    pub fn body(&self) -> Option<&str> {
284        self.body.as_deref()
285    }
286
287    pub fn group(&self) -> Option<&str> {
288        self.group.as_deref()
289    }
290
291    pub fn group_summary(&self) -> bool {
292        self.group_summary
293    }
294
295    pub fn data(&self) -> &HashMap<String, String> {
296        &self.data
297    }
298
299    pub fn extra(&self) -> &HashMap<String, serde_json::Value> {
300        &self.extra
301    }
302
303    pub fn attachments(&self) -> &[Attachment] {
304        &self.attachments
305    }
306
307    pub fn action_type_id(&self) -> Option<&str> {
308        self.action_type_id.as_deref()
309    }
310
311    pub fn schedule(&self) -> Option<&Schedule> {
312        self.schedule.as_ref()
313    }
314
315    pub fn sound(&self) -> Option<&str> {
316        self.sound.as_deref()
317    }
318}
319
320#[derive(Debug, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct ActionType {
323    id: String,
324    actions: Vec<Action>,
325    hidden_previews_body_placeholder: Option<String>,
326    #[serde(default)]
327    custom_dismiss_action: bool,
328    #[serde(default)]
329    allow_in_car_play: bool,
330    #[serde(default)]
331    hidden_previews_show_title: bool,
332    #[serde(default)]
333    hidden_previews_show_subtitle: bool,
334}
335
336#[derive(Debug, Serialize, Deserialize)]
337#[serde(rename_all = "camelCase")]
338pub struct Action {
339    id: String,
340    title: String,
341    #[serde(default)]
342    requires_authentication: bool,
343    #[serde(default)]
344    foreground: bool,
345    #[serde(default)]
346    destructive: bool,
347    #[serde(default)]
348    input: bool,
349    input_button_title: Option<String>,
350    input_placeholder: Option<String>,
351}
352
353pub use android::*;
354
355mod android {
356    use serde::{Deserialize, Serialize};
357    use serde_repr::{Deserialize_repr, Serialize_repr};
358
359    #[derive(Debug, Default, Clone, Copy, Serialize_repr, Deserialize_repr)]
360    #[repr(u8)]
361    pub enum Importance {
362        None = 0,
363        Min = 1,
364        Low = 2,
365        #[default]
366        Default = 3,
367        High = 4,
368    }
369
370    #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)]
371    #[repr(i8)]
372    pub enum Visibility {
373        Secret = -1,
374        Private = 0,
375        Public = 1,
376    }
377
378    #[derive(Debug, Serialize, Deserialize)]
379    #[serde(rename_all = "camelCase")]
380    pub struct Channel {
381        id: String,
382        name: String,
383        description: Option<String>,
384        sound: Option<String>,
385        lights: Option<bool>,
386        light_color: Option<String>,
387        vibration: Option<bool>,
388        importance: Option<Importance>,
389        visibility: Option<Visibility>,
390    }
391
392    #[derive(Debug)]
393    pub struct ChannelBuilder(Channel);
394
395    impl Channel {
396        pub fn builder(id: impl Into<String>, name: impl Into<String>) -> ChannelBuilder {
397            ChannelBuilder(Self {
398                id: id.into(),
399                name: name.into(),
400                description: None,
401                sound: None,
402                lights: Some(false),
403                light_color: None,
404                vibration: Some(false),
405                importance: Default::default(),
406                visibility: None,
407            })
408        }
409
410        pub fn id(&self) -> &str {
411            &self.id
412        }
413
414        pub fn name(&self) -> &str {
415            &self.name
416        }
417
418        pub fn description(&self) -> Option<&str> {
419            self.description.as_deref()
420        }
421
422        pub fn sound(&self) -> Option<&str> {
423            self.sound.as_deref()
424        }
425
426        pub fn lights(&self) -> bool {
427            self.lights.unwrap_or(false)
428        }
429
430        pub fn light_color(&self) -> Option<&str> {
431            self.light_color.as_deref()
432        }
433
434        pub fn vibration(&self) -> bool {
435            self.vibration.unwrap_or(false)
436        }
437
438        pub fn importance(&self) -> Importance {
439            self.importance.unwrap_or_default()
440        }
441
442        pub fn visibility(&self) -> Option<Visibility> {
443            self.visibility
444        }
445    }
446
447    impl ChannelBuilder {
448        pub fn description(mut self, description: impl Into<String>) -> Self {
449            self.0.description.replace(description.into());
450            self
451        }
452
453        pub fn sound(mut self, sound: impl Into<String>) -> Self {
454            self.0.sound.replace(sound.into());
455            self
456        }
457
458        pub fn lights(mut self, lights: bool) -> Self {
459            self.0.lights = Some(lights);
460            self
461        }
462
463        pub fn light_color(mut self, color: impl Into<String>) -> Self {
464            self.0.light_color.replace(color.into());
465            self
466        }
467
468        pub fn vibration(mut self, vibration: bool) -> Self {
469            self.0.vibration = Some(vibration);
470            self
471        }
472
473        pub fn importance(mut self, importance: Importance) -> Self {
474            self.0.importance = Some(importance);
475            self
476        }
477
478        pub fn visibility(mut self, visibility: Visibility) -> Self {
479            self.0.visibility.replace(visibility);
480            self
481        }
482
483        pub fn build(self) -> Channel {
484            self.0
485        }
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492
493    #[test]
494    fn test_attachment_creation() {
495        let url = Url::parse("https://example.com/image.png").expect("Failed to parse URL");
496        let attachment = Attachment::new("test_id", url.clone());
497        assert_eq!(attachment.id, "test_id");
498        assert_eq!(attachment.url, url);
499    }
500
501    #[test]
502    fn test_attachment_serialization() {
503        let url = Url::parse("https://example.com/image.png").expect("Failed to parse URL");
504        let attachment = Attachment::new("test_id", url);
505        let json = serde_json::to_string(&attachment).expect("Failed to serialize attachment");
506        assert!(json.contains("test_id"));
507        assert!(json.contains("https://example.com/image.png"));
508    }
509
510    #[test]
511    fn test_attachment_deserialization() {
512        let json = r#"{"id":"test_id","url":"https://example.com/image.png"}"#;
513        let attachment: Attachment =
514            serde_json::from_str(json).expect("Failed to deserialize attachment");
515        assert_eq!(attachment.id, "test_id");
516        assert_eq!(attachment.url.as_str(), "https://example.com/image.png");
517    }
518
519    #[test]
520    fn test_schedule_every_display() {
521        assert_eq!(ScheduleEvery::Year.to_string(), "year");
522        assert_eq!(ScheduleEvery::Month.to_string(), "month");
523        assert_eq!(ScheduleEvery::TwoWeeks.to_string(), "twoWeeks");
524        assert_eq!(ScheduleEvery::Week.to_string(), "week");
525        assert_eq!(ScheduleEvery::Day.to_string(), "day");
526        assert_eq!(ScheduleEvery::Hour.to_string(), "hour");
527        assert_eq!(ScheduleEvery::Minute.to_string(), "minute");
528        assert_eq!(ScheduleEvery::Second.to_string(), "second");
529    }
530
531    #[test]
532    fn test_schedule_every_serialization() {
533        let json = serde_json::to_string(&ScheduleEvery::Day).expect("Failed to serialize Day");
534        assert_eq!(json, "\"day\"");
535
536        let json =
537            serde_json::to_string(&ScheduleEvery::TwoWeeks).expect("Failed to serialize TwoWeeks");
538        assert_eq!(json, "\"twoWeeks\"");
539    }
540
541    #[test]
542    fn test_schedule_every_deserialization() {
543        let every: ScheduleEvery =
544            serde_json::from_str("\"year\"").expect("Failed to deserialize year");
545        assert!(matches!(every, ScheduleEvery::Year));
546
547        let every: ScheduleEvery =
548            serde_json::from_str("\"month\"").expect("Failed to deserialize month");
549        assert!(matches!(every, ScheduleEvery::Month));
550
551        let every: ScheduleEvery =
552            serde_json::from_str("\"twoweeks\"").expect("Failed to deserialize twoweeks");
553        assert!(matches!(every, ScheduleEvery::TwoWeeks));
554
555        let every: ScheduleEvery =
556            serde_json::from_str("\"week\"").expect("Failed to deserialize week");
557        assert!(matches!(every, ScheduleEvery::Week));
558
559        let every: ScheduleEvery =
560            serde_json::from_str("\"day\"").expect("Failed to deserialize day");
561        assert!(matches!(every, ScheduleEvery::Day));
562
563        let every: ScheduleEvery =
564            serde_json::from_str("\"hour\"").expect("Failed to deserialize hour");
565        assert!(matches!(every, ScheduleEvery::Hour));
566
567        let every: ScheduleEvery =
568            serde_json::from_str("\"minute\"").expect("Failed to deserialize minute");
569        assert!(matches!(every, ScheduleEvery::Minute));
570
571        let every: ScheduleEvery =
572            serde_json::from_str("\"second\"").expect("Failed to deserialize second");
573        assert!(matches!(every, ScheduleEvery::Second));
574    }
575
576    #[test]
577    fn test_schedule_every_deserialization_invalid() {
578        let result: Result<ScheduleEvery, _> = serde_json::from_str("\"invalid\"");
579        assert!(result.is_err());
580    }
581
582    #[test]
583    fn test_schedule_interval_default() {
584        let interval = ScheduleInterval::default();
585        assert!(interval.year.is_none());
586        assert!(interval.month.is_none());
587        assert!(interval.day.is_none());
588        assert!(interval.weekday.is_none());
589        assert!(interval.hour.is_none());
590        assert!(interval.minute.is_none());
591        assert!(interval.second.is_none());
592    }
593
594    #[test]
595    fn test_schedule_interval_serialization() {
596        let interval = ScheduleInterval {
597            year: Some(24),
598            month: Some(12),
599            day: Some(25),
600            weekday: Some(1),
601            hour: Some(10),
602            minute: Some(30),
603            second: Some(0),
604        };
605        let json = serde_json::to_string(&interval).expect("Failed to serialize interval");
606        assert!(json.contains("\"year\":24"));
607        assert!(json.contains("\"month\":12"));
608        assert!(json.contains("\"day\":25"));
609    }
610
611    #[test]
612    fn test_notification_data_default() {
613        let data = NotificationData::default();
614        assert!(data.id != 0); // Should be a random ID
615        assert!(data.channel_id.is_none());
616        assert!(data.title.is_none());
617        assert!(data.body.is_none());
618        assert!(data.schedule.is_none());
619        assert!(!data.group_summary);
620        assert!(!data.ongoing);
621        assert!(!data.auto_cancel);
622        assert!(!data.silent);
623        assert!(data.inbox_lines.is_empty());
624        assert!(data.attachments.is_empty());
625        assert!(data.extra.is_empty());
626    }
627
628    #[test]
629    fn test_notification_data_serialization() {
630        let data = NotificationData {
631            id: 123,
632            title: Some("Test Title".to_string()),
633            body: Some("Test Body".to_string()),
634            ongoing: true,
635            ..Default::default()
636        };
637
638        let json = serde_json::to_string(&data).expect("Failed to serialize notification data");
639        assert!(json.contains("\"id\":123"));
640        assert!(json.contains("\"title\":\"Test Title\""));
641        assert!(json.contains("\"body\":\"Test Body\""));
642        assert!(json.contains("\"ongoing\":true"));
643    }
644
645    #[test]
646    fn test_pending_notification_getters() {
647        let json = r#"{
648            "id": 456,
649            "title": "Pending Title",
650            "body": "Pending Body",
651            "schedule": {"every": {"interval": "day", "count": 1}}
652        }"#;
653        let pending: PendingNotification =
654            serde_json::from_str(json).expect("Failed to deserialize pending notification");
655
656        assert_eq!(pending.id(), 456);
657        assert_eq!(pending.title(), Some("Pending Title"));
658        assert_eq!(pending.body(), Some("Pending Body"));
659        assert!(matches!(pending.schedule(), Schedule::Every { .. }));
660    }
661
662    #[test]
663    fn test_active_notification_getters() {
664        let json = r#"{
665            "id": 789,
666            "title": "Active Title",
667            "body": "Active Body",
668            "group": "test_group",
669            "groupSummary": true
670        }"#;
671        let active: ActiveNotification =
672            serde_json::from_str(json).expect("Failed to deserialize active notification");
673
674        assert_eq!(active.id(), 789);
675        assert_eq!(active.title(), Some("Active Title"));
676        assert_eq!(active.body(), Some("Active Body"));
677        assert_eq!(active.group(), Some("test_group"));
678        assert!(active.group_summary());
679        assert!(active.data().is_empty());
680        assert!(active.extra().is_empty());
681        assert!(active.attachments().is_empty());
682        assert!(active.action_type_id().is_none());
683        assert!(active.schedule().is_none());
684        assert!(active.sound().is_none());
685    }
686
687    #[cfg(target_os = "android")]
688    #[test]
689    fn test_importance_default() {
690        let importance = Importance::default();
691        assert!(matches!(importance, Importance::Default));
692    }
693
694    #[cfg(target_os = "android")]
695    #[test]
696    fn test_importance_serialization() {
697        assert_eq!(
698            serde_json::to_string(&Importance::None).expect("Failed to serialize Importance::None"),
699            "0"
700        );
701        assert_eq!(
702            serde_json::to_string(&Importance::Min).expect("Failed to serialize Importance::Min"),
703            "1"
704        );
705        assert_eq!(
706            serde_json::to_string(&Importance::Low).expect("Failed to serialize Importance::Low"),
707            "2"
708        );
709        assert_eq!(
710            serde_json::to_string(&Importance::Default)
711                .expect("Failed to serialize Importance::Default"),
712            "3"
713        );
714        assert_eq!(
715            serde_json::to_string(&Importance::High).expect("Failed to serialize Importance::High"),
716            "4"
717        );
718    }
719
720    #[cfg(target_os = "android")]
721    #[test]
722    fn test_visibility_serialization() {
723        assert_eq!(
724            serde_json::to_string(&Visibility::Secret)
725                .expect("Failed to serialize Visibility::Secret"),
726            "-1"
727        );
728        assert_eq!(
729            serde_json::to_string(&Visibility::Private)
730                .expect("Failed to serialize Visibility::Private"),
731            "0"
732        );
733        assert_eq!(
734            serde_json::to_string(&Visibility::Public)
735                .expect("Failed to serialize Visibility::Public"),
736            "1"
737        );
738    }
739
740    #[cfg(target_os = "android")]
741    #[test]
742    fn test_channel_builder() {
743        let channel = Channel::builder("test_id", "Test Channel")
744            .description("Test Description")
745            .sound("test_sound")
746            .lights(true)
747            .light_color("#FF0000")
748            .vibration(true)
749            .importance(Importance::High)
750            .visibility(Visibility::Public)
751            .build();
752
753        assert_eq!(channel.id(), "test_id");
754        assert_eq!(channel.name(), "Test Channel");
755        assert_eq!(channel.description(), Some("Test Description"));
756        assert_eq!(channel.sound(), Some("test_sound"));
757        assert!(channel.lights());
758        assert_eq!(channel.light_color(), Some("#FF0000"));
759        assert!(channel.vibration());
760        assert!(matches!(channel.importance(), Importance::High));
761        assert_eq!(channel.visibility(), Some(Visibility::Public));
762    }
763
764    #[cfg(target_os = "android")]
765    #[test]
766    fn test_channel_builder_minimal() {
767        let channel = Channel::builder("minimal_id", "Minimal Channel").build();
768
769        assert_eq!(channel.id(), "minimal_id");
770        assert_eq!(channel.name(), "Minimal Channel");
771        assert_eq!(channel.description(), None);
772        assert_eq!(channel.sound(), None);
773        assert!(!channel.lights());
774        assert_eq!(channel.light_color(), None);
775        assert!(!channel.vibration());
776        assert!(matches!(channel.importance(), Importance::Default));
777        assert_eq!(channel.visibility(), None);
778    }
779
780    #[test]
781    fn test_schedule_at_serialization() {
782        use time::OffsetDateTime;
783
784        let date = OffsetDateTime::now_utc();
785        let schedule = Schedule::At {
786            date,
787            repeating: true,
788            allow_while_idle: false,
789        };
790
791        let json = serde_json::to_string(&schedule).expect("Failed to serialize Schedule::At");
792        assert!(json.contains("\"at\""));
793        assert!(json.contains("\"date\""));
794        assert!(json.contains("\"repeating\":true"));
795        assert!(json.contains("\"allowWhileIdle\":false"));
796    }
797
798    #[test]
799    fn test_schedule_interval_variant() {
800        let schedule = Schedule::Interval {
801            interval: ScheduleInterval {
802                hour: Some(10),
803                minute: Some(30),
804                ..Default::default()
805            },
806            allow_while_idle: true,
807        };
808
809        let json =
810            serde_json::to_string(&schedule).expect("Failed to serialize Schedule::Interval");
811        assert!(json.contains("\"interval\""));
812        assert!(json.contains("\"hour\":10"));
813        assert!(json.contains("\"minute\":30"));
814        assert!(json.contains("\"allowWhileIdle\":true"));
815    }
816
817    #[test]
818    fn test_schedule_every_variant() {
819        let schedule = Schedule::Every {
820            interval: ScheduleEvery::Day,
821            count: 5,
822            allow_while_idle: false,
823        };
824
825        let json = serde_json::to_string(&schedule).expect("Failed to serialize Schedule::Every");
826        assert!(json.contains("\"every\""));
827        assert!(json.contains("\"interval\":\"day\""));
828        assert!(json.contains("\"count\":5"));
829    }
830}