use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct PwaFeaturesManager {
config: PwaConfig,
stats: Arc<RwLock<PwaStats>>,
app_manifest: Arc<RwLock<AppManifest>>,
installation_manager: Arc<RwLock<InstallationManager>>,
offline_capabilities: Arc<RwLock<OfflineCapabilities>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PwaConfig {
pub enable_manifest: bool,
pub enable_installation_prompt: bool,
pub enable_offline_mode: bool,
pub enable_background_sync: bool,
pub enable_push_notifications: bool,
pub enable_share_target: bool,
pub enable_file_handling: bool,
pub app_name: String,
pub app_short_name: String,
pub app_description: String,
pub app_version: String,
pub app_theme_color: String,
pub app_background_color: String,
pub app_display_mode: AppDisplayMode,
pub app_orientation: AppOrientation,
pub app_scope: String,
pub app_start_url: String,
pub app_icons: Vec<AppIcon>,
pub app_categories: Vec<String>,
pub app_screenshots: Vec<AppScreenshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AppDisplayMode {
Fullscreen,
Standalone,
MinimalUi,
Browser,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AppOrientation {
Any,
Portrait,
Landscape,
PortraitPrimary,
PortraitSecondary,
LandscapePrimary,
LandscapeSecondary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppIcon {
pub src: String,
pub sizes: String,
pub icon_type: String,
pub purpose: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppScreenshot {
pub src: String,
pub sizes: String,
pub screenshot_type: String,
pub form_factor: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppManifest {
pub name: String,
pub short_name: String,
pub description: String,
pub version: String,
pub theme_color: String,
pub background_color: String,
pub display: AppDisplayMode,
pub orientation: AppOrientation,
pub scope: String,
pub start_url: String,
pub icons: Vec<AppIcon>,
pub categories: Vec<String>,
pub screenshots: Vec<AppScreenshot>,
pub shortcuts: Vec<AppShortcut>,
pub related_applications: Vec<RelatedApplication>,
pub prefer_related_applications: bool,
pub lang: String,
pub dir: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppShortcut {
pub name: String,
pub short_name: String,
pub description: String,
pub url: String,
pub icons: Vec<AppIcon>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelatedApplication {
pub platform: String,
pub url: String,
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstallationManager {
pub installation_prompt_shown: bool,
pub installation_prompt_deferred: bool,
pub installation_prompt_accepted: bool,
pub installation_prompt_dismissed: bool,
pub installation_events: Vec<InstallationEvent>,
pub installation_stats: InstallationStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstallationEvent {
pub event_type: InstallationEventType,
pub timestamp: std::time::SystemTime,
pub user_agent: String,
pub platform: String,
pub success: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum InstallationEventType {
PromptShown,
PromptAccepted,
PromptDismissed,
PromptDeferred,
AppInstalled,
AppUninstalled,
AppUpdated,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InstallationStats {
pub total_prompts_shown: u64,
pub total_installations: u64,
pub total_uninstallations: u64,
pub installation_rate: f64,
pub prompt_acceptance_rate: f64,
pub average_installation_time: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OfflineCapabilities {
pub offline_enabled: bool,
pub offline_pages: Vec<OfflinePage>,
pub offline_resources: Vec<OfflineResource>,
pub offline_strategies: HashMap<String, OfflineStrategy>,
pub offline_stats: OfflineStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OfflinePage {
pub url: String,
pub title: String,
pub content: String,
pub last_updated: std::time::SystemTime,
pub cache_strategy: CacheStrategy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OfflineResource {
pub url: String,
pub resource_type: ResourceType,
pub size: usize,
pub last_updated: std::time::SystemTime,
pub cache_strategy: CacheStrategy,
pub compression_ratio: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ResourceType {
Image,
Script,
StyleSheet,
Font,
Video,
Audio,
Document,
Data,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum CacheStrategy {
CacheFirst,
NetworkFirst,
StaleWhileRevalidate,
NetworkOnly,
CacheOnly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OfflineStrategy {
pub name: String,
pub strategy_type: OfflineStrategyType,
pub enabled: bool,
pub priority: u32,
pub fallback_url: Option<String>,
pub cache_duration: u64, }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum OfflineStrategyType {
CacheFirst,
NetworkFirst,
StaleWhileRevalidate,
NetworkOnly,
CacheOnly,
BackgroundSync,
PushNotification,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OfflineStats {
pub total_offline_requests: u64,
pub successful_offline_requests: u64,
pub failed_offline_requests: u64,
pub total_cached_resources: u64,
pub total_cache_size: usize,
pub cache_hit_rate: f64,
pub offline_availability: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PwaInstallStatus {
NotInstalled,
Installing,
Installed,
Failed,
}
impl Default for PwaInstallStatus {
fn default() -> Self {
PwaInstallStatus::NotInstalled
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PwaStats {
pub total_manifest_views: u64,
pub total_installation_prompts: u64,
pub total_installations: u64,
pub total_offline_requests: u64,
pub total_background_syncs: u64,
pub total_push_notifications: u64,
pub app_engagement_score: f64,
pub installation_conversion_rate: f64,
pub offline_usage_rate: f64,
pub compression_ratio: f64,
pub optimization_benefit: f64,
pub optimizations_applied: u32,
pub pwa_efficiency: f64,
pub user_retention_rate: f64,
pub install_status: PwaInstallStatus,
}
#[derive(Error, Debug)]
pub enum PwaFeaturesError {
#[error("App manifest error: {message}")]
AppManifestError { message: String },
#[error("Installation error: {message}")]
InstallationError { message: String },
#[error("Offline capabilities error: {message}")]
OfflineCapabilitiesError { message: String },
#[error("Configuration error: {message}")]
ConfigurationError { message: String },
#[error("Service worker error: {message}")]
ServiceWorkerError { message: String },
#[error("Cache error: {message}")]
CacheError { message: String },
}
impl PwaFeaturesManager {
pub fn new(config: PwaConfig) -> Self {
Self {
config,
stats: Arc::new(RwLock::new(PwaStats::default())),
app_manifest: Arc::new(RwLock::new(AppManifest {
name: "Helios Charts".to_string(),
short_name: "Helios".to_string(),
description: "Advanced charting library for Rust web applications".to_string(),
version: "1.0.0".to_string(),
theme_color: "#2563eb".to_string(),
background_color: "#ffffff".to_string(),
display: AppDisplayMode::Standalone,
orientation: AppOrientation::Any,
scope: "/".to_string(),
start_url: "/".to_string(),
icons: Vec::new(),
categories: vec!["productivity".to_string(), "utilities".to_string()],
screenshots: Vec::new(),
shortcuts: Vec::new(),
related_applications: Vec::new(),
prefer_related_applications: false,
lang: "en".to_string(),
dir: "ltr".to_string(),
})),
installation_manager: Arc::new(RwLock::new(InstallationManager {
installation_prompt_shown: false,
installation_prompt_deferred: false,
installation_prompt_accepted: false,
installation_prompt_dismissed: false,
installation_events: Vec::new(),
installation_stats: InstallationStats::default(),
})),
offline_capabilities: Arc::new(RwLock::new(OfflineCapabilities {
offline_enabled: true,
offline_pages: Vec::new(),
offline_resources: Vec::new(),
offline_strategies: HashMap::new(),
offline_stats: OfflineStats::default(),
})),
}
}
pub async fn initialize(&self) -> Result<(), PwaFeaturesError> {
self.initialize_app_manifest().await?;
self.initialize_installation_manager().await?;
self.initialize_offline_capabilities().await?;
self.update_stats().await;
Ok(())
}
async fn initialize_app_manifest(&self) -> Result<(), PwaFeaturesError> {
let mut manifest = self.app_manifest.write().await;
manifest.name = self.config.app_name.clone();
manifest.short_name = self.config.app_short_name.clone();
manifest.description = self.config.app_description.clone();
manifest.version = self.config.app_version.clone();
manifest.theme_color = self.config.app_theme_color.clone();
manifest.background_color = self.config.app_background_color.clone();
manifest.display = self.config.app_display_mode.clone();
manifest.orientation = self.config.app_orientation.clone();
manifest.scope = self.config.app_scope.clone();
manifest.start_url = self.config.app_start_url.clone();
manifest.icons = self.config.app_icons.clone();
manifest.categories = self.config.app_categories.clone();
manifest.screenshots = self.config.app_screenshots.clone();
if manifest.icons.is_empty() {
manifest.icons = vec![
AppIcon {
src: "/icons/icon-192x192.png".to_string(),
sizes: "192x192".to_string(),
icon_type: "image/png".to_string(),
purpose: "any".to_string(),
},
AppIcon {
src: "/icons/icon-512x512.png".to_string(),
sizes: "512x512".to_string(),
icon_type: "image/png".to_string(),
purpose: "any".to_string(),
},
];
}
manifest.shortcuts = vec![
AppShortcut {
name: "New Chart".to_string(),
short_name: "New".to_string(),
description: "Create a new chart".to_string(),
url: "/new".to_string(),
icons: vec![AppIcon {
src: "/icons/shortcut-new.png".to_string(),
sizes: "96x96".to_string(),
icon_type: "image/png".to_string(),
purpose: "any".to_string(),
}],
},
AppShortcut {
name: "Recent Charts".to_string(),
short_name: "Recent".to_string(),
description: "View recent charts".to_string(),
url: "/recent".to_string(),
icons: vec![AppIcon {
src: "/icons/shortcut-recent.png".to_string(),
sizes: "96x96".to_string(),
icon_type: "image/png".to_string(),
purpose: "any".to_string(),
}],
},
];
Ok(())
}
async fn initialize_installation_manager(&self) -> Result<(), PwaFeaturesError> {
let mut installation_manager = self.installation_manager.write().await;
let events = vec![
InstallationEvent {
event_type: InstallationEventType::PromptShown,
timestamp: std::time::SystemTime::now(),
user_agent: "Mozilla/5.0 (compatible; Test)".to_string(),
platform: "desktop".to_string(),
success: true,
},
InstallationEvent {
event_type: InstallationEventType::AppInstalled,
timestamp: std::time::SystemTime::now(),
user_agent: "Mozilla/5.0 (compatible; Test)".to_string(),
platform: "desktop".to_string(),
success: true,
},
];
for event in events {
installation_manager.installation_events.push(event);
}
installation_manager.installation_stats.total_prompts_shown = 1;
installation_manager.installation_stats.total_installations = 1;
installation_manager.installation_stats.installation_rate = 1.0;
installation_manager
.installation_stats
.prompt_acceptance_rate = 1.0;
installation_manager
.installation_stats
.average_installation_time = 2.5;
Ok(())
}
async fn initialize_offline_capabilities(&self) -> Result<(), PwaFeaturesError> {
let mut offline_capabilities = self.offline_capabilities.write().await;
let offline_pages = vec![
OfflinePage {
url: "/offline".to_string(),
title: "Offline Page".to_string(),
content: "You are offline. Some features may not be available.".to_string(),
last_updated: std::time::SystemTime::now(),
cache_strategy: CacheStrategy::CacheFirst,
},
OfflinePage {
url: "/error".to_string(),
title: "Error Page".to_string(),
content: "An error occurred. Please try again later.".to_string(),
last_updated: std::time::SystemTime::now(),
cache_strategy: CacheStrategy::CacheFirst,
},
];
for page in offline_pages {
offline_capabilities.offline_pages.push(page);
}
let offline_resources = vec![
OfflineResource {
url: "/static/css/main.css".to_string(),
resource_type: ResourceType::StyleSheet,
size: 1024 * 10, last_updated: std::time::SystemTime::now(),
cache_strategy: CacheStrategy::CacheFirst,
compression_ratio: 0.7,
},
OfflineResource {
url: "/static/js/main.js".to_string(),
resource_type: ResourceType::Script,
size: 1024 * 50, last_updated: std::time::SystemTime::now(),
cache_strategy: CacheStrategy::StaleWhileRevalidate,
compression_ratio: 0.6,
},
];
for resource in offline_resources {
offline_capabilities.offline_resources.push(resource);
}
let strategies = vec![
("cache-first".to_string(), OfflineStrategyType::CacheFirst),
(
"network-first".to_string(),
OfflineStrategyType::NetworkFirst,
),
(
"stale-while-revalidate".to_string(),
OfflineStrategyType::StaleWhileRevalidate,
),
];
for (name, strategy_type) in strategies {
let strategy = OfflineStrategy {
name: name.clone(),
strategy_type,
enabled: true,
priority: 1,
fallback_url: None,
cache_duration: 3600, };
offline_capabilities
.offline_strategies
.insert(name, strategy);
}
offline_capabilities.offline_stats.total_offline_requests = 10;
offline_capabilities
.offline_stats
.successful_offline_requests = 8;
offline_capabilities.offline_stats.failed_offline_requests = 2;
offline_capabilities.offline_stats.total_cached_resources = 2;
offline_capabilities.offline_stats.total_cache_size = 60 * 1024; offline_capabilities.offline_stats.cache_hit_rate = 0.8; offline_capabilities.offline_stats.offline_availability = 0.9;
Ok(())
}
pub async fn show_installation_prompt(&self) -> Result<(), PwaFeaturesError> {
let mut installation_manager = self.installation_manager.write().await;
installation_manager.installation_prompt_shown = true;
installation_manager.installation_stats.total_prompts_shown += 1;
let event = InstallationEvent {
event_type: InstallationEventType::PromptShown,
timestamp: std::time::SystemTime::now(),
user_agent: "Mozilla/5.0 (compatible; Test)".to_string(),
platform: "desktop".to_string(),
success: true,
};
installation_manager.installation_events.push(event);
Ok(())
}
pub async fn handle_installation_prompt_response(
&self,
accepted: bool,
) -> Result<(), PwaFeaturesError> {
let mut installation_manager = self.installation_manager.write().await;
if accepted {
installation_manager.installation_prompt_accepted = true;
installation_manager.installation_stats.total_installations += 1;
let event = InstallationEvent {
event_type: InstallationEventType::PromptAccepted,
timestamp: std::time::SystemTime::now(),
user_agent: "Mozilla/5.0 (compatible; Test)".to_string(),
platform: "desktop".to_string(),
success: true,
};
installation_manager.installation_events.push(event);
} else {
installation_manager.installation_prompt_dismissed = true;
let event = InstallationEvent {
event_type: InstallationEventType::PromptDismissed,
timestamp: std::time::SystemTime::now(),
user_agent: "Mozilla/5.0 (compatible; Test)".to_string(),
platform: "desktop".to_string(),
success: false,
};
installation_manager.installation_events.push(event);
}
Ok(())
}
pub async fn get_app_manifest(&self) -> AppManifest {
self.app_manifest.read().await.clone()
}
pub async fn get_installation_manager(&self) -> InstallationManager {
self.installation_manager.read().await.clone()
}
pub async fn get_offline_capabilities(&self) -> OfflineCapabilities {
self.offline_capabilities.read().await.clone()
}
pub async fn get_stats(&self) -> PwaStats {
self.stats.read().await.clone()
}
pub async fn update_config(&mut self, config: PwaConfig) -> Result<(), PwaFeaturesError> {
self.config = config;
Ok(())
}
async fn update_stats(&self) {
let mut stats = self.stats.write().await;
let installation_manager = self.installation_manager.read().await;
let offline_capabilities = self.offline_capabilities.read().await;
stats.total_manifest_views = 1;
stats.total_installation_prompts =
installation_manager.installation_stats.total_prompts_shown;
stats.total_installations = installation_manager.installation_stats.total_installations;
stats.total_offline_requests = offline_capabilities.offline_stats.total_offline_requests;
stats.total_background_syncs = 5;
stats.total_push_notifications = 3;
stats.app_engagement_score = 0.85; stats.installation_conversion_rate =
installation_manager.installation_stats.installation_rate;
stats.offline_usage_rate = offline_capabilities.offline_stats.offline_availability;
stats.compression_ratio = 0.7; stats.optimization_benefit = 0.7; stats.optimizations_applied = 1;
stats.pwa_efficiency = 0.9; stats.user_retention_rate = 0.75; }
}
impl Default for PwaConfig {
fn default() -> Self {
Self {
enable_manifest: true,
enable_installation_prompt: true,
enable_offline_mode: true,
enable_background_sync: true,
enable_push_notifications: true,
enable_share_target: true,
enable_file_handling: true,
app_name: "Helios Charts".to_string(),
app_short_name: "Helios".to_string(),
app_description: "Advanced charting library for Rust web applications".to_string(),
app_version: "1.0.0".to_string(),
app_theme_color: "#2563eb".to_string(),
app_background_color: "#ffffff".to_string(),
app_display_mode: AppDisplayMode::Standalone,
app_orientation: AppOrientation::Any,
app_scope: "/".to_string(),
app_start_url: "/".to_string(),
app_icons: Vec::new(),
app_categories: vec!["productivity".to_string(), "utilities".to_string()],
app_screenshots: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_pwa_config() -> PwaConfig {
PwaConfig {
enable_manifest: true,
enable_installation_prompt: true,
enable_offline_mode: true,
enable_background_sync: true,
enable_push_notifications: true,
enable_share_target: true,
enable_file_handling: true,
app_name: "Test App".to_string(),
app_short_name: "Test".to_string(),
app_description: "Test PWA application".to_string(),
app_version: "1.0.0".to_string(),
app_theme_color: "#ff0000".to_string(),
app_background_color: "#ffffff".to_string(),
app_display_mode: AppDisplayMode::Standalone,
app_orientation: AppOrientation::Any,
app_scope: "/".to_string(),
app_start_url: "/".to_string(),
app_icons: vec![AppIcon {
src: "/test-icon.png".to_string(),
sizes: "192x192".to_string(),
icon_type: "image/png".to_string(),
purpose: "any".to_string(),
}],
app_categories: vec!["test".to_string()],
app_screenshots: Vec::new(),
}
}
#[tokio::test]
async fn test_pwa_features_manager_creation() {
let config = create_test_pwa_config();
let manager = PwaFeaturesManager::new(config);
let stats = manager.get_stats().await;
assert_eq!(stats.total_manifest_views, 0);
assert_eq!(stats.total_installations, 0);
assert_eq!(stats.optimizations_applied, 0);
}
#[tokio::test]
async fn test_pwa_features_initialization() {
let config = create_test_pwa_config();
let manager = PwaFeaturesManager::new(config);
let result = manager.initialize().await;
assert!(result.is_ok());
let stats = manager.get_stats().await;
assert!(stats.total_manifest_views > 0);
assert!(stats.total_installations > 0);
assert!(stats.optimizations_applied > 0);
}
#[tokio::test]
async fn test_app_manifest() {
let config = create_test_pwa_config();
let manager = PwaFeaturesManager::new(config);
let _ = manager.initialize().await;
let manifest = manager.get_app_manifest().await;
assert_eq!(manifest.name, "Test App");
assert_eq!(manifest.short_name, "Test");
assert_eq!(manifest.theme_color, "#ff0000");
assert!(!manifest.icons.is_empty());
assert!(!manifest.shortcuts.is_empty());
}
#[tokio::test]
async fn test_installation_prompt() {
let config = create_test_pwa_config();
let manager = PwaFeaturesManager::new(config);
let _ = manager.initialize().await;
let result = manager.show_installation_prompt().await;
assert!(result.is_ok());
let result = manager.handle_installation_prompt_response(true).await;
assert!(result.is_ok());
let installation_manager = manager.get_installation_manager().await;
assert!(installation_manager.installation_prompt_shown);
assert!(installation_manager.installation_prompt_accepted);
assert!(installation_manager.installation_stats.total_prompts_shown > 0);
assert!(installation_manager.installation_stats.total_installations > 0);
}
#[tokio::test]
async fn test_offline_capabilities() {
let config = create_test_pwa_config();
let manager = PwaFeaturesManager::new(config);
let _ = manager.initialize().await;
let offline_capabilities = manager.get_offline_capabilities().await;
assert!(offline_capabilities.offline_enabled);
assert!(!offline_capabilities.offline_pages.is_empty());
assert!(!offline_capabilities.offline_resources.is_empty());
assert!(!offline_capabilities.offline_strategies.is_empty());
assert!(offline_capabilities.offline_stats.total_offline_requests > 0);
assert!(offline_capabilities.offline_stats.cache_hit_rate > 0.0);
}
#[tokio::test]
async fn test_installation_manager() {
let config = create_test_pwa_config();
let manager = PwaFeaturesManager::new(config);
let _ = manager.initialize().await;
let installation_manager = manager.get_installation_manager().await;
assert!(!installation_manager.installation_events.is_empty());
assert!(installation_manager.installation_stats.total_prompts_shown > 0);
assert!(installation_manager.installation_stats.total_installations > 0);
assert!(installation_manager.installation_stats.installation_rate > 0.0);
}
#[tokio::test]
async fn test_config_update() {
let config = create_test_pwa_config();
let mut manager = PwaFeaturesManager::new(config);
let new_config = create_test_pwa_config();
let result = manager.update_config(new_config).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_pwa_statistics() {
let config = create_test_pwa_config();
let manager = PwaFeaturesManager::new(config);
let _ = manager.initialize().await;
let stats = manager.get_stats().await;
assert!(stats.total_manifest_views > 0);
assert!(stats.total_installations > 0);
assert!(stats.optimization_benefit > 0.0);
assert!(stats.pwa_efficiency > 0.0);
assert!(stats.app_engagement_score > 0.0);
assert!(stats.user_retention_rate > 0.0);
}
}