1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
5#[serde(rename_all = "lowercase")]
6pub enum UpdateChannel {
7 Stable,
8 Beta,
9 Alpha,
10}
11
12impl UpdateChannel {
13 pub fn as_str(self) -> &'static str {
14 match self {
15 Self::Stable => "stable",
16 Self::Beta => "beta",
17 Self::Alpha => "alpha",
18 }
19 }
20}
21
22#[derive(Debug, Clone)]
23pub struct SourceConfig {
24 pub channel: UpdateChannel,
25 pub kind: SourceKind,
26}
27
28#[derive(Debug, Clone)]
29pub enum SourceKind {
30 Manifest {
31 updates_base_url: String,
32 updates_root: Option<PathBuf>,
33 },
34 GithubLatest {
35 repo: GithubRepo,
36 },
37 GithubTag {
38 repo: GithubRepo,
39 tag: String,
40 },
41}
42
43#[derive(Debug, Clone)]
44pub struct CheckRequest {
45 pub product: String,
46 pub source: SourceConfig,
47 pub current_version: String,
48 pub branch: String,
49}
50
51#[derive(Debug, Clone)]
52pub struct UpdateRequest {
53 pub product: String,
54 pub target: UpdateTarget,
55 pub source: SourceConfig,
56 pub current_version: String,
57 pub install_dir: Option<PathBuf>,
58 pub yes: bool,
59 pub dry_run: bool,
60 pub force: bool,
61}
62
63#[derive(Debug, Serialize)]
64pub struct CheckReport {
65 pub product: String,
66 pub channel: String,
67 pub branch: String,
68 pub source: String,
69 pub manifest_format: String,
70 pub current_version: String,
71 pub latest_version: String,
72 pub update_available: bool,
73 pub platform_key: String,
74 pub artifact: String,
75 pub sha256: String,
76}
77
78#[derive(Debug, Serialize)]
79pub struct UpdateReport {
80 pub product: String,
81 pub channel: String,
82 pub source: String,
83 pub current_version: String,
84 pub latest_version: String,
85 pub install_dir: String,
86 pub artifact: String,
87 pub dry_run: bool,
88 pub updated: bool,
89 pub status: String,
90}
91
92#[derive(Debug, Copy, Clone, Eq, PartialEq)]
93pub enum VersionRelation {
94 UpdateAvailable,
95 UpToDate,
96 AheadOfChannel,
97}
98
99#[derive(Debug)]
100pub struct ResolvedRelease {
101 pub version: String,
102 pub target: String,
103 pub artifact: String,
104 pub sha256: String,
105}
106
107#[derive(Debug, Clone, Eq, PartialEq)]
108pub struct GithubReleaseInfo {
109 pub tag_name: String,
110 pub assets: Vec<GithubReleaseAssetInfo>,
111}
112
113#[derive(Debug, Clone, Eq, PartialEq)]
114pub struct GithubReleaseAssetInfo {
115 pub name: String,
116 pub browser_download_url: String,
117}
118
119#[derive(Debug, Clone, Eq, PartialEq)]
120pub struct GithubRepo {
121 pub owner: String,
122 pub name: String,
123 pub url: String,
124}
125
126impl GithubRepo {
127 pub fn parse(raw: &str) -> Result<Self, String> {
128 let value = raw.trim().trim_end_matches('/');
129 if value.is_empty() {
130 return Err("GitHub repository cannot be empty".to_string());
131 }
132
133 let (owner, name) = if let Some(rest) = value.strip_prefix("https://github.com/") {
134 parse_repo_segments(rest)?
135 } else if let Some(rest) = value.strip_prefix("http://github.com/") {
136 parse_repo_segments(rest)?
137 } else if value.contains('/') && !value.contains("://") {
138 parse_repo_segments(value)?
139 } else {
140 return Err(format!(
141 "unsupported GitHub repository reference '{}': use https://github.com/<owner>/<repo> or <owner>/<repo>",
142 raw
143 ));
144 };
145
146 Ok(Self {
147 url: format!("https://github.com/{owner}/{name}"),
148 owner,
149 name,
150 })
151 }
152
153 pub fn latest_release_api_url(&self) -> String {
154 format!(
155 "https://api.github.com/repos/{}/{}/releases/latest",
156 self.owner, self.name
157 )
158 }
159
160 pub fn tag_release_api_url(&self, tag: &str) -> String {
161 format!(
162 "https://api.github.com/repos/{}/{}/releases/tags/{}",
163 self.owner, self.name, tag
164 )
165 }
166}
167
168fn parse_repo_segments(raw: &str) -> Result<(String, String), String> {
169 let mut parts = raw
170 .split('/')
171 .filter(|segment| !segment.is_empty())
172 .take(2)
173 .map(str::to_string)
174 .collect::<Vec<_>>();
175 if parts.len() != 2 {
176 return Err(format!(
177 "invalid GitHub repository reference '{}': expected <owner>/<repo>",
178 raw
179 ));
180 }
181 if let Some(name) = parts.get_mut(1) {
182 if let Some(trimmed) = name.strip_suffix(".git") {
183 *name = trimmed.to_string();
184 }
185 }
186 Ok((parts.remove(0), parts.remove(0)))
187}
188
189#[derive(Debug, Clone, Eq, PartialEq)]
190pub enum UpdateTarget {
191 Product(UpdateProduct),
192 Auto,
193 Bins(Vec<String>),
194}
195
196#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
197#[serde(rename_all = "lowercase")]
198pub enum UpdateProduct {
199 Suite,
200 Wparse,
201 Wpgen,
202 Wprescue,
203 Wproj,
204}
205
206impl UpdateProduct {
207 pub fn as_str(self) -> &'static str {
208 match self {
209 Self::Suite => "suite",
210 Self::Wparse => "wparse",
211 Self::Wpgen => "wpgen",
212 Self::Wprescue => "wprescue",
213 Self::Wproj => "wproj",
214 }
215 }
216
217 pub fn bins(self) -> &'static [&'static str] {
218 match self {
219 Self::Suite => &["wparse", "wpgen", "wprescue", "wproj"],
220 Self::Wparse => &["wparse"],
221 Self::Wpgen => &["wpgen"],
222 Self::Wprescue => &["wprescue"],
223 Self::Wproj => &["wproj"],
224 }
225 }
226
227 pub fn owned_bins(self) -> Vec<String> {
228 self.bins().iter().map(|bin| (*bin).to_string()).collect()
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn parse_github_repo_from_full_url() {
238 let repo = GithubRepo::parse("https://github.com/wp-labs/wpl-check").unwrap();
239 assert_eq!(repo.owner, "wp-labs");
240 assert_eq!(repo.name, "wpl-check");
241 assert_eq!(repo.url, "https://github.com/wp-labs/wpl-check");
242 }
243
244 #[test]
245 fn parse_github_repo_from_short_form() {
246 let repo = GithubRepo::parse("wp-labs/wpl-check").unwrap();
247 assert_eq!(repo.owner, "wp-labs");
248 assert_eq!(repo.name, "wpl-check");
249 }
250}