backgroundassets 0.3.0

Safe Rust bindings for Apple's BackgroundAssets framework — on-demand asset packs delivered via App Store on macOS
Documentation
use core::ffi::c_char;
use std::fmt;

use serde::{Deserialize, Serialize};

use crate::ffi;

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct BackgroundAssetsError {
    domain: String,
    code: i64,
    message: String,
    asset_pack_id: Option<String>,
    file_path: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct BridgeErrorPayload {
    #[serde(default = "default_bridge_domain")]
    pub domain: String,
    #[serde(default = "default_bridge_code")]
    pub code: i64,
    #[serde(default)]
    pub message: String,
    #[serde(default, rename = "assetPackID")]
    pub asset_pack_id: Option<String>,
    #[serde(default, rename = "filePath")]
    pub file_path: Option<String>,
}

const fn default_bridge_code() -> i64 {
    -1
}

fn default_bridge_domain() -> String {
    "BackgroundAssetsBridge".into()
}

fn is_managed_error_domain(domain: &str) -> bool {
    matches!(domain, "BAManagedErrorDomain" | "BackgroundAssets.ManagedBackgroundAssetsError")
        || domain.ends_with(".ManagedBackgroundAssetsError")
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ManagedBackgroundAssetsErrorCode {
    AssetPackNotFound,
    FileNotFound,
    Unknown(i64),
}

impl ManagedBackgroundAssetsErrorCode {
    pub const fn from_raw(value: i64) -> Self {
        match value {
            0 => Self::AssetPackNotFound,
            1 => Self::FileNotFound,
            other => Self::Unknown(other),
        }
    }

    pub const fn raw_value(self) -> i64 {
        match self {
            Self::AssetPackNotFound => 0,
            Self::FileNotFound => 1,
            Self::Unknown(value) => value,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ManagedBackgroundAssetsError {
    #[serde(flatten)]
    inner: BackgroundAssetsError,
    managed_code: ManagedBackgroundAssetsErrorCode,
}

impl ManagedBackgroundAssetsError {
    pub fn from_background_assets_error(error: BackgroundAssetsError) -> Option<Self> {
        is_managed_error_domain(error.domain()).then(|| Self {
            managed_code: ManagedBackgroundAssetsErrorCode::from_raw(error.code()),
            inner: error,
        })
    }

    pub const fn managed_code(&self) -> ManagedBackgroundAssetsErrorCode {
        self.managed_code
    }

    pub fn as_background_assets_error(&self) -> &BackgroundAssetsError {
        &self.inner
    }

    pub fn into_background_assets_error(self) -> BackgroundAssetsError {
        self.inner
    }

    pub fn domain(&self) -> &str {
        self.inner.domain()
    }

    pub const fn code(&self) -> i64 {
        self.inner.code()
    }

    pub fn message_text(&self) -> &str {
        self.inner.message_text()
    }

    pub fn asset_pack_id(&self) -> Option<&str> {
        self.inner.asset_pack_id()
    }

    pub fn file_path(&self) -> Option<&str> {
        self.inner.file_path()
    }
}

impl TryFrom<BackgroundAssetsError> for ManagedBackgroundAssetsError {
    type Error = BackgroundAssetsError;

    fn try_from(value: BackgroundAssetsError) -> Result<Self, Self::Error> {
        Self::from_background_assets_error(value.clone()).ok_or(value)
    }
}

impl From<ManagedBackgroundAssetsError> for BackgroundAssetsError {
    fn from(value: ManagedBackgroundAssetsError) -> Self {
        value.inner
    }
}

impl fmt::Display for ManagedBackgroundAssetsError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.inner.fmt(f)
    }
}

impl std::error::Error for ManagedBackgroundAssetsError {}

impl BackgroundAssetsError {
    pub(crate) fn invalid_argument(message: impl Into<String>) -> Self {
        Self {
            domain: default_bridge_domain(),
            code: default_bridge_code(),
            message: message.into(),
            asset_pack_id: None,
            file_path: None,
        }
    }

    pub(crate) fn message(message: impl Into<String>) -> Self {
        Self::invalid_argument(message)
    }

    pub(crate) fn from_json_str(json: &str) -> Self {
        serde_json::from_str::<BridgeErrorPayload>(json).map_or_else(
            |_| Self {
                domain: default_bridge_domain(),
                code: default_bridge_code(),
                message: json.to_string(),
                asset_pack_id: None,
                file_path: None,
            },
            Into::into,
        )
    }

    pub(crate) fn from_owned_json_ptr(ptr: *mut c_char) -> Self {
        Self::from_json_str(&unsafe { ffi::owned_string(ptr) })
    }

    pub fn domain(&self) -> &str {
        &self.domain
    }

    pub const fn code(&self) -> i64 {
        self.code
    }

    pub fn message_text(&self) -> &str {
        &self.message
    }

    pub fn asset_pack_id(&self) -> Option<&str> {
        self.asset_pack_id.as_deref()
    }

    pub fn file_path(&self) -> Option<&str> {
        self.file_path.as_deref()
    }

    pub fn managed_error_code(&self) -> Option<ManagedBackgroundAssetsErrorCode> {
        is_managed_error_domain(self.domain())
            .then(|| ManagedBackgroundAssetsErrorCode::from_raw(self.code()))
    }

    pub fn into_managed_background_assets_error(
        self,
    ) -> Result<ManagedBackgroundAssetsError, Self> {
        ManagedBackgroundAssetsError::try_from(self)
    }
}

impl From<BridgeErrorPayload> for BackgroundAssetsError {
    fn from(value: BridgeErrorPayload) -> Self {
        Self {
            domain: value.domain,
            code: value.code,
            message: if value.message.is_empty() {
                "BackgroundAssets operation failed".into()
            } else {
                value.message
            },
            asset_pack_id: value.asset_pack_id,
            file_path: value.file_path,
        }
    }
}

impl fmt::Display for BackgroundAssetsError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} ({}:{})", self.message, self.domain, self.code)
    }
}

