1use serde::{
2 de::{self, MapAccess, Visitor},
3 ser::{SerializeMap, SerializeStruct},
4 Deserialize, Serialize,
5};
6use serde_plain::{derive_display_from_serialize, derive_fromstr_from_deserialize};
7use serde_with::{serde_as, skip_serializing_none, BoolFromInt};
8
9fn is_false(v: &bool) -> bool {
10 !v
11}
12
13#[serde_as]
18#[skip_serializing_none]
19#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
20#[serde(rename_all = "kebab-case")]
21pub struct Payload<T = ()>
22where
23 T: Serialize,
24{
25 pub aps: Aps,
27
28 #[serde(flatten)]
30 pub user_info: Option<T>,
31}
32
33#[serde_as]
35#[skip_serializing_none]
36#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
37#[serde(rename_all = "kebab-case")]
38pub struct Aps {
39 pub alert: Option<Alert>,
41
42 pub badge: Option<u32>,
45
46 pub sound: Option<Sound>,
50
51 pub thread_id: Option<String>,
56
57 pub category: Option<String>,
63
64 #[serde(default, skip_serializing_if = "is_false")]
69 #[serde_as(as = "BoolFromInt")]
70 pub content_available: bool,
71
72 #[serde(default, skip_serializing_if = "is_false")]
78 #[serde_as(as = "BoolFromInt")]
79 pub mutable_content: bool,
80
81 pub target_content_id: Option<String>,
90
91 pub interruption_level: Option<InterruptionLevel>,
96
97 pub relevance_score: Option<f64>,
102}
103
104#[derive(Clone, Debug, Default, PartialEq, Eq)]
106pub struct Alert {
107 pub title: Option<String>,
111
112 pub subtitle: Option<String>,
115
116 pub body: Option<String>,
118
119 pub launch_image: Option<String>,
123
124 pub title_loc_key: Option<String>,
129
130 pub title_loc_args: Option<Vec<String>>,
137
138 pub subtitle_loc_key: Option<String>,
143
144 pub subtitle_loc_args: Option<Vec<String>>,
150
151 pub loc_key: Option<String>,
156
157 pub loc_args: Option<Vec<String>>,
163}
164
165impl From<String> for Alert {
166 fn from(value: String) -> Self {
167 Alert {
168 body: Some(value),
169 ..Default::default()
170 }
171 }
172}
173
174impl<'a> From<&'a str> for Alert {
175 fn from(value: &'a str) -> Self {
176 Alert {
177 body: Some(value.to_string()),
178 ..Default::default()
179 }
180 }
181}
182
183impl<'de> Deserialize<'de> for Alert {
184 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185 where
186 D: serde::Deserializer<'de>,
187 {
188 struct AlertVisitor;
189
190 impl<'de> Visitor<'de> for AlertVisitor {
191 type Value = Alert;
192
193 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
194 write!(formatter, "an alert struct")
195 }
196
197 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
198 where
199 E: de::Error,
200 {
201 Ok(Alert {
202 body: Some(v.into()),
203 ..Default::default()
204 })
205 }
206
207 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
208 where
209 E: de::Error,
210 {
211 Ok(Alert {
212 body: Some(v),
213 ..Default::default()
214 })
215 }
216
217 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
218 where
219 A: MapAccess<'de>,
220 {
221 let mut alert = Alert::default();
222
223 while let Some(key) = map.next_key::<&str>()? {
224 match key {
225 "title" => alert.title = map.next_value()?,
226 "title-loc-key" => alert.title_loc_key = map.next_value()?,
227 "title-loc-args" => alert.title_loc_args = map.next_value()?,
228 "subtitle" => alert.subtitle = map.next_value()?,
229 "subtitle-loc-key" => alert.subtitle_loc_key = map.next_value()?,
230 "subtitle-loc-args" => alert.subtitle_loc_args = map.next_value()?,
231 "body" => alert.body = map.next_value()?,
232 "loc-key" => alert.loc_key = map.next_value()?,
233 "loc-args" => alert.loc_args = map.next_value()?,
234 "launch-image" => alert.launch_image = map.next_value()?,
235 field => {
236 return Err(de::Error::unknown_field(
237 field,
238 &[
239 "title",
240 "title-loc-key",
241 "title-loc-args",
242 "subtitle",
243 "subtitle-loc-key",
244 "subtitle-loc-args",
245 "body",
246 "loc-key",
247 "loc-args",
248 "launch-image",
249 ],
250 ));
251 }
252 }
253 }
254
255 Ok(alert)
256 }
257 }
258
259 deserializer.deserialize_any(AlertVisitor)
260 }
261}
262
263impl Serialize for Alert {
264 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
265 where
266 S: serde::Serializer,
267 {
268 if self.title.is_none()
269 && self.title_loc_key.is_none()
270 && self.title_loc_args.is_none()
271 && self.subtitle.is_none()
272 && self.subtitle_loc_key.is_none()
273 && self.subtitle_loc_args.is_none()
274 && self.loc_key.is_none()
275 && self.loc_args.is_none()
276 && self.launch_image.is_none()
277 {
278 return serializer.serialize_str(self.body.as_deref().unwrap_or_default());
279 }
280
281 let mut len = 0;
282
283 if self.title_loc_key.is_some() {
285 len += 1;
286 if self.title_loc_args.is_some() {
287 len += 1;
288 }
289 } else if self.title.is_some() {
290 len += 1;
291 }
292
293 if self.subtitle_loc_key.is_some() {
295 len += 1;
296 if self.subtitle_loc_args.is_some() {
297 len += 1;
298 }
299 } else if self.subtitle.is_some() {
300 len += 1;
301 }
302
303 if self.loc_key.is_some() {
305 len += 1;
306 if self.loc_args.is_some() {
307 len += 1;
308 }
309 } else {
310 len += 1;
311 }
312
313 if self.launch_image.is_some() {
315 len += 1;
316 }
317
318 let mut alert = serializer.serialize_map(Some(len))?;
319
320 if let Some(title_loc_key) = &self.title_loc_key {
322 alert.serialize_entry("title-loc-key", title_loc_key)?;
323 if let Some(title_loc_args) = &self.title_loc_args {
324 alert.serialize_entry("title-loc-args", title_loc_args)?;
325 }
326 } else if let Some(title) = &self.title {
327 alert.serialize_entry("title", title)?;
328 }
329
330 if let Some(subtitle_loc_key) = &self.subtitle_loc_key {
332 alert.serialize_entry("subtitle-loc-key", subtitle_loc_key)?;
333 if let Some(subtitle_loc_args) = &self.subtitle_loc_args {
334 alert.serialize_entry("subtitle-loc-args", subtitle_loc_args)?;
335 }
336 } else if let Some(subtitle) = &self.subtitle {
337 alert.serialize_entry("subtitle", subtitle)?;
338 }
339
340 if let Some(loc_key) = &self.loc_key {
342 alert.serialize_entry("loc-key", loc_key)?;
343 if let Some(loc_args) = &self.loc_args {
344 alert.serialize_entry("loc-args", loc_args)?;
345 }
346 } else {
347 alert.serialize_entry("body", &self.body)?;
348 }
349
350 if let Some(launch_image) = &self.launch_image {
352 alert.serialize_entry("launch-image", launch_image)?;
353 }
354
355 alert.end()
356 }
357}
358
359#[derive(Clone, Debug, PartialEq)]
361pub struct Sound {
362 pub critical: bool,
364
365 pub name: String,
371
372 pub volume: f64,
375}
376
377impl Default for Sound {
378 fn default() -> Self {
379 Self {
380 critical: false,
381 name: "default".into(),
382 volume: 1.,
383 }
384 }
385}
386
387impl From<String> for Sound {
388 fn from(value: String) -> Self {
389 Self {
390 critical: false,
391 name: value,
392 volume: 0.,
393 }
394 }
395}
396
397impl<'a> From<&'a str> for Sound {
398 fn from(value: &'a str) -> Self {
399 Self {
400 critical: false,
401 name: value.to_string(),
402 volume: 0.,
403 }
404 }
405}
406
407impl<'de> Deserialize<'de> for Sound {
408 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
409 where
410 D: serde::Deserializer<'de>,
411 {
412 struct SoundVisitor;
413
414 impl<'de> Visitor<'de> for SoundVisitor {
415 type Value = Sound;
416
417 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
418 write!(formatter, "a sound struct")
419 }
420
421 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
422 where
423 E: de::Error,
424 {
425 Ok(Sound {
426 critical: false,
427 name: v.into(),
428 volume: 0.,
429 })
430 }
431
432 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
433 where
434 E: de::Error,
435 {
436 Ok(Sound {
437 critical: false,
438 name: v,
439 volume: 0.,
440 })
441 }
442
443 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
444 where
445 A: MapAccess<'de>,
446 {
447 let mut sound = Sound::default();
448 let mut match_critical = false;
449 let mut match_name = false;
450 let mut match_volume = false;
451
452 while let Some(key) = map.next_key::<&str>()? {
453 match key {
454 "critical" => {
455 let critical: i64 = map.next_value()?;
456 sound.critical = critical != 0;
457 match_critical = true;
458 }
459 "name" => {
460 sound.name = map.next_value()?;
461 match_name = true;
462 }
463 "volume" => {
464 sound.volume = map.next_value()?;
465 match_volume = true;
466 }
467 field => {
468 return Err(de::Error::unknown_field(
469 field,
470 &["critical", "name", "volume"],
471 ));
472 }
473 }
474 }
475
476 if !match_critical {
477 return Err(de::Error::missing_field("critical"));
478 }
479 if !match_name {
480 return Err(de::Error::missing_field("name"));
481 }
482 if !match_volume {
483 return Err(de::Error::missing_field("volume"));
484 }
485
486 Ok(sound)
487 }
488 }
489
490 deserializer.deserialize_any(SoundVisitor)
491 }
492}
493
494impl Serialize for Sound {
495 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
496 where
497 S: serde::Serializer,
498 {
499 if self.critical {
500 let mut sound = serializer.serialize_struct("Sound", 3)?;
501 sound.serialize_field("critical", &1)?;
502 sound.serialize_field("name", &self.name)?;
503 sound.serialize_field("volume", &self.volume.clamp(0., 1.))?;
504 sound.end()
505 } else {
506 self.name.serialize(serializer)
507 }
508 }
509}
510
511#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
513#[serde(rename_all = "kebab-case")]
514pub enum InterruptionLevel {
515 Active,
518
519 Critical,
522
523 Passive,
526
527 TimeSensitive,
531}
532
533derive_fromstr_from_deserialize!(InterruptionLevel);
534derive_display_from_serialize!(InterruptionLevel);
535
536#[cfg(test)]
537mod test {
538 use std::str::FromStr;
539
540 use serde_json::json;
541
542 use super::*;
543
544 #[derive(Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
545 struct TestUserInfo {
546 foo: bool,
547 bar: i64,
548 }
549
550 #[test]
551 fn payload_de() {
552 assert_eq!(
553 serde_json::from_str::<Payload>(
554 &json!({
555 "aps": {
556 "alert": "Hello World!",
557 "badge": 11,
558 "sound": "default",
559 "thread-id": "my-thread-id",
560 "category": "my-category",
561 "content-available": 1,
562 "mutable-content": 1,
563 "target-content-id": "my-target-id",
564 "interruption-level": "active",
565 "relevance-score": 0.5,
566 },
567 })
568 .to_string()
569 )
570 .unwrap(),
571 Payload {
572 aps: Aps {
573 alert: Some("Hello World!".into()),
574 badge: Some(11),
575 sound: Some("default".into()),
576 thread_id: Some("my-thread-id".into()),
577 category: Some("my-category".into()),
578 content_available: true,
579 mutable_content: true,
580 target_content_id: Some("my-target-id".into()),
581 interruption_level: Some(InterruptionLevel::Active),
582 relevance_score: Some(0.5),
583 },
584 user_info: Some(())
585 }
586 );
587 assert_eq!(
588 serde_json::from_str::<Payload<TestUserInfo>>(
589 &json!({
590 "aps": {
591 "alert": "Hello World!",
592 },
593 "foo": true,
594 "bar": -10,
595 })
596 .to_string()
597 )
598 .unwrap(),
599 Payload::<TestUserInfo> {
600 aps: Aps {
601 alert: Some("Hello World!".into()),
602 ..Default::default()
603 },
604 user_info: Some(TestUserInfo {
605 foo: true,
606 bar: -10
607 }),
608 }
609 );
610 }
611
612 #[test]
613 fn payload_ser() {
614 assert_eq!(
615 serde_json::to_value(&Payload {
616 aps: Aps {
617 alert: Some("Hello World!".into()),
618 badge: Some(11),
619 sound: Some("default".into()),
620 thread_id: Some("my-thread-id".into()),
621 category: Some("my-category".into()),
622 content_available: true,
623 mutable_content: true,
624 target_content_id: Some("my-target-id".into()),
625 interruption_level: Some(InterruptionLevel::Active),
626 relevance_score: Some(0.5),
627 },
628 user_info: Some(()),
629 })
630 .unwrap(),
631 json!({
632 "aps": {
633 "alert": "Hello World!",
634 "badge": 11,
635 "sound": "default",
636 "thread-id": "my-thread-id",
637 "category": "my-category",
638 "content-available": 1,
639 "mutable-content": 1,
640 "target-content-id": "my-target-id",
641 "interruption-level": "active",
642 "relevance-score": 0.5,
643 },
644 })
645 );
646 assert_eq!(
647 serde_json::to_value(&Payload::<TestUserInfo> {
648 aps: Aps {
649 alert: Some("Hello World!".into()),
650 ..Default::default()
651 },
652 user_info: Some(TestUserInfo {
653 foo: true,
654 bar: -10
655 }),
656 })
657 .unwrap(),
658 json!({
659 "aps": {
660 "alert": "Hello World!",
661 },
662 "foo": true,
663 "bar": -10,
664 })
665 );
666 }
667
668 #[test]
669 fn alert_de() {
670 assert_eq!(
671 serde_json::from_str::<Alert>(&json!("Hello World!").to_string()).unwrap(),
672 Alert {
673 body: Some("Hello World!".into()),
674 ..Default::default()
675 }
676 );
677 assert_eq!(
678 serde_json::from_str::<Alert>(
679 &json!({
680 "body": "Hello World!"
681 })
682 .to_string()
683 )
684 .unwrap(),
685 Alert {
686 body: Some("Hello World!".into()),
687 ..Default::default()
688 }
689 );
690 assert_eq!(
691 serde_json::from_str::<Alert>(
692 &json!({
693 "title": "Title",
694 "subtitle": "Subtitle",
695 "body": "Hello World!",
696 "launch-image": "http://example.com/img.png",
697 })
698 .to_string()
699 )
700 .unwrap(),
701 Alert {
702 title: Some("Title".into()),
703 subtitle: Some("Subtitle".into()),
704 body: Some("Hello World!".into()),
705 launch_image: Some("http://example.com/img.png".into()),
706 ..Default::default()
707 }
708 );
709 assert_eq!(
710 serde_json::from_str::<Alert>(
711 &json!({
712 "title": "Title",
713 "subtitle": "Subtitle",
714 "body": "Hello World!",
715 "launch-image": "http://example.com/img.png",
716 "title-loc-key": "REQUEST_FORMAT",
717 "title-loc-args": ["Foo", "Bar"],
718 "subtitle-loc-key": "SUBTITLE_FORMAT",
719 "subtitle-loc-args": ["Bar", "Baz"],
720 "loc-key": "BODY_FORMAT",
721 "loc-args": ["Apple", "Pie"],
722 })
723 .to_string()
724 )
725 .unwrap(),
726 Alert {
727 title: Some("Title".into()),
728 subtitle: Some("Subtitle".into()),
729 body: Some("Hello World!".into()),
730 launch_image: Some("http://example.com/img.png".into()),
731 title_loc_key: Some("REQUEST_FORMAT".into()),
732 title_loc_args: Some(vec!["Foo".into(), "Bar".into()]),
733 subtitle_loc_key: Some("SUBTITLE_FORMAT".into()),
734 subtitle_loc_args: Some(vec!["Bar".into(), "Baz".into()]),
735 loc_key: Some("BODY_FORMAT".into()),
736 loc_args: Some(vec!["Apple".into(), "Pie".into()]),
737 }
738 );
739 }
740
741 #[test]
742 fn alert_ser() {
743 assert_eq!(
744 serde_json::to_string(&Alert {
745 body: Some("Hello World!".into()),
746 ..Default::default()
747 })
748 .unwrap(),
749 json!("Hello World!").to_string()
750 );
751 assert_eq!(
752 serde_json::to_value(&Alert {
753 title: Some("Title".into()),
754 subtitle: Some("Subtitle".into()),
755 body: Some("Hello World!".into()),
756 launch_image: Some("http://example.com/img.png".into()),
757 ..Default::default()
758 })
759 .unwrap(),
760 json!({
761 "title": "Title",
762 "subtitle": "Subtitle",
763 "body": "Hello World!",
764 "launch-image": "http://example.com/img.png",
765 })
766 );
767 assert_eq!(
768 serde_json::to_value(&Alert {
769 title: Some("Title".into()),
770 subtitle: Some("Subtitle".into()),
771 body: Some("Hello World!".into()),
772 launch_image: Some("http://example.com/img.png".into()),
773 title_loc_key: Some("REQUEST_FORMAT".into()),
774 title_loc_args: Some(vec!["Foo".into(), "Bar".into()]),
775 subtitle_loc_key: Some("SUBTITLE_FORMAT".into()),
776 subtitle_loc_args: Some(vec!["Bar".into(), "Baz".into()]),
777 loc_key: Some("BODY_FORMAT".into()),
778 loc_args: Some(vec!["Apple".into(), "Pie".into()]),
779 })
780 .unwrap(),
781 json!({
782 "title-loc-key": "REQUEST_FORMAT",
783 "title-loc-args": ["Foo", "Bar"],
784 "subtitle-loc-key": "SUBTITLE_FORMAT",
785 "subtitle-loc-args": ["Bar", "Baz"],
786 "loc-key": "BODY_FORMAT",
787 "loc-args": ["Apple", "Pie"],
788 "launch-image": "http://example.com/img.png",
789 })
790 );
791 }
792
793 #[test]
794 fn sound_de() {
795 assert_eq!(
796 serde_json::from_str::<Sound>(&json!("default").to_string()).unwrap(),
797 Sound {
798 critical: false,
799 name: "default".into(),
800 volume: 0.
801 }
802 );
803 assert_eq!(
804 serde_json::from_str::<Sound>(
805 &json!({
806 "critical": 1,
807 "name": "custom",
808 "volume": 0.5,
809 })
810 .to_string()
811 )
812 .unwrap(),
813 Sound {
814 critical: true,
815 name: "custom".into(),
816 volume: 0.5
817 }
818 );
819 assert_eq!(
820 serde_json::from_str::<Sound>(
821 &json!({
822 "critical": 0,
823 "name": "default",
824 "volume": 1.,
825 })
826 .to_string()
827 )
828 .unwrap(),
829 Sound {
830 critical: false,
831 name: "default".into(),
832 volume: 1.
833 }
834 );
835 assert!(serde_json::from_str::<Sound>(
836 &json!({
837 "name": "default",
838 "volume": 1.,
839 })
840 .to_string()
841 )
842 .is_err());
843 }
844
845 #[test]
846 fn sound_ser() {
847 assert_eq!(
848 serde_json::to_string(&Sound {
849 critical: false,
850 name: "default".into(),
851 volume: 0.
852 })
853 .unwrap(),
854 json!("default").to_string(),
855 );
856 assert_eq!(
857 serde_json::to_string(&Sound {
858 critical: true,
859 name: "custom".into(),
860 volume: 0.5
861 })
862 .unwrap(),
863 json!({
864 "critical": 1,
865 "name": "custom",
866 "volume": 0.5,
867 })
868 .to_string()
869 );
870 assert_eq!(
871 serde_json::to_string(&Sound {
872 critical: true,
873 name: "default".into(),
874 volume: 1.
875 })
876 .unwrap(),
877 json!({
878 "critical": 1,
879 "name": "default",
880 "volume": 1.,
881 })
882 .to_string()
883 );
884 assert_eq!(
885 serde_json::to_string(&Sound {
886 critical: true,
887 name: "default".into(),
888 volume: 2.
889 })
890 .unwrap(),
891 json!({
892 "critical": 1,
893 "name": "default",
894 "volume": 1.,
895 })
896 .to_string()
897 );
898 }
899
900 #[test]
901 fn interruption_level_de() {
902 assert_eq!(
903 serde_json::from_str::<InterruptionLevel>("\"active\"").unwrap(),
904 InterruptionLevel::Active
905 );
906 assert_eq!(
907 serde_json::from_str::<InterruptionLevel>("\"critical\"").unwrap(),
908 InterruptionLevel::Critical
909 );
910 assert_eq!(
911 serde_json::from_str::<InterruptionLevel>("\"passive\"").unwrap(),
912 InterruptionLevel::Passive
913 );
914 assert_eq!(
915 serde_json::from_str::<InterruptionLevel>("\"time-sensitive\"").unwrap(),
916 InterruptionLevel::TimeSensitive
917 );
918 assert!(serde_json::from_str::<InterruptionLevel>("\"invalid\"").is_err());
919 }
920
921 #[test]
922 fn interruption_level_ser() {
923 assert_eq!(
924 serde_json::to_string(&InterruptionLevel::Active).unwrap(),
925 "\"active\""
926 );
927 assert_eq!(
928 serde_json::to_string(&InterruptionLevel::Critical).unwrap(),
929 "\"critical\""
930 );
931 assert_eq!(
932 serde_json::to_string(&InterruptionLevel::Passive).unwrap(),
933 "\"passive\""
934 );
935 assert_eq!(
936 serde_json::to_string(&InterruptionLevel::TimeSensitive).unwrap(),
937 "\"time-sensitive\""
938 );
939 }
940
941 #[test]
942 fn interruption_level_from_str() {
943 assert_eq!(
944 InterruptionLevel::from_str("active").unwrap(),
945 InterruptionLevel::Active
946 );
947 assert_eq!(
948 InterruptionLevel::from_str("critical").unwrap(),
949 InterruptionLevel::Critical
950 );
951 assert_eq!(
952 InterruptionLevel::from_str("passive").unwrap(),
953 InterruptionLevel::Passive
954 );
955 assert_eq!(
956 InterruptionLevel::from_str("time-sensitive").unwrap(),
957 InterruptionLevel::TimeSensitive
958 );
959 assert!(InterruptionLevel::from_str("invalid").is_err());
960 }
961
962 #[test]
963 fn interruption_level_to_str() {
964 assert_eq!(InterruptionLevel::Active.to_string(), "active");
965 assert_eq!(InterruptionLevel::Critical.to_string(), "critical");
966 assert_eq!(InterruptionLevel::Passive.to_string(), "passive");
967 assert_eq!(
968 InterruptionLevel::TimeSensitive.to_string(),
969 "time-sensitive"
970 );
971 }
972}