Skip to main content

upstream_rs/models/upstream/
package.rs

1use std::path::PathBuf;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::models::common::{
7    enums::{Channel, Filetype, Provider},
8    version::Version,
9};
10use crate::models::provider::Release;
11use crate::providers::pattern_matcher::PatternTable;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14pub enum InstallType {
15    Release,
16    Build,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Package {
21    pub name: String,
22    pub repo_slug: String,
23
24    pub filetype: Filetype,
25    pub version: Version,
26    pub channel: Channel,
27    pub provider: Provider,
28    pub base_url: Option<String>,
29    pub install_type: InstallType,
30    pub build_branch: Option<String>,
31    pub build_commit: Option<String>,
32
33    pub is_pinned: bool,
34    #[serde(default)]
35    pub match_pattern: PatternTable,
36    #[serde(default)]
37    pub exclude_pattern: PatternTable,
38    pub icon_path: Option<PathBuf>,
39    pub install_path: Option<PathBuf>,
40    pub exec_path: Option<PathBuf>,
41
42    pub last_upgraded: DateTime<Utc>,
43}
44
45impl Package {
46    #[allow(clippy::too_many_arguments)]
47    pub fn with_defaults(
48        name: String,
49        repo_slug: String,
50        filetype: Filetype,
51        match_pattern: Option<String>,
52        exclude_pattern: Option<String>,
53        channel: Channel,
54        provider: Provider,
55        base_url: Option<String>,
56    ) -> Self {
57        Self {
58            name,
59            repo_slug,
60
61            filetype,
62            version: Version::new(0, 0, 0, false),
63            channel,
64            provider,
65            base_url,
66            install_type: InstallType::Release,
67            build_branch: None,
68            build_commit: None,
69
70            is_pinned: false,
71            match_pattern: PatternTable::from_cli_arg(match_pattern),
72            exclude_pattern: PatternTable::from_cli_arg(exclude_pattern),
73            icon_path: None,
74            install_path: None,
75            exec_path: None,
76
77            last_upgraded: Utc::now(),
78        }
79    }
80
81    pub fn is_same_as(&self, other: &Package) -> bool {
82        self.provider == other.provider
83            && self.repo_slug == other.repo_slug
84            && self.channel == other.channel
85            && self.name == other.name
86            && self.base_url == other.base_url
87    }
88
89    pub fn is_update_available(&self, release: &Release) -> bool {
90        if self.channel == Channel::Nightly {
91            return release.published_at > self.last_upgraded;
92        }
93
94        if release.version.is_unknown() {
95            return release.published_at > self.last_upgraded;
96        }
97
98        release.version.is_newer_than(&self.version)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::{InstallType, Package};
105    use crate::models::{
106        common::{
107            Version,
108            enums::{Channel, Filetype, Provider},
109        },
110        provider::Release,
111    };
112    use crate::providers::pattern_matcher::PatternTable;
113    use chrono::{Duration, TimeZone, Utc};
114
115    fn update_test_package(version: Version, channel: Channel) -> Package {
116        let mut package = Package::with_defaults(
117            "tool".to_string(),
118            "owner/tool".to_string(),
119            Filetype::Archive,
120            None,
121            None,
122            channel,
123            Provider::Github,
124            None,
125        );
126        package.version = version;
127        package.last_upgraded = Utc
128            .with_ymd_and_hms(2026, 1, 1, 12, 0, 0)
129            .single()
130            .expect("valid timestamp");
131        package
132    }
133
134    fn update_test_release(version: Version, published_offset: Duration) -> Release {
135        let base = Utc
136            .with_ymd_and_hms(2026, 1, 1, 12, 0, 0)
137            .single()
138            .expect("valid timestamp");
139        Release {
140            id: 1,
141            tag: version.to_string(),
142            name: version.to_string(),
143            body: String::new(),
144            is_draft: false,
145            is_prerelease: false,
146            assets: Vec::new(),
147            version,
148            published_at: base + published_offset,
149        }
150    }
151
152    #[test]
153    fn is_same_as_uses_identity_fields_only() {
154        let mut a = Package::with_defaults(
155            "ripgrep".to_string(),
156            "BurntSushi/ripgrep".to_string(),
157            Filetype::Archive,
158            None,
159            None,
160            Channel::Stable,
161            Provider::Github,
162            Some("https://api.github.com".to_string()),
163        );
164        let mut b = a.clone();
165        b.version.major = 99;
166        b.is_pinned = true;
167        b.install_type = InstallType::Build;
168        b.match_pattern = PatternTable::from_patterns(["x86_64"]);
169        assert!(a.is_same_as(&b));
170
171        a.name = "rg".to_string();
172        assert!(!a.is_same_as(&b));
173    }
174
175    #[test]
176    fn stable_release_uses_semver_when_version_is_known() {
177        let package = update_test_package(Version::new(1, 0, 0, false), Channel::Stable);
178
179        assert!(package.is_update_available(&update_test_release(
180            Version::new(1, 0, 1, false),
181            Duration::seconds(-1)
182        )));
183        assert!(!package.is_update_available(&update_test_release(
184            Version::new(1, 0, 0, false),
185            Duration::days(1)
186        )));
187    }
188
189    #[test]
190    fn stable_unknown_release_uses_published_timestamp() {
191        let package = update_test_package(Version::new(0, 0, 0, false), Channel::Stable);
192
193        assert!(package.is_update_available(&update_test_release(
194            Version::new(0, 0, 0, false),
195            Duration::seconds(1)
196        )));
197        assert!(!package.is_update_available(&update_test_release(
198            Version::new(0, 0, 0, false),
199            Duration::seconds(0)
200        )));
201    }
202
203    #[test]
204    fn nightly_release_uses_published_timestamp() {
205        let package = update_test_package(Version::new(9, 9, 9, false), Channel::Nightly);
206
207        assert!(package.is_update_available(&update_test_release(
208            Version::new(1, 0, 0, false),
209            Duration::seconds(1)
210        )));
211        assert!(!package.is_update_available(&update_test_release(
212            Version::new(99, 0, 0, false),
213            Duration::seconds(0)
214        )));
215    }
216}