expo_push_notification_client 2.1.0

Expo Push Notification Client for Rust
Documentation
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::skip_serializing_none;

// <https://docs.expo.dev/push-notifications/sending-notifications/#message-request-format>
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CriticalSound {
    pub critical: Option<bool>,
    pub name: Option<String>,
    pub volume: Option<f64>,
}

impl CriticalSound {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn critical(mut self, critical: bool) -> Self {
        self.critical = Some(critical);
        self
    }

    pub fn name<S>(mut self, name: S) -> Self
    where
        S: Into<String>,
    {
        self.name = Some(name.into());
        self
    }

    pub fn volume(mut self, volume: f64) -> Self {
        self.volume = Some(volume);
        self
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum Sound {
    Default,
    Custom(String),
    Critical(CriticalSound),
}

impl Serialize for Sound {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Sound::Default => serializer.serialize_str("default"),
            Sound::Custom(s) => serializer.serialize_str(s),
            Sound::Critical(sound) => sound.serialize(serializer),
        }
    }
}

impl<'de> Deserialize<'de> for Sound {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum Repr {
            Name(String),
            Critical(CriticalSound),
        }

        match Repr::deserialize(deserializer)? {
            Repr::Name(s) if s == "default" => Ok(Sound::Default),
            Repr::Name(s) => Ok(Sound::Custom(s)),
            Repr::Critical(sound) => Ok(Sound::Critical(sound)),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_serialization() -> Result<(), serde_json::Error> {
        assert_eq!(serde_json::to_string(&Sound::Default)?, "\"default\"");
        assert_eq!(
            serde_json::to_string(&Sound::Custom("bells.wav".to_string()))?,
            "\"bells.wav\""
        );
        assert_eq!(
            serde_json::to_string(&Sound::Custom("custom_sound.mp3".to_string()))?,
            "\"custom_sound.mp3\""
        );
        Ok(())
    }

    #[test]
    fn test_deserialization() -> Result<(), serde_json::Error> {
        assert_eq!(
            serde_json::from_str::<Sound>("\"default\"")?,
            Sound::Default
        );
        assert_eq!(
            serde_json::from_str::<Sound>("\"bells.wav\"")?,
            Sound::Custom("bells.wav".to_string())
        );
        assert_eq!(
            serde_json::from_str::<Sound>("\"custom_sound.mp3\"")?,
            Sound::Custom("custom_sound.mp3".to_string())
        );
        Ok(())
    }

    #[test]
    fn test_critical_sound_serialization() -> Result<(), serde_json::Error> {
        let sound = Sound::Critical(
            CriticalSound::new()
                .critical(true)
                .name("bells.wav")
                .volume(0.5),
        );
        assert_eq!(
            serde_json::to_value(&sound)?,
            serde_json::json!({ "critical": true, "name": "bells.wav", "volume": 0.5 })
        );
        Ok(())
    }

    #[test]
    fn test_critical_sound_serialization_skips_unset_fields() -> Result<(), serde_json::Error> {
        let sound = Sound::Critical(CriticalSound::new().critical(true));
        assert_eq!(
            serde_json::to_value(&sound)?,
            serde_json::json!({ "critical": true })
        );
        assert_eq!(
            serde_json::to_value(Sound::Critical(CriticalSound::new()))?,
            serde_json::json!({})
        );
        Ok(())
    }

    #[test]
    fn test_critical_sound_deserialization() -> Result<(), serde_json::Error> {
        assert_eq!(
            serde_json::from_str::<Sound>(r#"{"critical":true,"name":"bells.wav","volume":0.5}"#)?,
            Sound::Critical(
                CriticalSound::new()
                    .critical(true)
                    .name("bells.wav")
                    .volume(0.5)
            )
        );
        assert_eq!(
            serde_json::from_str::<Sound>("{}")?,
            Sound::Critical(CriticalSound::new())
        );
        Ok(())
    }

    #[test]
    fn test_unknown_sound_object_is_rejected() {
        // Without `deny_unknown_fields` every field of `CriticalSound` is optional, so the
        // untagged representation would match any object and silently discard its contents.
        assert!(serde_json::from_str::<Sound>(r#"{"unknown":1}"#).is_err());
        assert!(serde_json::from_str::<Sound>(r#"{"critical":true,"typo":1}"#).is_err());
    }
}