Skip to main content

wp_self_update/
types.rs

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}