sync15/
device_type.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5//! This type is strictly owned by FxA, but is defined in this crate because of
6//! some hard-to-avoid hacks done for the tabs engine... See issue #2590.
7//!
8//! Thus, fxa-client ends up taking a dep on this crate, which is roughly
9//! the opposite of reality.
10
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13/// Enumeration for the different types of device.
14///
15/// Firefox Accounts and the broader Sync universe separates devices into broad categories for
16/// various purposes, such as distinguishing a desktop PC from a mobile phone.
17/// Upon signin, the application should inspect the device it is running on and select an appropriate
18/// [`DeviceType`] to include in its device registration record.
19///
20/// A special variant in this enum, [`DeviceType::Unknown`] is used to capture
21/// the string values we don't recognise. It also has a custom serde serializer and deserializer
22/// which implements the following semantics:
23/// * deserializing a `DeviceType` which uses a string value we don't recognise or null will return
24///   `DeviceType::Unknown` rather than returning an error.
25/// * serializing `DeviceType::Unknown` will serialize `null`.
26///
27/// This has a few important implications:
28/// * In general, `Option<DeviceType>` should be avoided, and a plain `DeviceType` used instead,
29///   because in that case, `None` would be semantically identical to `DeviceType::Unknown` and
30///   as mentioned above, `null` already deserializes as `DeviceType::Unknown`.
31/// * Any unknown device types can not be round-tripped via this enum - eg, if you deserialize
32///   a struct holding a `DeviceType` string value we don't recognize, then re-serialize it, the
33///   original string value is lost. We don't consider this a problem because in practice, we only
34///   upload records with *this* device's type, not the type of other devices, and it's reasonable
35///   to assume that this module knows about all valid device types for the device type it is
36///   deployed on.
37#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, uniffi::Enum)]
38pub enum DeviceType {
39    Desktop,
40    Mobile,
41    Tablet,
42    VR,
43    TV,
44    // See docstrings above re how Unknown is serialized and deserialized.
45    #[default]
46    Unknown,
47}
48
49impl<'de> Deserialize<'de> for DeviceType {
50    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51    where
52        D: Deserializer<'de>,
53    {
54        Ok(match String::deserialize(deserializer) {
55            Ok(s) => match s.as_str() {
56                "desktop" => DeviceType::Desktop,
57                "mobile" => DeviceType::Mobile,
58                "tablet" => DeviceType::Tablet,
59                "vr" => DeviceType::VR,
60                "tv" => DeviceType::TV,
61                // There's a vague possibility that desktop might serialize "phone" for mobile
62                // devices - https://searchfox.org/mozilla-central/rev/a156a65ced2dae5913ae35a68e9445b8ee7ca457/services/sync/modules/engines/clients.js#292
63                "phone" => DeviceType::Mobile,
64                // Everything else is Unknown.
65                _ => DeviceType::Unknown,
66            },
67            // Anything other than a string is "unknown" - this isn't ideal - we really only want
68            // to handle null and, eg, a number probably should be an error, but meh.
69            Err(_) => DeviceType::Unknown,
70        })
71    }
72}
73impl Serialize for DeviceType {
74    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
75    where
76        S: Serializer,
77    {
78        match self {
79            // It's unfortunate we need to duplicate the strings here...
80            DeviceType::Desktop => s.serialize_unit_variant("DeviceType", 0, "desktop"),
81            DeviceType::Mobile => s.serialize_unit_variant("DeviceType", 1, "mobile"),
82            DeviceType::Tablet => s.serialize_unit_variant("DeviceType", 2, "tablet"),
83            DeviceType::VR => s.serialize_unit_variant("DeviceType", 3, "vr"),
84            DeviceType::TV => s.serialize_unit_variant("DeviceType", 4, "tv"),
85            // This is the important bit - Unknown -> None
86            DeviceType::Unknown => s.serialize_none(),
87        }
88    }
89}
90
91#[cfg(test)]
92mod device_type_tests {
93    use super::*;
94
95    #[test]
96    fn test_serde_ser() {
97        assert_eq!(
98            serde_json::to_string(&DeviceType::Desktop).unwrap(),
99            "\"desktop\""
100        );
101        assert_eq!(
102            serde_json::to_string(&DeviceType::Mobile).unwrap(),
103            "\"mobile\""
104        );
105        assert_eq!(
106            serde_json::to_string(&DeviceType::Tablet).unwrap(),
107            "\"tablet\""
108        );
109        assert_eq!(serde_json::to_string(&DeviceType::VR).unwrap(), "\"vr\"");
110        assert_eq!(serde_json::to_string(&DeviceType::TV).unwrap(), "\"tv\"");
111        assert_eq!(serde_json::to_string(&DeviceType::Unknown).unwrap(), "null");
112    }
113
114    #[test]
115    fn test_serde_de() {
116        assert!(matches!(
117            serde_json::from_str::<DeviceType>("\"desktop\"").unwrap(),
118            DeviceType::Desktop
119        ));
120        assert!(matches!(
121            serde_json::from_str::<DeviceType>("\"mobile\"").unwrap(),
122            DeviceType::Mobile
123        ));
124        assert!(matches!(
125            serde_json::from_str::<DeviceType>("\"tablet\"").unwrap(),
126            DeviceType::Tablet
127        ));
128        assert!(matches!(
129            serde_json::from_str::<DeviceType>("\"vr\"").unwrap(),
130            DeviceType::VR
131        ));
132        assert!(matches!(
133            serde_json::from_str::<DeviceType>("\"tv\"").unwrap(),
134            DeviceType::TV
135        ));
136        assert!(matches!(
137            serde_json::from_str::<DeviceType>("\"something-else\"").unwrap(),
138            DeviceType::Unknown,
139        ));
140        assert!(matches!(
141            serde_json::from_str::<DeviceType>("null").unwrap(),
142            DeviceType::Unknown,
143        ));
144        assert!(matches!(
145            serde_json::from_str::<DeviceType>("99").unwrap(),
146            DeviceType::Unknown,
147        ));
148    }
149}