crepuscularity-native 0.5.6

View intermediate representation (JSON) for native SwiftUI / Compose shells (UNSTABLE).
Documentation
use serde::{Deserialize, Serialize};
use serde_json::Value;

pub const NATIVE_CAPABILITIES: &[NativeCapability] = &[
    NativeCapability::ActionSheet,
    NativeCapability::AppLauncher,
    NativeCapability::App,
    NativeCapability::BackgroundRunner,
    NativeCapability::BarcodeScanner,
    NativeCapability::Browser,
    NativeCapability::Bluetooth,
    NativeCapability::Camera,
    NativeCapability::Clipboard,
    NativeCapability::Cookies,
    NativeCapability::Device,
    NativeCapability::Dialog,
    NativeCapability::Filesystem,
    NativeCapability::FileTransfer,
    NativeCapability::FileViewer,
    NativeCapability::Geolocation,
    NativeCapability::GoogleMaps,
    NativeCapability::Haptics,
    NativeCapability::Http,
    NativeCapability::InAppBrowser,
    NativeCapability::Keyboard,
    NativeCapability::LocalLlm,
    NativeCapability::LocalNotifications,
    NativeCapability::Motion,
    NativeCapability::Network,
    NativeCapability::Preferences,
    NativeCapability::PrivacyScreen,
    NativeCapability::PushNotifications,
    NativeCapability::ScreenOrientation,
    NativeCapability::ScreenReader,
    NativeCapability::Share,
    NativeCapability::SplashScreen,
    NativeCapability::StatusBar,
    NativeCapability::SystemBars,
    NativeCapability::Sync,
    NativeCapability::TextZoom,
    NativeCapability::Toast,
];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum NativeCapability {
    ActionSheet,
    AppLauncher,
    App,
    BackgroundRunner,
    BarcodeScanner,
    Browser,
    Bluetooth,
    Camera,
    Clipboard,
    Cookies,
    Device,
    Dialog,
    Filesystem,
    FileTransfer,
    FileViewer,
    Geolocation,
    GoogleMaps,
    Haptics,
    Http,
    InAppBrowser,
    Keyboard,
    LocalLlm,
    LocalNotifications,
    Motion,
    Network,
    Preferences,
    PrivacyScreen,
    PushNotifications,
    ScreenOrientation,
    ScreenReader,
    Share,
    SplashScreen,
    StatusBar,
    SystemBars,
    Sync,
    TextZoom,
    Toast,
}

impl NativeCapability {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::ActionSheet => "actionSheet",
            Self::AppLauncher => "appLauncher",
            Self::App => "app",
            Self::BackgroundRunner => "backgroundRunner",
            Self::BarcodeScanner => "barcodeScanner",
            Self::Browser => "browser",
            Self::Bluetooth => "bluetooth",
            Self::Camera => "camera",
            Self::Clipboard => "clipboard",
            Self::Cookies => "cookies",
            Self::Device => "device",
            Self::Dialog => "dialog",
            Self::Filesystem => "filesystem",
            Self::FileTransfer => "fileTransfer",
            Self::FileViewer => "fileViewer",
            Self::Geolocation => "geolocation",
            Self::GoogleMaps => "googleMaps",
            Self::Haptics => "haptics",
            Self::Http => "http",
            Self::InAppBrowser => "inAppBrowser",
            Self::Keyboard => "keyboard",
            Self::LocalLlm => "localLlm",
            Self::LocalNotifications => "localNotifications",
            Self::Motion => "motion",
            Self::Network => "network",
            Self::Preferences => "preferences",
            Self::PrivacyScreen => "privacyScreen",
            Self::PushNotifications => "pushNotifications",
            Self::ScreenOrientation => "screenOrientation",
            Self::ScreenReader => "screenReader",
            Self::Share => "share",
            Self::SplashScreen => "splashScreen",
            Self::StatusBar => "statusBar",
            Self::SystemBars => "systemBars",
            Self::Sync => "sync",
            Self::TextZoom => "textZoom",
            Self::Toast => "toast",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum NativeRequest {
    FilePicker(FilePickerRequest),
    Plugin(NativePluginRequest),
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FilePickerRequest {
    pub accept: Vec<String>,
    pub multiple: bool,
}

impl FilePickerRequest {
    pub fn media() -> Self {
        Self {
            accept: vec!["image/*".to_string(), "video/*".to_string()],
            multiple: true,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NativePluginRequest {
    pub capability: NativeCapability,
    pub method: String,
    pub payload: Value,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum NativeResponse {
    FilePicker(FilePickerResponse),
    Plugin(NativePluginResponse),
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FilePickerResponse {
    pub files: Vec<PickedFile>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PickedFile {
    pub name: String,
    pub mime_type: String,
    pub bytes: u64,
    pub data_base64: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NativePluginResponse {
    pub capability: NativeCapability,
    pub value: Value,
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;

    #[test]
    fn file_picker_request_is_portable_json() {
        let json = serde_json::to_value(NativeRequest::FilePicker(FilePickerRequest::media()))
            .expect("serialize request");
        assert_eq!(json["kind"], "filePicker");
        assert_eq!(json["accept"][0], "image/*");
        assert_eq!(json["multiple"], true);
    }

    #[test]
    fn native_capability_registry_has_unique_names() {
        let names = NATIVE_CAPABILITIES
            .iter()
            .map(|capability| capability.as_str())
            .collect::<HashSet<_>>();

        assert_eq!(NATIVE_CAPABILITIES.len(), 37);
        assert_eq!(names.len(), NATIVE_CAPABILITIES.len());
        assert!(names.contains("bluetooth"));
        assert!(names.contains("camera"));
        assert!(names.contains("clipboard"));
        assert!(names.contains("fileTransfer"));
        assert!(names.contains("sync"));
        assert!(names.contains("toast"));
    }

    #[test]
    fn generic_plugin_request_is_portable_json() {
        let request = NativeRequest::Plugin(NativePluginRequest {
            capability: NativeCapability::Share,
            method: "share".to_string(),
            payload: serde_json::json!({ "title": "Cupboard" }),
        });
        let json = serde_json::to_value(request).expect("serialize request");

        assert_eq!(json["kind"], "plugin");
        assert_eq!(json["capability"], "share");
        assert_eq!(json["method"], "share");
        assert_eq!(json["payload"]["title"], "Cupboard");
    }
}