use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct WebAppManifest {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub short_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub orientation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dir: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub icons: Vec<ManifestIcon>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub related_applications: Vec<RelatedApplication>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefer_related_applications: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub categories: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub screenshots: Vec<ManifestImage>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shortcuts: Vec<ManifestShortcut>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ManifestIcon {
pub src: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sizes: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RelatedApplication {
pub platform: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ManifestImage {
pub src: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sizes: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ManifestShortcut {
pub name: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub short_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub icons: Vec<ManifestIcon>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ManifestDiscovery {
#[serde(skip_serializing_if = "Option::is_none")]
pub href: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest: Option<WebAppManifest>,
}
impl WebAppManifest {
pub fn to_py_dict(&self, py: Python) -> Py<PyDict> {
let dict = PyDict::new_bound(py);
if let Some(ref name) = self.name {
dict.set_item("name", name).unwrap();
}
if let Some(ref short_name) = self.short_name {
dict.set_item("short_name", short_name).unwrap();
}
if let Some(ref description) = self.description {
dict.set_item("description", description).unwrap();
}
if let Some(ref start_url) = self.start_url {
dict.set_item("start_url", start_url).unwrap();
}
if let Some(ref display) = self.display {
dict.set_item("display", display).unwrap();
}
if let Some(ref orientation) = self.orientation {
dict.set_item("orientation", orientation).unwrap();
}
if let Some(ref theme_color) = self.theme_color {
dict.set_item("theme_color", theme_color).unwrap();
}
if let Some(ref background_color) = self.background_color {
dict.set_item("background_color", background_color).unwrap();
}
if let Some(ref scope) = self.scope {
dict.set_item("scope", scope).unwrap();
}
if let Some(ref lang) = self.lang {
dict.set_item("lang", lang).unwrap();
}
if let Some(ref dir) = self.dir {
dict.set_item("dir", dir).unwrap();
}
if let Some(ref id) = self.id {
dict.set_item("id", id).unwrap();
}
if let Some(prefer) = self.prefer_related_applications {
dict.set_item("prefer_related_applications", prefer).unwrap();
}
if !self.icons.is_empty() {
let icons_list = PyList::empty_bound(py);
for icon in &self.icons {
icons_list.append(icon.to_py_dict(py)).unwrap();
}
dict.set_item("icons", icons_list).unwrap();
}
if !self.related_applications.is_empty() {
let apps_list = PyList::empty_bound(py);
for app in &self.related_applications {
apps_list.append(app.to_py_dict(py)).unwrap();
}
dict.set_item("related_applications", apps_list).unwrap();
}
if !self.categories.is_empty() {
dict.set_item("categories", self.categories.clone()).unwrap();
}
if !self.screenshots.is_empty() {
let screenshots_list = PyList::empty_bound(py);
for screenshot in &self.screenshots {
screenshots_list.append(screenshot.to_py_dict(py)).unwrap();
}
dict.set_item("screenshots", screenshots_list).unwrap();
}
if !self.shortcuts.is_empty() {
let shortcuts_list = PyList::empty_bound(py);
for shortcut in &self.shortcuts {
shortcuts_list.append(shortcut.to_py_dict(py)).unwrap();
}
dict.set_item("shortcuts", shortcuts_list).unwrap();
}
dict.unbind()
}
}
impl ManifestIcon {
pub fn to_py_dict(&self, py: Python) -> Py<PyDict> {
let dict = PyDict::new_bound(py);
dict.set_item("src", &self.src).unwrap();
if let Some(ref sizes) = self.sizes {
dict.set_item("sizes", sizes).unwrap();
}
if let Some(ref mime_type) = self.mime_type {
dict.set_item("type", mime_type).unwrap();
}
if let Some(ref purpose) = self.purpose {
dict.set_item("purpose", purpose).unwrap();
}
dict.unbind()
}
}
impl RelatedApplication {
pub fn to_py_dict(&self, py: Python) -> Py<PyDict> {
let dict = PyDict::new_bound(py);
dict.set_item("platform", &self.platform).unwrap();
if let Some(ref url) = self.url {
dict.set_item("url", url).unwrap();
}
if let Some(ref id) = self.id {
dict.set_item("id", id).unwrap();
}
dict.unbind()
}
}
impl ManifestImage {
pub fn to_py_dict(&self, py: Python) -> Py<PyDict> {
let dict = PyDict::new_bound(py);
dict.set_item("src", &self.src).unwrap();
if let Some(ref sizes) = self.sizes {
dict.set_item("sizes", sizes).unwrap();
}
if let Some(ref mime_type) = self.mime_type {
dict.set_item("type", mime_type).unwrap();
}
if let Some(ref label) = self.label {
dict.set_item("label", label).unwrap();
}
dict.unbind()
}
}
impl ManifestShortcut {
pub fn to_py_dict(&self, py: Python) -> Py<PyDict> {
let dict = PyDict::new_bound(py);
dict.set_item("name", &self.name).unwrap();
dict.set_item("url", &self.url).unwrap();
if let Some(ref short_name) = self.short_name {
dict.set_item("short_name", short_name).unwrap();
}
if let Some(ref description) = self.description {
dict.set_item("description", description).unwrap();
}
if !self.icons.is_empty() {
let icons_list = PyList::empty_bound(py);
for icon in &self.icons {
icons_list.append(icon.to_py_dict(py)).unwrap();
}
dict.set_item("icons", icons_list).unwrap();
}
dict.unbind()
}
}
impl ManifestDiscovery {
pub fn to_py_dict(&self, py: Python) -> Py<PyDict> {
let dict = PyDict::new_bound(py);
if let Some(ref href) = self.href {
dict.set_item("href", href).unwrap();
}
if let Some(ref manifest) = self.manifest {
dict.set_item("manifest", manifest.to_py_dict(py)).unwrap();
}
dict.unbind()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_web_app_manifest_default() {
let manifest = WebAppManifest::default();
assert!(manifest.name.is_none());
assert!(manifest.icons.is_empty());
}
#[test]
fn test_manifest_icon_serde() {
let icon = ManifestIcon {
src: "/icon-192.png".to_string(),
sizes: Some("192x192".to_string()),
mime_type: Some("image/png".to_string()),
purpose: Some("any".to_string()),
};
let json = serde_json::to_string(&icon).unwrap();
let deserialized: ManifestIcon = serde_json::from_str(&json).unwrap();
assert_eq!(icon, deserialized);
}
#[test]
fn test_manifest_serde_skip_none() {
let manifest = WebAppManifest {
name: Some("Test App".to_string()),
..Default::default()
};
let json = serde_json::to_value(&manifest).unwrap();
let obj = json.as_object().unwrap();
assert!(obj.contains_key("name"));
assert!(!obj.contains_key("description"));
assert!(!obj.contains_key("start_url"));
}
#[test]
fn test_manifest_full_parse() {
let json = r##"
{
"name": "Test PWA",
"short_name": "Test",
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"icons": [{
"src": "/icon.png",
"sizes": "192x192",
"type": "image/png"
}]
}
"##;
let manifest: WebAppManifest = serde_json::from_str(json).unwrap();
assert_eq!(manifest.name, Some("Test PWA".to_string()));
assert_eq!(manifest.icons.len(), 1);
}
#[test]
fn test_manifest_to_py_dict() {
Python::with_gil(|py| {
let manifest = WebAppManifest {
name: Some("Test App".to_string()),
short_name: Some("Test".to_string()),
..Default::default()
};
let py_dict = manifest.to_py_dict(py);
let dict = py_dict.bind(py);
assert!(dict.contains("name").unwrap());
assert!(dict.contains("short_name").unwrap());
});
}
#[test]
fn test_manifest_icon_to_py_dict() {
Python::with_gil(|py| {
let icon = ManifestIcon {
src: "/icon.png".to_string(),
sizes: Some("192x192".to_string()),
mime_type: Some("image/png".to_string()),
purpose: None,
};
let py_dict = icon.to_py_dict(py);
let dict = py_dict.bind(py);
assert!(dict.contains("src").unwrap());
assert!(dict.contains("sizes").unwrap());
assert!(dict.contains("type").unwrap());
});
}
#[test]
fn test_related_application() {
let app = RelatedApplication {
platform: "play".to_string(),
url: Some("https://play.google.com/store/apps/details?id=com.example".to_string()),
id: Some("com.example".to_string()),
};
let json = serde_json::to_string(&app).unwrap();
let deserialized: RelatedApplication = serde_json::from_str(&json).unwrap();
assert_eq!(app, deserialized);
}
#[test]
fn test_manifest_shortcut() {
let shortcut = ManifestShortcut {
name: "New Item".to_string(),
url: "/new".to_string(),
short_name: None,
description: Some("Create new item".to_string()),
icons: vec![],
};
let json = serde_json::to_string(&shortcut).unwrap();
let deserialized: ManifestShortcut = serde_json::from_str(&json).unwrap();
assert_eq!(shortcut, deserialized);
}
#[test]
fn test_manifest_discovery_default() {
let discovery = ManifestDiscovery::default();
assert!(discovery.href.is_none());
assert!(discovery.manifest.is_none());
}
#[test]
fn test_manifest_discovery_to_py_dict() {
Python::with_gil(|py| {
let discovery = ManifestDiscovery {
href: Some("/manifest.json".to_string()),
manifest: None,
};
let py_dict = discovery.to_py_dict(py);
let dict = py_dict.bind(py);
assert!(dict.contains("href").unwrap());
});
}
}