upstream-rs 1.16.0

Fetch package updates directly from the source.
Documentation
use std::path::PathBuf;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::models::common::{
    enums::{Channel, Filetype, Provider},
    version::Version,
};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum InstallType {
    #[default]
    #[serde(alias = "release")]
    Release,
    #[serde(alias = "build")]
    Build,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Package {
    pub name: String,
    pub repo_slug: String,

    pub filetype: Filetype,
    pub version: Version,
    pub channel: Channel,
    pub provider: Provider,
    pub base_url: Option<String>,
    #[serde(default)]
    pub install_type: InstallType,
    #[serde(default)]
    pub build_branch: Option<String>,
    #[serde(default)]
    pub build_commit: Option<String>,

    pub is_pinned: bool,
    pub match_pattern: Option<String>,
    pub exclude_pattern: Option<String>,
    pub icon_path: Option<PathBuf>,
    pub install_path: Option<PathBuf>,
    pub exec_path: Option<PathBuf>,

    pub last_upgraded: DateTime<Utc>,
}

impl Package {
    #[allow(clippy::too_many_arguments)]
    pub fn with_defaults(
        name: String,
        repo_slug: String,
        filetype: Filetype,
        match_pattern: Option<String>,
        exclude_pattern: Option<String>,
        channel: Channel,
        provider: Provider,
        base_url: Option<String>,
    ) -> Self {
        Self {
            name,
            repo_slug,

            filetype,
            version: Version::new(0, 0, 0, false),
            channel,
            provider,
            base_url,
            install_type: InstallType::Release,
            build_branch: None,
            build_commit: None,

            is_pinned: false,
            match_pattern,
            exclude_pattern,
            icon_path: None,
            install_path: None,
            exec_path: None,

            last_upgraded: Utc::now(),
        }
    }

    pub fn is_same_as(&self, other: &Package) -> bool {
        self.provider == other.provider
            && self.repo_slug == other.repo_slug
            && self.channel == other.channel
            && self.name == other.name
            && self.base_url == other.base_url
    }
}

#[cfg(test)]
mod tests {
    use super::{InstallType, Package};
    use crate::models::common::enums::{Channel, Filetype, Provider};

    #[test]
    fn with_defaults_sets_expected_base_state() {
        let pkg = Package::with_defaults(
            "bat".to_string(),
            "sharkdp/bat".to_string(),
            Filetype::Auto,
            Some("linux".to_string()),
            Some("debug".to_string()),
            Channel::Stable,
            Provider::Github,
            None,
        );

        assert_eq!(pkg.version.major, 0);
        assert!(!pkg.is_pinned);
        assert_eq!(pkg.install_type, InstallType::Release);
        assert!(pkg.install_path.is_none());
        assert!(pkg.exec_path.is_none());
        assert_eq!(pkg.match_pattern.as_deref(), Some("linux"));
        assert_eq!(pkg.exclude_pattern.as_deref(), Some("debug"));
    }

    #[test]
    fn is_same_as_uses_identity_fields_only() {
        let mut a = Package::with_defaults(
            "ripgrep".to_string(),
            "BurntSushi/ripgrep".to_string(),
            Filetype::Archive,
            None,
            None,
            Channel::Stable,
            Provider::Github,
            Some("https://api.github.com".to_string()),
        );
        let mut b = a.clone();
        b.version.major = 99;
        b.is_pinned = true;
        b.install_type = InstallType::Build;
        b.match_pattern = Some("x86_64".to_string());
        assert!(a.is_same_as(&b));

        a.name = "rg".to_string();
        assert!(!a.is_same_as(&b));
    }

    #[test]
    fn deserialize_defaults_install_type_to_release_for_legacy_records() {
        let legacy = r#"{
            "name":"tool",
            "repo_slug":"owner/tool",
            "filetype":"Binary",
            "version":{"major":1,"minor":2,"patch":3,"is_prerelease":false},
            "channel":"Stable",
            "provider":"Github",
            "base_url":null,
            "is_pinned":false,
            "match_pattern":null,
            "exclude_pattern":null,
            "icon_path":null,
            "install_path":null,
            "exec_path":null,
            "last_upgraded":"2026-01-01T00:00:00Z"
        }"#;
        let pkg: Package = serde_json::from_str(legacy).expect("deserialize legacy package");
        assert_eq!(pkg.install_type, InstallType::Release);
    }

    #[test]
    fn deserialize_accepts_legacy_lowercase_install_type_values() {
        let legacy_lowercase = r#"{
            "name":"tool",
            "repo_slug":"owner/tool",
            "filetype":"Binary",
            "version":{"major":1,"minor":2,"patch":3,"is_prerelease":false},
            "channel":"Stable",
            "provider":"Github",
            "base_url":null,
            "install_type":"release",
            "is_pinned":false,
            "match_pattern":null,
            "exclude_pattern":null,
            "icon_path":null,
            "install_path":null,
            "exec_path":null,
            "last_upgraded":"2026-01-01T00:00:00Z"
        }"#;
        let pkg: Package =
            serde_json::from_str(legacy_lowercase).expect("deserialize lowercase install type");
        assert_eq!(pkg.install_type, InstallType::Release);
    }
}