flare_core/common/
device.rs1use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum DevicePlatform {
11 Web,
13 PC,
15 H5,
17 Android,
19 IOS,
21 HarmonyOS,
23 Other(String),
25}
26
27impl DevicePlatform {
28 #[allow(clippy::should_implement_trait)]
30 pub fn from_str(s: &str) -> Self {
31 match s {
32 s if s.eq_ignore_ascii_case("web") => DevicePlatform::Web,
33 s if s.eq_ignore_ascii_case("pc") || s.eq_ignore_ascii_case("desktop") => {
34 DevicePlatform::PC
35 }
36 s if s.eq_ignore_ascii_case("h5") || s.eq_ignore_ascii_case("mobile_web") => {
37 DevicePlatform::H5
38 }
39 s if s.eq_ignore_ascii_case("android") => DevicePlatform::Android,
40 s if s.eq_ignore_ascii_case("ios")
41 || s.eq_ignore_ascii_case("iphone")
42 || s.eq_ignore_ascii_case("ipad") =>
43 {
44 DevicePlatform::IOS
45 }
46 s if s.eq_ignore_ascii_case("harmonyos")
47 || s.eq_ignore_ascii_case("harmony")
48 || s.eq_ignore_ascii_case("openharmony") =>
49 {
50 DevicePlatform::HarmonyOS
51 }
52 other => DevicePlatform::Other(other.to_string()),
53 }
54 }
55
56 pub fn as_str(&self) -> &str {
58 match self {
59 DevicePlatform::Web => "web",
60 DevicePlatform::PC => "pc",
61 DevicePlatform::H5 => "h5",
62 DevicePlatform::Android => "android",
63 DevicePlatform::IOS => "ios",
64 DevicePlatform::HarmonyOS => "harmonyos",
65 DevicePlatform::Other(s) => s,
66 }
67 }
68
69 pub fn is_mobile(&self) -> bool {
71 matches!(
72 self,
73 DevicePlatform::H5
74 | DevicePlatform::Android
75 | DevicePlatform::IOS
76 | DevicePlatform::HarmonyOS
77 )
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct DeviceInfo {
84 pub device_id: String,
86 pub platform: DevicePlatform,
88 pub model: Option<String>,
90 pub app_version: Option<String>,
92 pub system_version: Option<String>,
94 pub metadata: std::collections::HashMap<String, String>,
96}
97
98impl DeviceInfo {
99 pub fn new(device_id: String, platform: DevicePlatform) -> Self {
101 Self {
102 device_id,
103 platform,
104 model: None,
105 app_version: None,
106 system_version: None,
107 metadata: std::collections::HashMap::new(),
108 }
109 }
110
111 pub fn with_model(mut self, model: String) -> Self {
113 self.model = Some(model);
114 self
115 }
116
117 pub fn with_app_version(mut self, version: String) -> Self {
119 self.app_version = Some(version);
120 self
121 }
122
123 pub fn with_system_version(mut self, version: String) -> Self {
125 self.system_version = Some(version);
126 self
127 }
128
129 pub fn with_metadata(mut self, key: String, value: String) -> Self {
131 self.metadata.insert(key, value);
132 self
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
138pub enum DeviceConflictStrategy {
139 #[default]
141 AllowAll,
142 MobileExclusive,
145 PlatformExclusive,
148 FullyExclusive,
150 MobileAndPcCoexist,
153 Custom {
155 allowed_combinations: Vec<HashSet<DevicePlatform>>,
158 exclusive_groups: Vec<HashSet<DevicePlatform>>,
161 },
162}
163
164impl DeviceConflictStrategy {
165 pub fn check_conflict(
175 &self,
176 new_device: DevicePlatform,
177 existing_devices: &HashSet<DevicePlatform>,
178 ) -> Result<(), Vec<DevicePlatform>> {
179 match self {
180 DeviceConflictStrategy::AllowAll => Ok(()),
181
182 DeviceConflictStrategy::MobileExclusive => {
183 if new_device.is_mobile() {
184 let mobile_conflicts = self.get_mobile_devices(existing_devices);
185 if !mobile_conflicts.is_empty() {
186 return Err(mobile_conflicts);
187 }
188 }
189 Ok(())
190 }
191
192 DeviceConflictStrategy::PlatformExclusive => {
193 if existing_devices.contains(&new_device) {
194 return Err(vec![new_device]);
195 }
196 Ok(())
197 }
198
199 DeviceConflictStrategy::FullyExclusive => {
200 if !existing_devices.is_empty() {
201 return Err(existing_devices.iter().cloned().collect());
202 }
203 Ok(())
204 }
205
206 DeviceConflictStrategy::MobileAndPcCoexist => {
207 if new_device.is_mobile() {
209 let mobile_conflicts = self.get_mobile_devices(existing_devices);
210 if !mobile_conflicts.is_empty() {
211 return Err(mobile_conflicts);
212 }
213 } else if matches!(new_device, DevicePlatform::Web | DevicePlatform::PC) {
214 let pc_conflicts: Vec<DevicePlatform> = existing_devices
215 .iter()
216 .filter(|p| matches!(p, DevicePlatform::Web | DevicePlatform::PC))
217 .cloned()
218 .collect();
219 if !pc_conflicts.is_empty() {
220 return Err(pc_conflicts);
221 }
222 } else {
223 if existing_devices.contains(&new_device) {
225 return Err(vec![new_device]);
226 }
227 }
228 Ok(())
229 }
230
231 DeviceConflictStrategy::Custom {
232 allowed_combinations,
233 exclusive_groups,
234 } => {
235 for group in exclusive_groups {
237 if group.contains(&new_device) {
238 let conflicts: Vec<DevicePlatform> = existing_devices
239 .iter()
240 .filter(|p| group.contains(p))
241 .cloned()
242 .collect();
243 if !conflicts.is_empty() {
244 return Err(conflicts);
245 }
246 }
247 }
248
249 let mut combined = existing_devices.clone();
251 combined.insert(new_device.clone());
252
253 if combined.len() > 1 {
254 let is_allowed = allowed_combinations
255 .iter()
256 .any(|allowed| combined.is_subset(allowed));
257
258 if !is_allowed {
259 return Err(existing_devices.iter().cloned().collect());
260 }
261 }
262
263 Ok(())
264 }
265 }
266 }
267
268 fn get_mobile_devices(&self, devices: &HashSet<DevicePlatform>) -> Vec<DevicePlatform> {
270 devices.iter().filter(|p| p.is_mobile()).cloned().collect()
271 }
272}
273
274pub struct DeviceConflictStrategyBuilder {
276 strategy: DeviceConflictStrategy,
277}
278
279impl DeviceConflictStrategyBuilder {
280 pub fn new() -> Self {
282 Self {
283 strategy: DeviceConflictStrategy::AllowAll,
284 }
285 }
286
287 pub fn mobile_exclusive(mut self) -> Self {
289 self.strategy = DeviceConflictStrategy::MobileExclusive;
290 self
291 }
292
293 pub fn platform_exclusive(mut self) -> Self {
295 self.strategy = DeviceConflictStrategy::PlatformExclusive;
296 self
297 }
298
299 pub fn fully_exclusive(mut self) -> Self {
301 self.strategy = DeviceConflictStrategy::FullyExclusive;
302 self
303 }
304
305 pub fn mobile_and_pc_coexist(mut self) -> Self {
310 self.strategy = DeviceConflictStrategy::MobileAndPcCoexist;
311 self
312 }
313
314 pub fn custom(
316 mut self,
317 allowed_combinations: Vec<HashSet<DevicePlatform>>,
318 exclusive_groups: Vec<HashSet<DevicePlatform>>,
319 ) -> Self {
320 self.strategy = DeviceConflictStrategy::Custom {
321 allowed_combinations,
322 exclusive_groups,
323 };
324 self
325 }
326
327 pub fn build(self) -> DeviceConflictStrategy {
329 self.strategy
330 }
331}
332
333impl Default for DeviceConflictStrategyBuilder {
334 fn default() -> Self {
335 Self::new()
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn test_mobile_exclusive() {
345 let strategy = DeviceConflictStrategy::MobileExclusive;
346 let mut existing = HashSet::new();
347 existing.insert(DevicePlatform::Android);
348
349 assert!(
351 strategy
352 .check_conflict(DevicePlatform::IOS, &existing)
353 .is_err()
354 );
355
356 assert!(
358 strategy
359 .check_conflict(DevicePlatform::PC, &existing)
360 .is_ok()
361 );
362 }
363
364 #[test]
365 fn test_platform_exclusive() {
366 let strategy = DeviceConflictStrategy::PlatformExclusive;
367 let mut existing = HashSet::new();
368 existing.insert(DevicePlatform::Web);
369
370 assert!(
372 strategy
373 .check_conflict(DevicePlatform::Web, &existing)
374 .is_err()
375 );
376
377 assert!(
379 strategy
380 .check_conflict(DevicePlatform::PC, &existing)
381 .is_ok()
382 );
383 }
384}