Skip to main content

apns_h2/request/notification/
default.rs

1use crate::InterruptionLevel;
2use crate::request::notification::{NotificationBuilder, NotificationOptions};
3use crate::request::payload::{APS, APSAlert, APSSound, Payload};
4
5use std::{borrow::Cow, collections::BTreeMap};
6
7/// Represents a bool that serializes as a u8 0/1 for false/true respectively
8mod bool_as_u8 {
9    use serde::{
10        Deserialize,
11        de::{self, Deserializer, Unexpected},
12        ser::Serializer,
13    };
14
15    pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
16    where
17        D: Deserializer<'de>,
18    {
19        match u8::deserialize(deserializer)? {
20            0 => Ok(false),
21            1 => Ok(true),
22            other => Err(de::Error::invalid_value(
23                Unexpected::Unsigned(other as u64),
24                &"zero or one",
25            )),
26        }
27    }
28
29    pub fn serialize<S>(value: &bool, serializer: S) -> Result<S::Ok, S::Error>
30    where
31        S: Serializer,
32    {
33        serializer.serialize_u8(match value {
34            false => 0,
35            true => 1,
36        })
37    }
38}
39
40#[derive(Deserialize, Serialize, Debug, Clone, Default)]
41#[serde(rename_all = "kebab-case")]
42pub struct DefaultSound<'a> {
43    #[serde(skip_serializing_if = "std::ops::Not::not", with = "bool_as_u8")]
44    critical: bool,
45
46    #[serde(skip_serializing_if = "Option::is_none")]
47    name: Option<Cow<'a, str>>,
48
49    #[serde(skip_serializing_if = "Option::is_none")]
50    volume: Option<f64>,
51}
52
53#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq)]
54#[serde(rename_all = "kebab-case")]
55pub struct DefaultAlert<'a> {
56    #[serde(skip_serializing_if = "Option::is_none")]
57    title: Option<Cow<'a, str>>,
58
59    #[serde(skip_serializing_if = "Option::is_none")]
60    subtitle: Option<Cow<'a, str>>,
61
62    #[serde(skip_serializing_if = "Option::is_none")]
63    body: Option<Cow<'a, str>>,
64
65    #[serde(skip_serializing_if = "Option::is_none")]
66    launch_image: Option<Cow<'a, str>>,
67
68    #[serde(skip_serializing_if = "Option::is_none")]
69    title_loc_key: Option<Cow<'a, str>>,
70
71    #[serde(skip_serializing_if = "Option::is_none")]
72    title_loc_args: Option<Vec<Cow<'a, str>>>,
73
74    #[serde(skip_serializing_if = "Option::is_none")]
75    subtitle_loc_key: Option<Cow<'a, str>>,
76
77    #[serde(skip_serializing_if = "Option::is_none")]
78    subtitle_loc_args: Option<Vec<Cow<'a, str>>>,
79
80    #[serde(skip_serializing_if = "Option::is_none")]
81    action_loc_key: Option<Cow<'a, str>>,
82
83    #[serde(skip_serializing_if = "Option::is_none")]
84    loc_key: Option<Cow<'a, str>>,
85
86    #[serde(skip_serializing_if = "Option::is_none")]
87    loc_args: Option<Vec<Cow<'a, str>>>,
88}
89
90impl<'a> DefaultAlert<'a> {
91    pub(crate) fn body_only(body: Cow<'a, str>) -> Self {
92        DefaultAlert::<'_> {
93            body: Some(body),
94            ..Default::default()
95        }
96    }
97}
98
99/// A builder to create an APNs payload.
100///
101/// # Example
102///
103/// ```rust
104/// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
105/// # use apns_h2::request::payload::PayloadLike;
106/// # fn main() {
107/// let mut builder = DefaultNotificationBuilder::new()
108///     .title("Hi there")
109///     .subtitle("From bob")
110///     .body("What's up?")
111///     .badge(420)
112///     .category("cat1")
113///     .sound("prööt")
114///     .thread_id("my-thread")
115///     .critical(false, None)
116///     .mutable_content()
117///     .action_loc_key("PLAY")
118///     .launch_image("foo.jpg")
119///     .loc_args(&["argh", "narf"])
120///     .title_loc_key("STOP")
121///     .title_loc_args(&["herp", "derp"])
122///     .loc_key("PAUSE")
123///     .loc_args(&["narf", "derp"]);
124/// let payload = builder.build("device_id", Default::default())
125///   .to_json_string().unwrap();
126/// # }
127/// ```
128#[derive(Debug, Clone, Default)]
129pub struct DefaultNotificationBuilder<'a> {
130    alert: DefaultAlert<'a>,
131    badge: Option<u32>,
132    sound: DefaultSound<'a>,
133    thread_id: Option<Cow<'a, str>>,
134    category: Option<Cow<'a, str>>,
135    mutable_content: u8,
136    content_available: Option<u8>,
137    interruption_level: Option<InterruptionLevel>,
138    timestamp: Option<u64>,
139    event: Option<Cow<'a, str>>,
140    content_state: Option<serde_json::Value>,
141    attributes_type: Option<Cow<'a, str>>,
142    attributes: Option<serde_json::Value>,
143    input_push_channel: Option<Cow<'a, str>>,
144    input_push_token: Option<u8>,
145    dismissal_date: Option<u64>,
146}
147
148impl<'a> DefaultNotificationBuilder<'a> {
149    /// Creates a new builder with the minimum amount of content.
150    ///
151    /// ```rust
152    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
153    /// # use apns_h2::request::payload::PayloadLike;
154    /// # fn main() {
155    /// let payload = DefaultNotificationBuilder::new()
156    ///     .title("a title")
157    ///     .body("a body")
158    ///     .build("token", Default::default());
159    ///
160    /// assert_eq!(
161    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\",\"body\":\"a body\"},\"mutable-content\":0}}",
162    ///     &payload.to_json_string().unwrap()
163    /// );
164    /// # }
165    /// ```
166    pub fn new() -> DefaultNotificationBuilder<'a> {
167        Self::default()
168    }
169
170    /// Set the title of the notification.
171    /// Apple Watch displays this string in the short look notification interface.
172    /// Specify a string that's quickly understood by the user.
173    ///
174    /// ```rust
175    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
176    /// # use apns_h2::request::payload::PayloadLike;
177    /// # fn main() {
178    /// let mut builder = DefaultNotificationBuilder::new()
179    ///     .title("a title");
180    /// let payload = builder.build("token", Default::default());
181    ///
182    /// assert_eq!(
183    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0}}",
184    ///     &payload.to_json_string().unwrap()
185    /// );
186    /// # }
187    /// ```
188    pub fn title(mut self, title: impl Into<Cow<'a, str>>) -> Self {
189        self.alert.title = Some(title.into());
190        self
191    }
192
193    #[deprecated(
194        since = "0.11.0",
195        note = "Use the idiomatic `title` instead of the legacy `set_*` fn"
196    )]
197    pub fn set_title(self, title: impl Into<Cow<'a, str>>) -> Self {
198        self.title(title)
199    }
200
201    /// Set critical alert value for this notification
202    /// Volume can only be set when the notification is marked as critcial
203    /// Note: You'll need the [critical alerts entitlement](https://developer.apple.com/contact/request/notifications-critical-alerts-entitlement/) to use `true`!
204    ///
205    /// ```rust
206    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
207    /// # use apns_h2::request::payload::PayloadLike;
208    /// # fn main() {
209    /// let mut builder = DefaultNotificationBuilder::new()
210    ///     .critical(true, None);
211    /// let payload = builder.build("token", Default::default());
212    ///
213    /// assert_eq!(
214    ///     "{\"aps\":{\"sound\":{\"critical\":1},\"mutable-content\":0}}",
215    ///     &payload.to_json_string().unwrap()
216    /// );
217    /// # }
218    /// ```
219    pub fn critical(mut self, critical: bool, volume: Option<f64>) -> Self {
220        if !critical {
221            self.sound.volume = None;
222            self.sound.critical = false;
223        } else {
224            self.sound.volume = volume;
225            self.sound.critical = true;
226        }
227        self
228    }
229
230    #[deprecated(
231        since = "0.11.0",
232        note = "Use the idiomatic `critical` instead of the legacy `set_*` fn"
233    )]
234    pub fn set_critical(self, critical: bool, volume: Option<f64>) -> Self {
235        self.critical(critical, volume)
236    }
237
238    /// Used to set the subtitle which should provide additional information that explains the purpose of the notification.
239    ///
240    /// ```rust
241    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
242    /// # use apns_h2::request::payload::PayloadLike;
243    /// # fn main() {
244    /// let mut builder = DefaultNotificationBuilder::new()
245    ///     .subtitle("a subtitle");
246    /// let payload = builder.build("token", Default::default());
247    ///
248    /// assert_eq!(
249    ///     "{\"aps\":{\"alert\":{\"subtitle\":\"a subtitle\"},\"mutable-content\":0}}",
250    ///     &payload.to_json_string().unwrap()
251    /// );
252    /// # }
253    /// ```
254    pub fn subtitle(mut self, subtitle: impl Into<Cow<'a, str>>) -> Self {
255        self.alert.subtitle = Some(subtitle.into());
256        self
257    }
258
259    #[deprecated(
260        since = "0.11.0",
261        note = "Use the idiomatic `subtitle` instead of the legacy `set_*` fn"
262    )]
263    pub fn set_subtitle(self, subtitle: impl Into<Cow<'a, str>>) -> Self {
264        self.subtitle(subtitle)
265    }
266
267    /// Sets the content of the alert message.
268    ///
269    /// ```rust
270    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
271    /// # use apns_h2::request::payload::PayloadLike;
272    /// # fn main() {
273    /// let mut builder = DefaultNotificationBuilder::new()
274    ///     .body("a body");
275    /// let payload = builder.build("token", Default::default());
276    ///
277    /// assert_eq!(
278    ///     "{\"aps\":{\"alert\":{\"body\":\"a body\"},\"mutable-content\":0}}",
279    ///     &payload.to_json_string().unwrap()
280    /// );
281    /// # }
282    /// ```
283    pub fn body(mut self, body: impl Into<Cow<'a, str>>) -> Self {
284        self.alert.body = Some(body.into());
285        self
286    }
287
288    #[deprecated(
289        since = "0.11.0",
290        note = "Use the idiomatic `body` instead of the legacy `set_*` fn"
291    )]
292    pub fn set_body(self, body: impl Into<Cow<'a, str>>) -> Self {
293        self.body(body)
294    }
295
296    /// A number to show on a badge on top of the app icon.
297    ///
298    /// ```rust
299    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
300    /// # use apns_h2::request::payload::PayloadLike;
301    /// # fn main() {
302    /// let mut builder = DefaultNotificationBuilder::new()
303    ///     .badge(4);
304    /// let payload = builder.build("token", Default::default());
305    ///
306    /// assert_eq!(
307    ///     "{\"aps\":{\"badge\":4,\"mutable-content\":0}}",
308    ///     &payload.to_json_string().unwrap()
309    /// );
310    /// # }
311    /// ```
312    pub fn badge(mut self, badge: u32) -> Self {
313        self.badge = Some(badge);
314        self
315    }
316
317    #[deprecated(
318        since = "0.11.0",
319        note = "Use the idiomatic `badge` instead of the legacy `set_*` fn"
320    )]
321    pub fn set_badge(self, badge: u32) -> Self {
322        self.badge(badge)
323    }
324
325    /// File name of the custom sound to play when receiving the notification.
326    ///
327    /// ```rust
328    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
329    /// # use apns_h2::request::payload::PayloadLike;
330    /// # fn main() {
331    /// let mut builder = DefaultNotificationBuilder::new()
332    ///     .title("a title")
333    ///     .sound("ping");
334    /// let payload = builder.build("token", Default::default());
335    ///
336    /// assert_eq!(
337    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"sound\":\"ping\",\"mutable-content\":0}}",
338    ///     &payload.to_json_string().unwrap()
339    /// );
340    /// # }
341    /// ```
342    pub fn sound(mut self, sound: impl Into<Cow<'a, str>>) -> Self {
343        self.sound.name = Some(sound.into());
344        self
345    }
346
347    #[deprecated(
348        since = "0.11.0",
349        note = "Use the idiomatic `sound` instead of the legacy `set_*` fn"
350    )]
351    pub fn set_sound(self, sound: impl Into<Cow<'a, str>>) -> Self {
352        self.sound(sound)
353    }
354
355    /// An application-specific name that allows notifications to be grouped together.
356    ///
357    /// ```rust
358    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
359    /// # use apns_h2::request::payload::PayloadLike;
360    /// # fn main() {
361    /// let mut builder = DefaultNotificationBuilder::new()
362    ///     .title("a title")
363    ///     .thread_id("my-thread");
364    /// let payload = builder.build("token", Default::default());
365    ///
366    /// assert_eq!(
367    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"thread-id\":\"my-thread\",\"mutable-content\":0}}",
368    ///     &payload.to_json_string().unwrap()
369    /// );
370    /// # }
371    /// ```
372    pub fn thread_id(mut self, thread_id: impl Into<Cow<'a, str>>) -> Self {
373        self.thread_id = Some(thread_id.into());
374        self
375    }
376
377    /// When a notification includes the category key, the system displays the
378    /// actions for that category as buttons in the banner or alert interface.
379    ///
380    /// ```rust
381    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
382    /// # use apns_h2::request::payload::PayloadLike;
383    /// # fn main() {
384    /// let mut builder = DefaultNotificationBuilder::new()
385    ///     .title("a title")
386    ///     .category("cat1");
387    /// let payload = builder.build("token", Default::default());
388    ///
389    /// assert_eq!(
390    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"category\":\"cat1\",\"mutable-content\":0}}",
391    ///     &payload.to_json_string().unwrap()
392    /// );
393    /// # }
394    /// ```
395    pub fn category(mut self, category: impl Into<Cow<'a, str>>) -> Self {
396        self.category = Some(category.into());
397        self
398    }
399
400    #[deprecated(
401        since = "0.11.0",
402        note = "Use the idiomatic `category` instead of the legacy `set_*` fn"
403    )]
404    pub fn set_category(self, category: impl Into<Cow<'a, str>>) -> Self {
405        self.category(category)
406    }
407
408    /// The subtitle localization key for the notification title.
409    ///
410    /// ```rust
411    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
412    /// # use apns_h2::request::payload::PayloadLike;
413    /// # fn main() {
414    /// let mut builder = DefaultNotificationBuilder::new()
415    ///     .title("a title")
416    ///     .subtitle_loc_key("yolo");
417    /// let payload = builder.build("token", Default::default());
418    ///
419    /// assert_eq!(
420    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\",\"subtitle-loc-key\":\"yolo\"},\"mutable-content\":0}}",
421    ///     &payload.to_json_string().unwrap()
422    /// );
423    /// # }
424    /// ```
425    pub fn subtitle_loc_key(mut self, key: impl Into<Cow<'a, str>>) -> Self {
426        self.alert.subtitle_loc_key = Some(key.into());
427        self
428    }
429
430    /// Arguments for the title localization.
431    ///
432    /// ```rust
433    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
434    /// # use apns_h2::request::payload::PayloadLike;
435    /// # fn main() {
436    /// let mut builder = DefaultNotificationBuilder::new()
437    ///     .title("a title")
438    ///     .subtitle_loc_args(&["fooz", "barz"]);
439    /// let payload = builder.build("token", Default::default());
440    ///
441    /// assert_eq!(
442    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\",\"subtitle-loc-args\":[\"fooz\",\"barz\"]},\"mutable-content\":0}}",
443    ///     &payload.to_json_string().unwrap()
444    /// );
445    /// # }
446    /// ```
447    pub fn subtitle_loc_args<S>(mut self, args: &'a [S]) -> Self
448    where
449        S: Into<Cow<'a, str>> + AsRef<str>,
450    {
451        let converted = args.iter().map(AsRef::as_ref).map(Into::into).collect();
452
453        self.alert.subtitle_loc_args = Some(converted);
454        self
455    }
456
457    /// The localization key for the notification title.
458    ///
459    /// ```rust
460    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
461    /// # use apns_h2::request::payload::PayloadLike;
462    /// # fn main() {
463    /// let mut builder = DefaultNotificationBuilder::new()
464    ///     .title("a title")
465    ///     .title_loc_key("play");
466    /// let payload = builder.build("token", Default::default());
467    ///
468    /// assert_eq!(
469    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\",\"title-loc-key\":\"play\"},\"mutable-content\":0}}",
470    ///     &payload.to_json_string().unwrap()
471    /// );
472    /// # }
473    /// ```
474    pub fn title_loc_key(mut self, key: impl Into<Cow<'a, str>>) -> Self {
475        self.alert.title_loc_key = Some(key.into());
476        self
477    }
478
479    #[deprecated(
480        since = "0.11.0",
481        note = "Use the idiomatic `title_loc_key` instead of the legacy `set_*` fn"
482    )]
483    pub fn set_title_loc_key(self, key: impl Into<Cow<'a, str>>) -> Self {
484        self.title_loc_key(key)
485    }
486
487    /// Arguments for the title localization.
488    ///
489    /// ```rust
490    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
491    /// # use apns_h2::request::payload::PayloadLike;
492    /// # fn main() {
493    /// let mut builder = DefaultNotificationBuilder::new()
494    ///     .title("a title")
495    ///     .title_loc_args(&["foo", "bar"]);
496    /// let payload = builder.build("token", Default::default());
497    ///
498    /// assert_eq!(
499    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\",\"title-loc-args\":[\"foo\",\"bar\"]},\"mutable-content\":0}}",
500    ///     &payload.to_json_string().unwrap()
501    /// );
502    /// # }
503    /// ```
504    pub fn title_loc_args<S>(mut self, args: &'a [S]) -> Self
505    where
506        S: Into<Cow<'a, str>> + AsRef<str>,
507    {
508        let converted = args.iter().map(AsRef::as_ref).map(Into::into).collect();
509
510        self.alert.title_loc_args = Some(converted);
511        self
512    }
513
514    #[deprecated(
515        since = "0.11.0",
516        note = "Use the idiomatic `title_loc_args` instead of the legacy `set_*` fn"
517    )]
518    pub fn set_title_loc_args<S>(self, key: &'a [S]) -> Self
519    where
520        S: Into<Cow<'a, str>> + AsRef<str>,
521    {
522        self.title_loc_args(key)
523    }
524
525    /// The localization key for the action.
526    ///
527    /// ```rust
528    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
529    /// # use apns_h2::request::payload::PayloadLike;
530    /// # fn main() {
531    /// let mut builder = DefaultNotificationBuilder::new()
532    ///     .title("a title")
533    ///     .action_loc_key("stop");
534    /// let payload = builder.build("token", Default::default());
535    ///
536    /// assert_eq!(
537    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\",\"action-loc-key\":\"stop\"},\"mutable-content\":0}}",
538    ///     &payload.to_json_string().unwrap()
539    /// );
540    /// # }
541    /// ```
542    pub fn action_loc_key(mut self, key: impl Into<Cow<'a, str>>) -> Self {
543        self.alert.action_loc_key = Some(key.into());
544        self
545    }
546
547    #[deprecated(
548        since = "0.11.0",
549        note = "Use the idiomatic `action_loc_key` instead of the legacy `set_*` fn"
550    )]
551    pub fn set_action_loc_key(self, key: impl Into<Cow<'a, str>>) -> Self {
552        self.action_loc_key(key)
553    }
554
555    /// The localization key for the push message body.
556    ///
557    /// ```rust
558    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
559    /// # use apns_h2::request::payload::PayloadLike;
560    /// # fn main() {
561    /// let mut builder = DefaultNotificationBuilder::new()
562    ///     .title("a title")
563    ///     .loc_key("lol");
564    /// let payload = builder.build("token", Default::default());
565    ///
566    /// assert_eq!(
567    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\",\"loc-key\":\"lol\"},\"mutable-content\":0}}",
568    ///     &payload.to_json_string().unwrap()
569    /// );
570    /// # }
571    /// ```
572    pub fn loc_key(mut self, key: impl Into<Cow<'a, str>>) -> Self {
573        self.alert.loc_key = Some(key.into());
574        self
575    }
576
577    #[deprecated(
578        since = "0.11.0",
579        note = "Use the idiomatic `loc_key` instead of the legacy `set_*` fn"
580    )]
581    pub fn set_loc_key(self, key: impl Into<Cow<'a, str>>) -> Self {
582        self.loc_key(key)
583    }
584
585    /// Arguments for the content localization.
586    ///
587    /// ```rust
588    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
589    /// # use apns_h2::request::payload::PayloadLike;
590    /// # fn main() {
591    /// let mut builder = DefaultNotificationBuilder::new()
592    ///     .title("a title")
593    ///     .loc_args(&["omg", "foo"]);
594    /// let payload = builder.build("token", Default::default());
595    ///
596    /// assert_eq!(
597    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\",\"loc-args\":[\"omg\",\"foo\"]},\"mutable-content\":0}}",
598    ///     &payload.to_json_string().unwrap()
599    /// );
600    /// # }
601    /// ```
602    pub fn loc_args<S>(mut self, args: &'a [S]) -> Self
603    where
604        S: Into<Cow<'a, str>> + AsRef<str>,
605    {
606        let converted = args.iter().map(|a| a.as_ref().into()).collect();
607
608        self.alert.loc_args = Some(converted);
609        self
610    }
611
612    #[deprecated(
613        since = "0.11.0",
614        note = "Use the idiomatic `loc_args` instead of the legacy `set_*` fn"
615    )]
616    pub fn set_loc_args<S>(self, key: &'a [S]) -> Self
617    where
618        S: Into<Cow<'a, str>> + AsRef<str>,
619    {
620        self.loc_args(key)
621    }
622
623    /// Image to display in the rich notification.
624    ///
625    /// ```rust
626    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
627    /// # use apns_h2::request::payload::PayloadLike;
628    /// # fn main() {
629    /// let mut builder = DefaultNotificationBuilder::new()
630    ///     .title("a title")
631    ///     .launch_image("cat.png");
632    /// let payload = builder.build("token", Default::default());
633    ///
634    /// assert_eq!(
635    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\",\"launch-image\":\"cat.png\"},\"mutable-content\":0}}",
636    ///     &payload.to_json_string().unwrap()
637    /// );
638    /// # }
639    /// ```
640    pub fn launch_image(mut self, image: impl Into<Cow<'a, str>>) -> Self {
641        self.alert.launch_image = Some(image.into());
642        self
643    }
644
645    #[deprecated(
646        since = "0.11.0",
647        note = "Use the idiomatic `launch_image` instead of the legacy `set_*` fn"
648    )]
649    pub fn set_launch_image(self, image: impl Into<Cow<'a, str>>) -> Self {
650        self.launch_image(image)
651    }
652
653    /// Allow client to modify push content before displaying.
654    ///
655    /// ```rust
656    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
657    /// # use apns_h2::request::payload::PayloadLike;
658    /// # fn main() {
659    /// let mut builder = DefaultNotificationBuilder::new()
660    ///     .title("a title")
661    ///     .mutable_content();
662    /// let payload = builder.build("token", Default::default());
663    ///
664    /// assert_eq!(
665    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":1}}",
666    ///     &payload.to_json_string().unwrap()
667    /// );
668    /// # }
669    /// ```
670    pub fn mutable_content(mut self) -> Self {
671        self.mutable_content = 1;
672        self
673    }
674
675    #[deprecated(
676        since = "0.11.0",
677        note = "Use the idiomatic `mutable_content` instead of the legacy `set_*` fn"
678    )]
679    pub fn set_mutable_content(self) -> Self {
680        self.mutable_content()
681    }
682
683    /// Used for adding custom data to push notifications
684    ///
685    /// ```rust
686    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
687    /// # use apns_h2::request::payload::PayloadLike;
688    /// # fn main() {
689    /// let mut builder = DefaultNotificationBuilder::new()
690    ///     .title("a title")
691    ///     .content_available();
692    /// let payload = builder.build("token", Default::default());
693    ///
694    /// assert_eq!(
695    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"content-available\":1,\"mutable-content\":0}}",
696    ///     &payload.to_json_string().unwrap()
697    /// );
698    /// # }
699    /// ```
700    pub fn content_available(mut self) -> Self {
701        self.content_available = Some(1);
702        self
703    }
704
705    #[deprecated(
706        since = "0.11.0",
707        note = "Use the idiomatic `content_available` instead of the legacy `set_*` fn"
708    )]
709    pub fn set_content_available(self) -> Self {
710        self.content_available()
711    }
712
713    /// Set the interruption level to active. The system presents the notification
714    /// immediately, lights up the screen, and can play a sound.
715    ///
716    /// ```rust
717    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
718    /// # use apns_h2::request::payload::PayloadLike;
719    /// # fn main() {
720    /// let mut builder = DefaultNotificationBuilder::new()
721    ///     .title("a title")
722    ///     .active_interruption_level();
723    /// let payload = builder.build("token", Default::default());
724    ///
725    /// assert_eq!(
726    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"interruption-level\":\"active\"}}",
727    ///     &payload.to_json_string().unwrap()
728    /// );
729    /// # }
730    /// ```
731    pub fn active_interruption_level(mut self) -> Self {
732        self.interruption_level = Some(InterruptionLevel::Active);
733        self
734    }
735
736    /// Set the interruption level to critical. The system presents the notification
737    /// immediately, lights up the screen, and bypasses the mute switch to play a sound.
738    ///
739    /// ```rust
740    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
741    /// # use apns_h2::request::payload::PayloadLike;
742    /// # fn main() {
743    /// let mut builder = DefaultNotificationBuilder::new()
744    ///     .title("a title")
745    ///     .critical_interruption_level();
746    /// let payload = builder.build("token", Default::default());
747    ///
748    /// assert_eq!(
749    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"interruption-level\":\"critical\"}}",
750    ///     &payload.to_json_string().unwrap()
751    /// );
752    /// # }
753    /// ```
754    pub fn critical_interruption_level(mut self) -> Self {
755        self.interruption_level = Some(InterruptionLevel::Critical);
756        self
757    }
758
759    /// Set the interruption level to passive. The system adds the notification to
760    /// the notification list without lighting up the screen or playing a sound.
761    ///
762    /// ```rust
763    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
764    /// # use apns_h2::request::payload::PayloadLike;
765    /// # fn main() {
766    /// let mut builder = DefaultNotificationBuilder::new()
767    ///     .title("a title")
768    ///     .passive_interruption_level();
769    /// let payload = builder.build("token", Default::default());
770    ///
771    /// assert_eq!(
772    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"interruption-level\":\"passive\"}}",
773    ///     &payload.to_json_string().unwrap()
774    /// );
775    /// # }
776    /// ```
777    pub fn passive_interruption_level(mut self) -> Self {
778        self.interruption_level = Some(InterruptionLevel::Passive);
779        self
780    }
781
782    /// Set the interruption level to time sensitive. The system presents the notification
783    /// immediately, lights up the screen, can play a sound, and breaks through system
784    /// notification controls.
785    ///
786    /// ```rust
787    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
788    /// # use apns_h2::request::payload::PayloadLike;
789    /// # fn main() {
790    /// let mut builder = DefaultNotificationBuilder::new()
791    ///     .title("a title")
792    ///     .time_sensitive_interruption_level();
793    /// let payload = builder.build("token", Default::default());
794    ///
795    /// assert_eq!(
796    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"interruption-level\":\"time-sensitive\"}}",
797    ///     &payload.to_json_string().unwrap()
798    /// );
799    /// # }
800    /// ```
801    pub fn time_sensitive_interruption_level(mut self) -> Self {
802        self.interruption_level = Some(InterruptionLevel::TimeSensitive);
803        self
804    }
805
806    /// Set the interruption level directly. Controls how the notification is presented to the user.
807    ///
808    /// ```rust
809    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
810    /// # use apns_h2::request::payload::{PayloadLike, InterruptionLevel};
811    /// # fn main() {
812    /// let mut builder = DefaultNotificationBuilder::new()
813    ///     .title("a title")
814    ///     .interruption_level(InterruptionLevel::Active);
815    /// let payload = builder.build("token", Default::default());
816    ///
817    /// assert_eq!(
818    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"interruption-level\":\"active\"}}",
819    ///     &payload.to_json_string().unwrap()
820    /// );
821    /// # }
822    /// ```
823    pub fn interruption_level(mut self, level: InterruptionLevel) -> Self {
824        self.interruption_level = Some(level);
825        self
826    }
827
828    /// Set the timestamp for a Live Activity update
829    ///
830    /// ```rust
831    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
832    /// # use apns_h2::request::payload::PayloadLike;
833    /// # fn main() {
834    /// let payload = DefaultNotificationBuilder::new()
835    ///     .timestamp(1234)
836    ///     .build("token", Default::default());
837    ///
838    /// assert_eq!(
839    ///     "{\"aps\":{\"mutable-content\":0,\"timestamp\":1234}}",
840    ///     &payload.to_json_string().unwrap()
841    /// );
842    /// # }
843    /// ```
844    pub fn timestamp(mut self, timestamp: u64) -> Self {
845        self.timestamp = Some(timestamp);
846        self
847    }
848
849    /// Set the event for a Live Activity. Use "start" to begin a Live Activity.
850    ///
851    /// ```rust
852    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
853    /// # use apns_h2::request::payload::PayloadLike;
854    /// # fn main() {
855    /// let payload = DefaultNotificationBuilder::new()
856    ///     .event("start")
857    ///     .build("token", Default::default());
858    ///
859    /// assert_eq!(
860    ///     "{\"aps\":{\"mutable-content\":0,\"event\":\"start\"}}",
861    ///     &payload.to_json_string().unwrap()
862    /// );
863    /// # }
864    /// ```
865    pub fn event(mut self, event: impl Into<Cow<'a, str>>) -> Self {
866        self.event = Some(event.into());
867        self
868    }
869
870    /// Set the content state for a Live Activity with dynamic data
871    ///
872    /// ```rust
873    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
874    /// # use apns_h2::request::payload::PayloadLike;
875    /// # use serde_json::json;
876    /// # fn main() {
877    /// let content_state = json!({
878    ///     "currentHealthLevel": 100,
879    ///     "eventDescription": "Adventure has begun!"
880    /// });
881    /// let payload = DefaultNotificationBuilder::new()
882    ///     .content_state(&content_state)
883    ///     .build("token", Default::default());
884    ///
885    /// assert!(payload.to_json_string().unwrap().contains("\"content-state\":{\"currentHealthLevel\":100,\"eventDescription\":\"Adventure has begun!\"}"));
886    /// # }
887    /// ```
888    pub fn content_state(mut self, content_state: &serde_json::Value) -> Self {
889        self.content_state = Some(content_state.clone());
890        self
891    }
892
893    /// Set the attributes type for a Live Activity
894    ///
895    /// ```rust
896    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
897    /// # use apns_h2::request::payload::PayloadLike;
898    /// # fn main() {
899    /// let payload = DefaultNotificationBuilder::new()
900    ///     .attributes_type("AdventureAttributes")
901    ///     .build("token", Default::default());
902    ///
903    /// assert_eq!(
904    ///     "{\"aps\":{\"mutable-content\":0,\"attributes-type\":\"AdventureAttributes\"}}",
905    ///     &payload.to_json_string().unwrap()
906    /// );
907    /// # }
908    /// ```
909    pub fn attributes_type(mut self, attributes_type: impl Into<Cow<'a, str>>) -> Self {
910        self.attributes_type = Some(attributes_type.into());
911        self
912    }
913
914    /// Set the attributes for a Live Activity with data defining the Live Activity
915    ///
916    /// ```rust
917    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
918    /// # use apns_h2::request::payload::PayloadLike;
919    /// # use serde_json::json;
920    /// # fn main() {
921    /// let attributes = json!({
922    ///     "currentHealthLevel": 100,
923    ///     "eventDescription": "Adventure has begun!"
924    /// });
925    /// let payload = DefaultNotificationBuilder::new()
926    ///     .attributes(&attributes)
927    ///     .build("token", Default::default());
928    ///
929    /// assert!(payload.to_json_string().unwrap().contains("\"attributes\":{\"currentHealthLevel\":100,\"eventDescription\":\"Adventure has begun!\"}"));
930    /// # }
931    /// ```
932    pub fn attributes(mut self, attributes: &serde_json::Value) -> Self {
933        self.attributes = Some(attributes.clone());
934        self
935    }
936
937    /// Set the input push channel ID for iOS 18+ channel-based Live Activity updates
938    ///
939    /// ```rust
940    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
941    /// # use apns_h2::request::payload::PayloadLike;
942    /// # fn main() {
943    /// let payload = DefaultNotificationBuilder::new()
944    ///     .input_push_channel("dHN0LXNyY2gtY2hubA==")
945    ///     .build("token", Default::default());
946    ///
947    /// assert_eq!(
948    ///     "{\"aps\":{\"mutable-content\":0,\"input-push-channel\":\"dHN0LXNyY2gtY2hubA==\"}}",
949    ///     &payload.to_json_string().unwrap()
950    /// );
951    /// # }
952    /// ```
953    pub fn input_push_channel(mut self, channel_id: impl Into<Cow<'a, str>>) -> Self {
954        self.input_push_channel = Some(channel_id.into());
955        self
956    }
957
958    /// Enable input push token request for iOS 18+ token-based Live Activity updates
959    ///
960    /// ```rust
961    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
962    /// # use apns_h2::request::payload::PayloadLike;
963    /// # fn main() {
964    /// let payload = DefaultNotificationBuilder::new()
965    ///     .input_push_token()
966    ///     .build("token", Default::default());
967    ///
968    /// assert_eq!(
969    ///     "{\"aps\":{\"mutable-content\":0,\"input-push-token\":1}}",
970    ///     &payload.to_json_string().unwrap()
971    /// );
972    /// # }
973    /// ```
974    pub fn input_push_token(mut self) -> Self {
975        self.input_push_token = Some(1);
976        self
977    }
978
979    /// Set the dismissal date for when the system should automatically remove the notification.
980    /// The timestamp should be in Unix epoch time (seconds since 1970-01-01 00:00:00 UTC).
981    ///
982    /// ```rust
983    /// # use apns_h2::request::notification::{DefaultNotificationBuilder, NotificationBuilder};
984    /// # use apns_h2::request::payload::PayloadLike;
985    /// # fn main() {
986    /// let payload = DefaultNotificationBuilder::new()
987    ///     .title("a title")
988    ///     .dismissal_date(1672531200) // January 1, 2023 00:00:00 UTC
989    ///     .build("token", Default::default());
990    ///
991    /// assert_eq!(
992    ///     "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0,\"dismissal-date\":1672531200}}",
993    ///     &payload.to_json_string().unwrap()
994    /// );
995    /// # }
996    /// ```
997    pub fn dismissal_date(mut self, dismissal_date: u64) -> Self {
998        self.dismissal_date = Some(dismissal_date);
999        self
1000    }
1001}
1002
1003impl<'a> NotificationBuilder<'a> for DefaultNotificationBuilder<'a> {
1004    fn build(self, device_token: impl Into<Cow<'a, str>>, options: NotificationOptions<'a>) -> Payload<'a> {
1005        use std::sync::OnceLock;
1006
1007        static DEFAULT_ALERT: OnceLock<DefaultAlert<'static>> = OnceLock::new();
1008
1009        Payload {
1010            aps: APS {
1011                alert: if &self.alert == DEFAULT_ALERT.get_or_init(Default::default) {
1012                    None
1013                } else {
1014                    Some(APSAlert::Default(Box::new(self.alert)))
1015                },
1016                badge: self.badge,
1017                sound: if self.sound.critical {
1018                    Some(APSSound::Critical(self.sound))
1019                } else {
1020                    self.sound.name.map(APSSound::Sound)
1021                },
1022                thread_id: self.thread_id,
1023                content_available: self.content_available,
1024                category: self.category,
1025                mutable_content: Some(self.mutable_content),
1026                interruption_level: self.interruption_level,
1027                dismissal_date: self.dismissal_date,
1028                url_args: None,
1029                timestamp: self.timestamp,
1030                event: self.event,
1031                content_state: self.content_state,
1032                attributes_type: self.attributes_type,
1033                attributes: self.attributes,
1034                input_push_channel: self.input_push_channel,
1035                input_push_token: self.input_push_token,
1036            },
1037            device_token: device_token.into(),
1038            options,
1039            data: BTreeMap::new(),
1040        }
1041    }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046    use super::*;
1047    use serde_json::value::to_value;
1048
1049    #[test]
1050    fn test_default_notification_with_minimal_required_values() {
1051        let payload = DefaultNotificationBuilder::new()
1052            .title("the title")
1053            .body("the body")
1054            .build("device-token", Default::default());
1055
1056        let expected_payload = json!({
1057            "aps": {
1058                "alert": {
1059                    "body": "the body",
1060                    "title": "the title",
1061                },
1062                "mutable-content": 0
1063            }
1064        });
1065
1066        assert_eq!(expected_payload, to_value(payload).unwrap());
1067    }
1068
1069    #[test]
1070    fn test_default_notification_with_dismissal_date() {
1071        let builder = DefaultNotificationBuilder::new()
1072            .title("Test Title")
1073            .body("Test Body")
1074            .dismissal_date(1672531200); // January 1, 2023 00:00:00 UTC
1075
1076        let payload = builder.build("device-token", Default::default());
1077
1078        let expected_payload = json!({
1079            "aps": {
1080                "alert": {
1081                    "title": "Test Title",
1082                    "body": "Test Body"
1083                },
1084                "dismissal-date": 1672531200,
1085                "mutable-content": 0
1086            }
1087        });
1088
1089        assert_eq!(expected_payload, to_value(payload).unwrap());
1090    }
1091
1092    #[test]
1093    fn test_loc_args_inputs() {
1094        let owned_strings: Vec<String> = vec!["hello".to_string(), "world".to_string()];
1095        let borrowed_strings: Vec<&str> = vec!["foo", "bar"];
1096        let slice_strings: &[&str] = &["baz", "qux"];
1097        let owned_cows: Vec<Cow<'static, str>> = vec![Cow::Borrowed("narf"), Cow::Owned("derp".to_string())];
1098        let builder = DefaultNotificationBuilder::new()
1099            .loc_args(&owned_strings)
1100            .loc_args(&borrowed_strings)
1101            .loc_args(slice_strings)
1102            .loc_args(&owned_cows);
1103
1104        let payload = builder.build("device-token", Default::default());
1105
1106        let expected_payload = json!({
1107            "aps": {
1108                "alert": {
1109                    "loc-args": ["narf", "derp"],
1110                },
1111                "mutable-content": 0,
1112            }
1113        });
1114
1115        assert_eq!(expected_payload, to_value(payload).unwrap());
1116    }
1117
1118    #[test]
1119    fn test_default_notification_with_full_data() {
1120        let builder = DefaultNotificationBuilder::new()
1121            .title("the title")
1122            .body("the body")
1123            .badge(420)
1124            .category("cat1")
1125            .sound("prööt")
1126            .critical(true, Some(1.0))
1127            .mutable_content()
1128            .action_loc_key("PLAY")
1129            .launch_image("foo.jpg")
1130            .loc_args(&["argh", "narf"])
1131            .title_loc_key("STOP")
1132            .title_loc_args(&["herp", "derp"])
1133            .loc_key("PAUSE")
1134            .loc_args(&["narf", "derp"]);
1135
1136        let payload = builder.build("device-token", Default::default());
1137
1138        let expected_payload = json!({
1139            "aps": {
1140                "alert": {
1141                    "action-loc-key": "PLAY",
1142                    "body": "the body",
1143                    "launch-image": "foo.jpg",
1144                    "loc-args": ["narf", "derp"],
1145                    "loc-key": "PAUSE",
1146                    "title": "the title",
1147                    "title-loc-args": ["herp", "derp"],
1148                    "title-loc-key": "STOP"
1149                },
1150                "badge": 420,
1151                "sound": {
1152                    "critical": 1,
1153                    "name": "prööt",
1154                    "volume": 1.0,
1155                },
1156                "category": "cat1",
1157                "mutable-content": 1,
1158            }
1159        });
1160
1161        assert_eq!(expected_payload, to_value(payload).unwrap());
1162    }
1163
1164    #[test]
1165    fn test_notification_with_custom_data_1() {
1166        #[derive(Serialize, Debug)]
1167        struct SubData {
1168            nothing: &'static str,
1169        }
1170
1171        #[derive(Serialize, Debug)]
1172        struct TestData {
1173            key_str: &'static str,
1174            key_num: u32,
1175            key_bool: bool,
1176            key_struct: SubData,
1177        }
1178
1179        let test_data = TestData {
1180            key_str: "foo",
1181            key_num: 42,
1182            key_bool: false,
1183            key_struct: SubData { nothing: "here" },
1184        };
1185
1186        let mut payload = DefaultNotificationBuilder::new()
1187            .title("the title")
1188            .body("the body")
1189            .build("device-token", Default::default());
1190
1191        payload.add_custom_data("custom", &test_data).unwrap();
1192
1193        let expected_payload = json!({
1194            "custom": {
1195                "key_str": "foo",
1196                "key_num": 42,
1197                "key_bool": false,
1198                "key_struct": {
1199                    "nothing": "here"
1200                }
1201            },
1202            "aps": {
1203                "alert": {
1204                    "body": "the body",
1205                    "title": "the title",
1206                },
1207                "mutable-content": 0,
1208            },
1209        });
1210
1211        assert_eq!(expected_payload, to_value(payload).unwrap());
1212    }
1213
1214    #[test]
1215    fn test_notification_with_custom_data_2() {
1216        #[derive(Serialize, Debug)]
1217        struct SubData {
1218            nothing: &'static str,
1219        }
1220
1221        #[derive(Serialize, Debug)]
1222        struct TestData {
1223            key_str: &'static str,
1224            key_num: u32,
1225            key_bool: bool,
1226            key_struct: SubData,
1227        }
1228
1229        let test_data = TestData {
1230            key_str: "foo",
1231            key_num: 42,
1232            key_bool: false,
1233            key_struct: SubData { nothing: "here" },
1234        };
1235
1236        let mut payload = DefaultNotificationBuilder::new()
1237            .body("kulli")
1238            .build("device-token", Default::default());
1239
1240        payload.add_custom_data("custom", &test_data).unwrap();
1241
1242        let expected_payload = json!({
1243            "custom": {
1244                "key_str": "foo",
1245                "key_num": 42,
1246                "key_bool": false,
1247                "key_struct": {
1248                    "nothing": "here"
1249                }
1250            },
1251            "aps": {
1252                "alert": {
1253                    "body": "kulli"
1254                },
1255                "mutable-content": 0
1256            }
1257        });
1258
1259        assert_eq!(expected_payload, to_value(payload).unwrap());
1260    }
1261
1262    #[test]
1263    fn test_silent_notification_with_no_content() {
1264        let payload = DefaultNotificationBuilder::new()
1265            .content_available()
1266            .build("device-token", Default::default());
1267
1268        let expected_payload = json!({
1269            "aps": {
1270                "content-available": 1,
1271                "mutable-content": 0
1272            }
1273        });
1274
1275        assert_eq!(expected_payload, to_value(payload).unwrap());
1276    }
1277
1278    #[test]
1279    fn test_silent_notification_with_custom_data() {
1280        #[derive(Serialize, Debug)]
1281        struct SubData {
1282            nothing: &'static str,
1283        }
1284
1285        #[derive(Serialize, Debug)]
1286        struct TestData {
1287            key_str: &'static str,
1288            key_num: u32,
1289            key_bool: bool,
1290            key_struct: SubData,
1291        }
1292
1293        let test_data = TestData {
1294            key_str: "foo",
1295            key_num: 42,
1296            key_bool: false,
1297            key_struct: SubData { nothing: "here" },
1298        };
1299
1300        let mut payload = DefaultNotificationBuilder::new()
1301            .content_available()
1302            .build("device-token", Default::default());
1303
1304        payload.add_custom_data("custom", &test_data).unwrap();
1305
1306        let expected_payload = json!({
1307            "aps": {
1308                "content-available": 1,
1309                "mutable-content": 0
1310            },
1311            "custom": {
1312                "key_str": "foo",
1313                "key_num": 42,
1314                "key_bool": false,
1315                "key_struct": {
1316                    "nothing": "here"
1317                }
1318            }
1319        });
1320
1321        assert_eq!(expected_payload, to_value(payload).unwrap());
1322    }
1323
1324    #[test]
1325    fn test_silent_notification_with_custom_hashmap() {
1326        let mut test_data = BTreeMap::new();
1327        test_data.insert("key_str", "foo");
1328        test_data.insert("key_str2", "bar");
1329
1330        let mut payload = DefaultNotificationBuilder::new()
1331            .content_available()
1332            .build("device-token", Default::default());
1333
1334        payload.add_custom_data("custom", &test_data).unwrap();
1335
1336        let expected_payload = json!({
1337            "aps": {
1338                "content-available": 1,
1339                "mutable-content": 0,
1340            },
1341            "custom": {
1342                "key_str": "foo",
1343                "key_str2": "bar"
1344            }
1345        });
1346
1347        assert_eq!(expected_payload, to_value(payload).unwrap());
1348    }
1349}