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,
        ));
    }
}