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}