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;
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}