use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DevicePlatform {
Web,
PC,
H5,
Android,
IOS,
HarmonyOS,
Other(String),
}
impl DevicePlatform {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self {
match s {
s if s.eq_ignore_ascii_case("web") => DevicePlatform::Web,
s if s.eq_ignore_ascii_case("pc") || s.eq_ignore_ascii_case("desktop") => {
DevicePlatform::PC
}
s if s.eq_ignore_ascii_case("h5") || s.eq_ignore_ascii_case("mobile_web") => {
DevicePlatform::H5
}
s if s.eq_ignore_ascii_case("android") => DevicePlatform::Android,
s if s.eq_ignore_ascii_case("ios")
|| s.eq_ignore_ascii_case("iphone")
|| s.eq_ignore_ascii_case("ipad") =>
{
DevicePlatform::IOS
}
s if s.eq_ignore_ascii_case("harmonyos")
|| s.eq_ignore_ascii_case("harmony")
|| s.eq_ignore_ascii_case("openharmony") =>
{
DevicePlatform::HarmonyOS
}
other => DevicePlatform::Other(other.to_string()),
}
}
pub fn as_str(&self) -> &str {
match self {
DevicePlatform::Web => "web",
DevicePlatform::PC => "pc",
DevicePlatform::H5 => "h5",
DevicePlatform::Android => "android",
DevicePlatform::IOS => "ios",
DevicePlatform::HarmonyOS => "harmonyos",
DevicePlatform::Other(s) => s,
}
}
pub fn is_mobile(&self) -> bool {
matches!(
self,
DevicePlatform::H5
| DevicePlatform::Android
| DevicePlatform::IOS
| DevicePlatform::HarmonyOS
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceInfo {
pub device_id: String,
pub platform: DevicePlatform,
pub model: Option<String>,
pub app_version: Option<String>,
pub system_version: Option<String>,
pub metadata: std::collections::HashMap<String, String>,
}
impl DeviceInfo {
pub fn new(device_id: String, platform: DevicePlatform) -> Self {
Self {
device_id,
platform,
model: None,
app_version: None,
system_version: None,
metadata: std::collections::HashMap::new(),
}
}
pub fn with_model(mut self, model: String) -> Self {
self.model = Some(model);
self
}
pub fn with_app_version(mut self, version: String) -> Self {
self.app_version = Some(version);
self
}
pub fn with_system_version(mut self, version: String) -> Self {
self.system_version = Some(version);
self
}
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum DeviceConflictStrategy {
#[default]
AllowAll,
MobileExclusive,
PlatformExclusive,
FullyExclusive,
MobileAndPcCoexist,
Custom {
allowed_combinations: Vec<HashSet<DevicePlatform>>,
exclusive_groups: Vec<HashSet<DevicePlatform>>,
},
}
impl DeviceConflictStrategy {
pub fn check_conflict(
&self,
new_device: DevicePlatform,
existing_devices: &HashSet<DevicePlatform>,
) -> Result<(), Vec<DevicePlatform>> {
match self {
DeviceConflictStrategy::AllowAll => Ok(()),
DeviceConflictStrategy::MobileExclusive => {
if new_device.is_mobile() {
let mobile_conflicts = self.get_mobile_devices(existing_devices);
if !mobile_conflicts.is_empty() {
return Err(mobile_conflicts);
}
}
Ok(())
}
DeviceConflictStrategy::PlatformExclusive => {
if existing_devices.contains(&new_device) {
return Err(vec![new_device]);
}
Ok(())
}
DeviceConflictStrategy::FullyExclusive => {
if !existing_devices.is_empty() {
return Err(existing_devices.iter().cloned().collect());
}
Ok(())
}
DeviceConflictStrategy::MobileAndPcCoexist => {
if new_device.is_mobile() {
let mobile_conflicts = self.get_mobile_devices(existing_devices);
if !mobile_conflicts.is_empty() {
return Err(mobile_conflicts);
}
} else if matches!(new_device, DevicePlatform::Web | DevicePlatform::PC) {
let pc_conflicts: Vec<DevicePlatform> = existing_devices
.iter()
.filter(|p| matches!(p, DevicePlatform::Web | DevicePlatform::PC))
.cloned()
.collect();
if !pc_conflicts.is_empty() {
return Err(pc_conflicts);
}
} else {
if existing_devices.contains(&new_device) {
return Err(vec![new_device]);
}
}
Ok(())
}
DeviceConflictStrategy::Custom {
allowed_combinations,
exclusive_groups,
} => {
for group in exclusive_groups {
if group.contains(&new_device) {
let conflicts: Vec<DevicePlatform> = existing_devices
.iter()
.filter(|p| group.contains(p))
.cloned()
.collect();
if !conflicts.is_empty() {
return Err(conflicts);
}
}
}
let mut combined = existing_devices.clone();
combined.insert(new_device.clone());
if combined.len() > 1 {
let is_allowed = allowed_combinations
.iter()
.any(|allowed| combined.is_subset(allowed));
if !is_allowed {
return Err(existing_devices.iter().cloned().collect());
}
}
Ok(())
}
}
}
fn get_mobile_devices(&self, devices: &HashSet<DevicePlatform>) -> Vec<DevicePlatform> {
devices.iter().filter(|p| p.is_mobile()).cloned().collect()
}
}
pub struct DeviceConflictStrategyBuilder {
strategy: DeviceConflictStrategy,
}
impl DeviceConflictStrategyBuilder {
pub fn new() -> Self {
Self {
strategy: DeviceConflictStrategy::AllowAll,
}
}
pub fn mobile_exclusive(mut self) -> Self {
self.strategy = DeviceConflictStrategy::MobileExclusive;
self
}
pub fn platform_exclusive(mut self) -> Self {
self.strategy = DeviceConflictStrategy::PlatformExclusive;
self
}
pub fn fully_exclusive(mut self) -> Self {
self.strategy = DeviceConflictStrategy::FullyExclusive;
self
}
pub fn mobile_and_pc_coexist(mut self) -> Self {
self.strategy = DeviceConflictStrategy::MobileAndPcCoexist;
self
}
pub fn custom(
mut self,
allowed_combinations: Vec<HashSet<DevicePlatform>>,
exclusive_groups: Vec<HashSet<DevicePlatform>>,
) -> Self {
self.strategy = DeviceConflictStrategy::Custom {
allowed_combinations,
exclusive_groups,
};
self
}
pub fn build(self) -> DeviceConflictStrategy {
self.strategy
}
}
impl Default for DeviceConflictStrategyBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mobile_exclusive() {
let strategy = DeviceConflictStrategy::MobileExclusive;
let mut existing = HashSet::new();
existing.insert(DevicePlatform::Android);
assert!(
strategy
.check_conflict(DevicePlatform::IOS, &existing)
.is_err()
);
assert!(
strategy
.check_conflict(DevicePlatform::PC, &existing)
.is_ok()
);
}
#[test]
fn test_platform_exclusive() {
let strategy = DeviceConflictStrategy::PlatformExclusive;
let mut existing = HashSet::new();
existing.insert(DevicePlatform::Web);
assert!(
strategy
.check_conflict(DevicePlatform::Web, &existing)
.is_err()
);
assert!(
strategy
.check_conflict(DevicePlatform::PC, &existing)
.is_ok()
);
}
}