n5i 0.11.2

Common components for n5i
Documentation
// SPDX-FileCopyrightText: 2024-2026 The n5i Project
//
// SPDX-License-Identifier: AGPL-3.0-or-later

use crate::utils::MultiLanguageItem;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::{self, Debug};

#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
pub struct StoreMetadata {
    pub name: MultiLanguageItem,
    pub tagline: MultiLanguageItem,
    pub description: MultiLanguageItem,
    pub icon: String,
    // Developer name -> their website
    pub developers: BTreeMap<String, String>,
    pub license: String,
}

#[derive(Debug, Deserialize, Clone, Default)]
pub struct LegacyAppStore {
    pub id: uuid::Uuid,
    #[serde(flatten)]
    pub metadata: StoreMetadata,
    #[serde(default)]
    pub r#type: StoreType,
    pub src: HashMap<String, String>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct AppStore {
    pub id: uuid::Uuid,
    #[serde(flatten)]
    pub metadata: StoreMetadata,
    pub provider_id: String,
    pub format_id: String,
    pub src: HashMap<String, String>,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum OnDiskAppStore {
    Legacy(LegacyAppStore),
    Current(AppStore),
}

impl TryFrom<LegacyAppStore> for AppStore {
    type Error = anyhow::Error;

    fn try_from(legacy: LegacyAppStore) -> Result<Self, Self::Error> {
        Ok(AppStore {
            id: legacy.id,
            metadata: legacy.metadata,
            provider_id: match legacy.r#type {
                StoreType::Git => "git".to_string(),
                StoreType::Plugin => "plugin".to_string(),
                StoreType::Http => "http".to_string(),
            },
            format_id: match legacy.r#type {
                StoreType::Git => "git".to_string(),
                StoreType::Plugin => legacy
                    .src
                    .get("plugin_id")
                    .cloned()
                    .ok_or_else(|| anyhow::anyhow!("Missing plugin_id for plugin store"))?,
                StoreType::Http => "http".to_string(),
            },
            src: legacy.src,
        })
    }
}

impl TryFrom<OnDiskAppStore> for AppStore {
    type Error = anyhow::Error;

    fn try_from(on_disk: OnDiskAppStore) -> Result<Self, Self::Error> {
        match on_disk {
            OnDiskAppStore::Legacy(legacy) => legacy.try_into(),
            OnDiskAppStore::Current(current) => Ok(current),
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AppUpdateInfo {
    pub id: String,
    pub current_version: semver::Version,
    pub new_version: semver::Version,
    pub release_notes: BTreeMap<String, MultiLanguageItem>,
    pub current_permissions: HashSet<String>,
    pub new_permissions: HashSet<String>,
}

pub type StoresYml = Vec<AppStore>;

#[derive(Debug, Serialize, Deserialize, Clone, Default, Eq, PartialEq)]
pub enum StoreType {
    #[default]
    // Rename for backwards compatibility
    #[serde(rename = "Nirvati")]
    Git,
    Plugin,
    Http,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct InvalidEnumValue(pub i32);

impl TryFrom<i32> for StoreType {
    type Error = InvalidEnumValue;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(StoreType::Git),
            -1 => Ok(StoreType::Plugin),
            unknown => Err(InvalidEnumValue(unknown)),
        }
    }
}

impl fmt::Display for InvalidEnumValue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Invalid enum value {}", self.0)
    }
}

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