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///
18/// A special variant in this enum, `DeviceType::Unknown` is used to capture
19/// the string values we don't recognise. It also has a custom serde serializer and deserializer
20/// which implements the following semantics:
21/// * deserializing a `DeviceType` which uses a string value we don't recognise or null will return
22///   `DeviceType::Unknown` rather than returning an error.
23/// * serializing `DeviceType::Unknown` will serialize `null`.
24///
25/// This has a few important implications:
26/// * In general, `Option<DeviceType>` should be avoided, and a plain `DeviceType` used instead,
27///   because in that case, `None` would be semantically identical to `DeviceType::Unknown` and
28///   as mentioned above, `null` already deserializes as `DeviceType::Unknown`.
29/// * Any unknown device types can not be round-tripped via this enum - eg, if you deserialize
30///   a struct holding a `DeviceType` string value we don't recognize, then re-serialize it, the
31///   original string value is lost. We don't consider this a problem because in practice, we only
32///   upload records with *this* device's type, not the type of other devices, and it's reasonable
33///   to assume that this module knows about all valid device types for the device type it is
34///   deployed on.
35#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
36pub enum DeviceType {
37    Desktop,
38    Mobile,
39    Tablet,
40    VR,
41    TV,
42    // See docstrings above re how Unknown is serialized and deserialized.
43    #[default]
44    Unknown,
45}
46
47impl<'de> Deserialize<'de> for DeviceType {
48    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
49    where
50        D: Deserializer<'de>,
51    {
52        Ok(match String::deserialize(deserializer) {
53            Ok(s) => match s.as_str() {
54                "desktop" => DeviceType::Desktop,
55                "mobile" => DeviceType::Mobile,
56                "tablet" => DeviceType::Tablet,
57                "vr" => DeviceType::VR,
58                "tv" => DeviceType::TV,
59                // There's a vague possibility that desktop might serialize "phone" for mobile
60                // devices - https://searchfox.org/mozilla-central/rev/a156a65ced2dae5913ae35a68e9445b8ee7ca457/services/sync/modules/engines/clients.js#292
61                "phone" => DeviceType::Mobile,
62                // Everything else is Unknown.
63                _ => DeviceType::Unknown,
64            },
65            // Anything other than a string is "unknown" - this isn't ideal - we really only want
66            // to handle null and, eg, a number probably should be an error, but meh.
67            Err(_) => DeviceType::Unknown,
68        })
69    }
70}
71impl Serialize for DeviceType {
72    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
73    where
74        S: Serializer,
75    {
76        match self {
77            // It's unfortunate we need to duplicate the strings here...
78            DeviceType::Desktop => s.serialize_unit_variant("DeviceType", 0, "desktop"),
79            DeviceType::Mobile => s.serialize_unit_variant("DeviceType", 1, "mobile"),
80            DeviceType::Tablet => s.serialize_unit_variant("DeviceType", 2, "tablet"),
81            DeviceType::VR => s.serialize_unit_variant("DeviceType", 3, "vr"),
82            DeviceType::TV => s.serialize_unit_variant("DeviceType", 4, "tv"),
83            // This is the important bit - Unknown -> None
84            DeviceType::Unknown => s.serialize_none(),
85        }
86    }
87}
88
89#[cfg(test)]
90mod device_type_tests {
91    use super::*;
92
93    #[test]
94    fn test_serde_ser() {
95        assert_eq!(
96            serde_json::to_string(&DeviceType::Desktop).unwrap(),
97            "\"desktop\""
98        );
99        assert_eq!(
100            serde_json::to_string(&DeviceType::Mobile).unwrap(),
101            "\"mobile\""
102        );
103        assert_eq!(
104            serde_json::to_string(&DeviceType::Tablet).unwrap(),
105            "\"tablet\""
106        );
107        assert_eq!(serde_json::to_string(&DeviceType::VR).unwrap(), "\"vr\"");
108        assert_eq!(serde_json::to_string(&DeviceType::TV).unwrap(), "\"tv\"");
109        assert_eq!(serde_json::to_string(&DeviceType::Unknown).unwrap(), "null");
110    }
111
112    #[test]
113    fn test_serde_de() {
114        assert!(matches!(
115            serde_json::from_str::<DeviceType>("\"desktop\"").unwrap(),
116            DeviceType::Desktop
117        ));
118        assert!(matches!(
119            serde_json::from_str::<DeviceType>("\"mobile\"").unwrap(),
120            DeviceType::Mobile
121        ));
122        assert!(matches!(
123            serde_json::from_str::<DeviceType>("\"tablet\"").unwrap(),
124            DeviceType::Tablet
125        ));
126        assert!(matches!(
127            serde_json::from_str::<DeviceType>("\"vr\"").unwrap(),
128            DeviceType::VR
129        ));
130        assert!(matches!(
131            serde_json::from_str::<DeviceType>("\"tv\"").unwrap(),
132            DeviceType::TV
133        ));
134        assert!(matches!(
135            serde_json::from_str::<DeviceType>("\"something-else\"").unwrap(),
136            DeviceType::Unknown,
137        ));
138        assert!(matches!(
139            serde_json::from_str::<DeviceType>("null").unwrap(),
140            DeviceType::Unknown,
141        ));
142        assert!(matches!(
143            serde_json::from_str::<DeviceType>("99").unwrap(),
144            DeviceType::Unknown,
145        ));
146    }
147}