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