1use 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
140mod 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); 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}