tauri_plugin_notifications/
models.rs

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