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