impl std::error::Error for BackgroundAssetsError {}

#[cfg(test)]
mod tests {
    use super::{BackgroundAssetsError, ManagedBackgroundAssetsErrorCode};

    #[test]
    fn managed_error_conversion_preserves_asset_pack_id() {
        let error = BackgroundAssetsError::from_json_str(
            r#"{"domain":"BackgroundAssets.ManagedBackgroundAssetsError","code":0,"message":"missing","assetPackID":"pack.one"}"#,
        );

        assert_eq!(
            error.managed_error_code(),
            Some(ManagedBackgroundAssetsErrorCode::AssetPackNotFound)
        );
        let managed = error.into_managed_background_assets_error().unwrap();
        assert_eq!(managed.asset_pack_id(), Some("pack.one"));
        assert_eq!(managed.file_path(), None);
    }

    #[test]
    fn managed_error_conversion_preserves_file_path() {
        let error = BackgroundAssetsError::from_json_str(
            r#"{"domain":"BAManagedErrorDomain","code":1,"message":"missing","filePath":"Assets/file.bin"}"#,
        );

        assert_eq!(
            error.managed_error_code(),
            Some(ManagedBackgroundAssetsErrorCode::FileNotFound)
        );
        let managed = error.into_managed_background_assets_error().unwrap();
        assert_eq!(managed.asset_pack_id(), None);
        assert_eq!(managed.file_path(), Some("Assets/file.bin"));
    }

    #[test]
    fn non_managed_error_rejects_conversion() {
        let error = BackgroundAssetsError::from_json_str(
            r#"{"domain":"BackgroundAssetsBridge","code":-1,"message":"nope"}"#,
        );

        assert_eq!(error.managed_error_code(), None);
        assert!(error.into_managed_background_assets_error().is_err());
    }
}