upstream_rs/models/upstream/
package.rs1use 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}