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