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}